commit f426c9ab9675ba53f66885fd7e7403cd3924c24a parent 461ba7b9aef9e899084cdeda91f603d954b10b3b Author: Ruben Guerrero <rubengu@microsoft.com> Date: Thu, 5 Jan 2023 19:03:21 -0800 Implement WinGetUserSettings cmdlets (#2776) * Squash merge from my super branch * enable zip * add to uninstall too * More improvements * commands in one place * More tests * spelling * jobject * Minor fix * Hashtable: * convert * :) Diffstat:
55 files changed, 2919 insertions(+), 926 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -159,6 +159,7 @@ ISQ ISVs itr IWin +JArray jdk jfearn JObject @@ -166,6 +167,8 @@ jpalardy JREs jrsoftware jsoncpp +JToken +JValue KNOWNFOLDERID ktf ldcase @@ -288,6 +291,7 @@ rosoft rowids RRF rrr +runspace runtimeclass ryfu rzkzqaqjwj diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -21,6 +21,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.8" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <PackageReference Include="Microsoft.Msix.Utils" Version="2.1.1" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -44,48 +44,5 @@ namespace AppInstallerCLIE2ETests // to enable testing it by default. Until then, leaving this here... TestCommon.SetupTestSource(useGroupPolicyForTestSource); } - - /// <summary> - /// Configure experimental features. - /// </summary> - /// <param name="featureName">Feature name.</param> - /// <param name="status">Status.</param> - public void ConfigureFeature(string featureName, bool status) - { - string localAppDataPath = Environment.GetEnvironmentVariable(Constants.LocalAppData); - JObject settingsJson = JObject.Parse(File.ReadAllText(Path.Combine(localAppDataPath, TestCommon.SettingsJsonFilePath))); - JObject experimentalFeatures = (JObject)settingsJson["experimentalFeatures"]; - experimentalFeatures[featureName] = status; - - File.WriteAllText(Path.Combine(localAppDataPath, TestCommon.SettingsJsonFilePath), settingsJson.ToString()); - } - - /// <summary> - /// Configure the install behavior. - /// </summary> - /// <param name="settingName">Setting name.</param> - /// <param name="value">Setting value.</param> - public void ConfigureInstallBehavior(string settingName, string value) - { - string localAppDataPath = Environment.GetEnvironmentVariable(Constants.LocalAppData); - JObject settingsJson = JObject.Parse(File.ReadAllText(Path.Combine(localAppDataPath, TestCommon.SettingsJsonFilePath))); - JObject installBehavior = (JObject)settingsJson["installBehavior"]; - installBehavior[settingName] = value; - - File.WriteAllText(Path.Combine(localAppDataPath, TestCommon.SettingsJsonFilePath), settingsJson.ToString()); - } - - /// <summary> - /// Initialize all features. - /// </summary> - /// <param name="status">Initialized feature value.</param> - public void InitializeAllFeatures(bool status) - { - this.ConfigureFeature("experimentalArg", status); - this.ConfigureFeature("experimentalCmd", status); - this.ConfigureFeature("dependencies", status); - this.ConfigureFeature("directMSI", status); - this.ConfigureFeature("openLogsArgument", status); - } } } diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -19,7 +19,7 @@ namespace AppInstallerCLIE2ETests [SetUp] public void Setup() { - this.InitializeAllFeatures(false); + WinGetSettingsHelper.InitializeAllFeatures(false); } /// <summary> @@ -28,7 +28,7 @@ namespace AppInstallerCLIE2ETests [TearDown] public void TearDown() { - this.InitializeAllFeatures(false); + WinGetSettingsHelper.InitializeAllFeatures(false); } /// <summary> @@ -49,10 +49,10 @@ namespace AppInstallerCLIE2ETests [Test] public void EnableExperimentalFeatures() { - this.ConfigureFeature("experimentalArg", true); - this.ConfigureFeature("experimentalCmd", true); - this.ConfigureFeature("directMSI", true); - this.ConfigureFeature("openLogsArgument", true); + WinGetSettingsHelper.ConfigureFeature("experimentalArg", true); + WinGetSettingsHelper.ConfigureFeature("experimentalCmd", true); + WinGetSettingsHelper.ConfigureFeature("directMSI", true); + WinGetSettingsHelper.ConfigureFeature("openLogsArgument", true); var result = TestCommon.RunAICLICommand("features", string.Empty); Assert.True(result.StdOut.Contains("Enabled")); } diff --git a/src/AppInstallerCLIE2ETests/GroupPolicy.cs b/src/AppInstallerCLIE2ETests/GroupPolicy.cs @@ -20,7 +20,7 @@ namespace AppInstallerCLIE2ETests [SetUp] public void Setup() { - this.InitializeAllFeatures(false); + WinGetSettingsHelper.InitializeAllFeatures(false); GroupPolicyHelper.DeleteExistingPolicies(); } @@ -30,7 +30,7 @@ namespace AppInstallerCLIE2ETests [TearDown] public void TearDown() { - this.InitializeAllFeatures(false); + WinGetSettingsHelper.InitializeAllFeatures(false); GroupPolicyHelper.DeleteExistingPolicies(); } @@ -62,7 +62,7 @@ namespace AppInstallerCLIE2ETests [Test] public void EnableExperimentalFeatures() { - this.ConfigureFeature("experimentalCmd", true); + WinGetSettingsHelper.ConfigureFeature("experimentalCmd", true); var result = TestCommon.RunAICLICommand("experimental", string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -403,7 +403,7 @@ namespace AppInstallerCLIE2ETests public void InstallPortable_UserScope() { string installDir = TestCommon.GetRandomTestDir(); - this.ConfigureInstallBehavior(Constants.PortablePackageUserRoot, installDir); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.PortablePackageUserRoot, installDir); string packageId, commandAlias, fileName, packageDirName, productCode; packageId = "AppInstallerTest.TestPortableExe"; @@ -411,7 +411,7 @@ namespace AppInstallerCLIE2ETests commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; var result = TestCommon.RunAICLICommand("install", $"{packageId} --scope user"); - this.ConfigureInstallBehavior(Constants.PortablePackageUserRoot, string.Empty); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.PortablePackageUserRoot, string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); @@ -424,7 +424,7 @@ namespace AppInstallerCLIE2ETests public void InstallPortable_MachineScope() { string installDir = TestCommon.GetRandomTestDir(); - this.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, installDir); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, installDir); string packageId, commandAlias, fileName, packageDirName, productCode; packageId = "AppInstallerTest.TestPortableExe"; @@ -432,7 +432,7 @@ namespace AppInstallerCLIE2ETests commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; var result = TestCommon.RunAICLICommand("install", $"{packageId} --scope machine"); - this.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, string.Empty); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Successfully installed")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.Machine); diff --git a/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs b/src/AppInstallerCLIE2ETests/PowerShell/PowerShellHost.cs @@ -0,0 +1,101 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellHost.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests.PowerShell +{ + using System; + using System.Collections; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using Microsoft.PowerShell; + using NUnit.Framework; + + /// <summary> + /// Helper class to run powershell commands. + /// </summary> + internal class PowerShellHost : IDisposable + { + private readonly Runspace runspace = null; + + private bool disposed = false; + + /// <summary> + /// Initializes a new instance of the <see cref="PowerShellHost"/> class. + /// </summary> + public PowerShellHost() + { + InitialSessionState initialSessionState = InitialSessionState.CreateDefault(); + initialSessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted; + initialSessionState.ImportPSModule(new string[] + { + TestCommon.PowerShellModulePath, + }); + + this.runspace = RunspaceFactory.CreateRunspace(initialSessionState); + this.runspace.Open(); + this.VerifyErrorState(); + + this.PowerShell = PowerShell.Create(this.runspace); + } + + /// <summary> + /// Finalizes an instance of the <see cref="PowerShellHost"/> class. + /// </summary> + ~PowerShellHost() => this.Dispose(false); + + /// <summary> + /// Gets PowerShell. + /// </summary> + public PowerShell PowerShell { get; private set; } = null; + + /// <summary> + /// Dispose. + /// </summary> + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Protected implementation of dispose pattern. + /// </summary> + /// <param name="disposing">Dispose.</param> + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.PowerShell.Dispose(); + this.runspace.Dispose(); + } + + this.disposed = true; + } + } + + /// <summary> + /// The most common error is that the module was not found. + /// </summary> + private void VerifyErrorState() + { + var errors = (ArrayList)this.runspace.SessionStateProxy.PSVariable.GetValue("Error"); + + if (errors.Count > 0) + { + string errorMessage = "PSVariable Error:"; + foreach (var error in errors) + { + errorMessage += Environment.NewLine + ((ErrorRecord)error).Exception.Message; + } + + TestContext.Error.WriteLine(errorMessage); + throw new Exception(errorMessage); + } + } + } +} diff --git a/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs b/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs @@ -7,18 +7,21 @@ namespace AppInstallerCLIE2ETests.PowerShell { using System; + using System.Collections; using System.Diagnostics; using System.Linq; + using System.Management.Automation; using NUnit.Framework; /// <summary> /// Basic E2E smoke tests for verifying the behavior of the PowerShell Microsoft.WinGet.Client module cmdlets. - /// Running the x86 PowerShell Module requires PowerShell Core (x86). These tests currently only target PowerShell Core (x64). + /// Running the x86 PowerShell Module requires PowerShell Core (x86). These tests currently only target PowerShell Core (x64) + /// in the CI/CD pipeline. /// </summary> [Category("PowerShell")] public class WinGetClientModule { - // TODO: Consider using Pester framework for conducting more extensive PowerShell module tests. + // TODO: Consider using Pester framework for conducting more extensive PowerShell module tests or move to Powershell Host. /// <summary> /// Set setup. @@ -45,6 +48,7 @@ namespace AppInstallerCLIE2ETests.PowerShell } TestCommon.RunAICLICommand("source remove", $"{Constants.TestSourceName}"); + WinGetSettingsHelper.InitializeWingetSettings(); } /// <summary> @@ -197,9 +201,935 @@ namespace AppInstallerCLIE2ETests.PowerShell Assert.IsTrue(serverProcessExit, $"{Constants.WindowsPackageManagerServer} failed to terminate after creating COM object."); } + /// <summary> + /// Test Get-WinGetUserSettings. + /// </summary> + [Test] + public void GetWinGetUserSettings() + { + var ogSettings = @"{ + ""visual"": { + ""progressBar"": ""rainbow"" + }, + ""experimentalFeatures"": { + ""experimentalArg"": false, + ""experimentalCmd"": true + } +}"; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Get-WinGetUserSettings") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<Hashtable>(result[0].BaseObject); + } + + /// <summary> + /// Test Get-WinGetUserSettings when the local settings file is not a json. + /// </summary> + [Test] + public void GetWinGetUserSettings_BadJsonFile() + { + WinGetSettingsHelper.SetWingetSettings("Hi, im not a json. Thank you, Test."); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + + var cmdletException = Assert.Throws<CmdletInvocationException>( + () => powerShellHost.PowerShell + .AddCommand("Get-WinGetUserSettings") + .Invoke()); + + // If we reference Microsoft.WinGet.Client to this project PowerShell host fails with + // System.Management.Automation.CmdletInvocationException : Operation is not supported on this platform. (0x80131539) + // System.PlatformNotSupportedException : Operation is not supported on this platform. (0x80131539) + // trying to load the runspace. This is most probably because the same dll is already loaded. + // Check the type the long way. + dynamic exception = cmdletException.InnerException; + Assert.AreEqual(exception.GetType().ToString(), "Microsoft.WinGet.Client.Exceptions.UserSettingsReadException"); + } + + /// <summary> + /// Test Test-WinGetUserSettings. Settings are equal. + /// </summary> + [Test] + public void TestWinGetUserSettings_Equal() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", ogSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsTrue((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings. Settings are equal. Ignore schema. + /// </summary> + [Test] + public void TestWinGetUserSettings_Equal_Schema() + { + var ogSettings = new Hashtable() + { + { + "$schema", + "https://aka.ms/winget-settings.schema.json" + }, + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsTrue((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings. Settings are not equal. + /// </summary> + [Test] + public void TestWinGetUserSettings_NotEqual() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "rainbow" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings. Local settings has more properties. + /// </summary> + [Test] + public void TestWinGetUserSettings_MoreSettingsLocal() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings. Input has more properties. + /// </summary> + [Test] + public void TestWinGetUserSettings_MoreSettingsInput() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell.AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings. IgnoreNotSet. + /// They are equal. + /// </summary> + [Test] + public void TestWinGetUserSettings_Equal_IgnoreNotSet() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", ogSettings) + .AddParameter("IgnoreNotSet") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsTrue((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings IgnoreNotSet. + /// Ignore comparing properties that are not set in the input. + /// </summary> + [Test] + public void TestWinGetUserSettings_MoreSettingsLocal_IgnoreNotSet() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("IgnoreNotSet") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsTrue((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings IgnoreNotSet. + /// Local settings doesnt have some properties. + /// </summary> + [Test] + public void TestWinGetUserSettings_MoreSettingsInput_IgnoreNotSet() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("IgnoreNotSet") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings IgnoreNotSet. + /// DeepEquals fails, but we should still fail at experimentalArg. + /// </summary> + [Test] + public void TestWinGetUserSettings_DifferentValue_IgnoreNotSet() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("IgnoreNotSet") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings IgnoreNotSet. + /// DeepEquals fails, but we should still fail at comparing the array. + /// </summary> + [Test] + public void TestWinGetUserSettings_ArrayDifferent_IgnoreNotSet() + { + var ogSettings = new Hashtable() + { + { + "installBehavior", + new Hashtable() + { + { + "preferences", + new Hashtable() + { + { "architectures", new string[] { "x64", "x86" } }, + } + }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "installBehavior", + new Hashtable() + { + { + "preferences", + new Hashtable() + { + { "architectures", new string[] { "x64", "arm64" } }, + } + }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("IgnoreNotSet") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings IgnoreNotSet. + /// DeepEquals fails, but we should still fail at experimentalArg because is an int. + /// </summary> + [Test] + public void TestWinGetUserSettings_DifferentValueType_IgnoreNotSet() + { + var ogSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", 4 }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("IgnoreNotSet") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Test-WinGetUserSettings. + /// Settings file is not a json. + /// </summary> + [Test] + public void TestWinGetUserSettings_BadJsonFile() + { + WinGetSettingsHelper.SetWingetSettings("Hi, im not a json. Thank you, Test."); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Test-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<bool>(result[0].BaseObject); + Assert.IsFalse((bool)result[0].BaseObject); + } + + /// <summary> + /// Test Set-WinGetUserSettings. + /// </summary> + [Test] + public void SetWinGetUserSettings_Overwrite() + { + var ogSettings = new Hashtable() + { + { + "$schema", + "https://aka.ms/winget-settings.schema.json" + }, + { + "source", + new Hashtable() + { + { "autoUpdateIntervalInMinutes", 3 }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Set-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<Hashtable>(result[0].BaseObject); + var settingsResult = result[0].BaseObject as Hashtable; + + Assert.True(settingsResult.ContainsKey("$schema")); + Assert.False(settingsResult.ContainsKey("source")); + Assert.True(settingsResult.ContainsKey("experimentalFeatures")); + Assert.True(settingsResult.ContainsKey("visual")); + } + + /// <summary> + /// Test Set-WinGetUserSettings. Merge local settings with input. + /// </summary> + [Test] + public void SetWinGetUserSettings_Merge() + { + var ogSettings = new Hashtable() + { + { + "source", + new Hashtable() + { + { "autoUpdateIntervalInMinutes", 3 }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Set-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("Merge") + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<Hashtable>(result[0].BaseObject); + var settingsResult = result[0].BaseObject as Hashtable; + + Assert.True(settingsResult.ContainsKey("$schema")); + Assert.True(settingsResult.ContainsKey("source")); + Assert.True(settingsResult.ContainsKey("experimentalFeatures")); + Assert.True(settingsResult.ContainsKey("visual")); + } + + /// <summary> + /// Test Set-WinGetUserSettings when the local settings file already have the schema property. It shouldn't + /// be added twice. + /// </summary> + [Test] + public void SetWinGetUserSettings_Schema() + { + var ogSettings = new Hashtable() + { + { + "$schema", + "https://aka.ms/winget-settings.schema.json" + }, + { + "source", + new Hashtable() + { + { "autoUpdateIntervalInMinutes", 3 }, + } + }, + }; + + WinGetSettingsHelper.SetWingetSettings(ogSettings); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", true }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Set-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<Hashtable>(result[0].BaseObject); + var settingsResult = result[0].BaseObject as Hashtable; + + Assert.True(settingsResult.ContainsKey("$schema")); + Assert.False(settingsResult.ContainsKey("source")); + Assert.True(settingsResult.ContainsKey("experimentalFeatures")); + } + + /// <summary> + /// Test Set-WinGetUserSettings when the local settings file is not a json. + /// </summary> + [Test] + public void SetWinGetUserSettings_BadJsonFile() + { + WinGetSettingsHelper.SetWingetSettings("Hi, im not a json. Thank you, Test."); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + var result = powerShellHost.PowerShell + .AddCommand("Set-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .Invoke(); + + Assert.That(result, Has.Exactly(1).Items); + Assert.IsInstanceOf<Hashtable>(result[0].BaseObject); + var settingsResult = result[0].BaseObject as Hashtable; + + Assert.True(settingsResult.ContainsKey("$schema")); + Assert.True(settingsResult.ContainsKey("visual")); + } + + /// <summary> + /// Test Set-WinGetUserSettings when the local settings file is not a json. + /// </summary> + [Test] + public void SetWinGetUserSettings_BadJsonFile_Merge() + { + WinGetSettingsHelper.SetWingetSettings("Hi, im not a json. Thank you, Test."); + + var inputSettings = new Hashtable() + { + { + "visual", + new Hashtable() + { + { "progressBar", "retro" }, + } + }, + }; + + using var powerShellHost = new PowerShellHost(); + + var cmdletException = Assert.Throws<CmdletInvocationException>( + () => powerShellHost.PowerShell + .AddCommand("Set-WinGetUserSettings") + .AddParameter("UserSettings", inputSettings) + .AddParameter("Merge") + .Invoke()); + + // If we reference Microsoft.WinGet.Client to this project PowerShell host fails with + // System.Management.Automation.CmdletInvocationException : Operation is not supported on this platform. (0x80131539) + // System.PlatformNotSupportedException : Operation is not supported on this platform. (0x80131539) + // trying to load the runspace. This is most probably because the same dll is already loaded. + // Check the type the long way. + dynamic exception = cmdletException.InnerException; + Assert.AreEqual(exception.GetType().ToString(), "Microsoft.WinGet.Client.Exceptions.UserSettingsReadException"); + } + private bool IsRunning(string processName) { return Process.GetProcessesByName(processName).Length > 0; } } -} +}+ \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -113,7 +113,9 @@ namespace AppInstallerCLIE2ETests TestIndexSetup.GenerateTestDirectory(); - this.InitializeWingetSettings(); + TestCommon.SettingsJsonFilePath = WinGetSettingsHelper.GetUserSettingsPath(); + + WinGetSettingsHelper.InitializeWingetSettings(); } /// <summary> @@ -142,42 +144,6 @@ namespace AppInstallerCLIE2ETests } } - /// <summary> - /// Initialize settings. - /// </summary> - public void InitializeWingetSettings() - { - string localAppDataPath = Environment.GetEnvironmentVariable(Constants.LocalAppData); - - var settingsJson = new - { - experimentalFeatures = new - { - experimentalArg = false, - experimentalCmd = false, - dependencies = false, - directMSI = false, - openLogsArgument = false, - }, - debugging = new - { - enableSelfInitiatedMinidump = true, - }, - installBehavior = new - { - portablePackageUserRoot = string.Empty, - portablePackageMachineRoot = string.Empty, - }, - }; - - // Run winget one time to initialize settings directory - // when running in unpackaged context - TestCommon.RunAICLICommand(string.Empty, "-v"); - - var serializedSettingsJson = JsonConvert.SerializeObject(settingsJson, Formatting.Indented); - File.WriteAllText(Path.Combine(localAppDataPath, TestCommon.SettingsJsonFilePath), serializedSettingsJson); - } - // Returns whether there's a change to the dev mode state after execution private bool EnableDevMode(bool enable) { diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -100,17 +100,9 @@ namespace AppInstallerCLIE2ETests public static string PowerShellModulePath { get; set; } /// <summary> - /// Gets the settings json path. + /// Gets or sets the settings json path. /// </summary> - public static string SettingsJsonFilePath - { - get - { - return PackagedContext ? - @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json" : - @"Microsoft\WinGet\Settings\settings.json"; - } - } + public static string SettingsJsonFilePath { get; set; } /// <summary> /// Run winget command. diff --git a/src/AppInstallerCLIE2ETests/UpgradeCommand.cs b/src/AppInstallerCLIE2ETests/UpgradeCommand.cs @@ -117,7 +117,7 @@ namespace AppInstallerCLIE2ETests public void UpgradePortableMachineScope() { string installDir = TestCommon.GetRandomTestDir(); - this.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, installDir); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, installDir); string packageId, commandAlias, fileName, packageDirName, productCode; packageId = "AppInstallerTest.TestPortableExe"; @@ -129,7 +129,7 @@ namespace AppInstallerCLIE2ETests Assert.True(result.StdOut.Contains("Successfully installed")); var result2 = TestCommon.RunAICLICommand("upgrade", $"{packageId} -v 2.0.0.0"); - this.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, string.Empty); + WinGetSettingsHelper.ConfigureInstallBehavior(Constants.PortablePackageMachineRoot, string.Empty); Assert.AreEqual(Constants.ErrorCode.S_OK, result2.ExitCode); Assert.True(result2.StdOut.Contains("Successfully installed")); TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.Machine); diff --git a/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/WinGetSettingsHelper.cs @@ -0,0 +1,142 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetSettingsHelper.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.Collections; + using System.IO; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// <summary> + /// Helper class to set winget settings. + /// </summary> + internal static class WinGetSettingsHelper + { + /// <summary> + /// Gets the user settings path by calling winget settings export. + /// </summary> + /// <returns>Expanded path for user settings.</returns> + public static string GetUserSettingsPath() + { + var result = TestCommon.RunAICLICommand("settings", "export"); + var output = result.StdOut; + var serialized = JObject.Parse(output); + return (string)serialized.GetValue("userSettingsFile"); + } + + /// <summary> + /// Initialize settings. + /// </summary> + public static void InitializeWingetSettings() + { + var settingsJson = new Hashtable() + { + { + "experimentalFeatures", + new Hashtable() + { + { "experimentalArg", false }, + { "experimentalCmd", false }, + { "dependencies", false }, + { "directMSI", false }, + { "openLogsArgument", false }, + } + }, + { + "debugging", + new Hashtable() + { + { "enableSelfInitiatedMinidump", false }, + } + }, + { + "installBehavior", + new Hashtable() + { + } + }, + }; + + // Run winget one time to initialize settings directory + // when running in unpackaged context + TestCommon.RunAICLICommand(string.Empty, "-v"); + + SetWingetSettings(settingsJson); + } + + /// <summary> + /// Converts a hashtable to json and writes to the settings file. + /// </summary> + /// <param name="settingsJson">Settings to set.</param> + public static void SetWingetSettings(Hashtable settingsJson) + { + SetWingetSettings(JsonConvert.SerializeObject(settingsJson, Formatting.Indented)); + } + + /// <summary> + /// Writes string to settings file. + /// </summary> + /// <param name="settings">Settings as string.</param> + public static void SetWingetSettings(string settings) + { + File.WriteAllText(TestCommon.SettingsJsonFilePath, settings); + } + + /// <summary> + /// Configure experimental features. + /// </summary> + /// <param name="featureName">Feature name.</param> + /// <param name="status">Status.</param> + public static void ConfigureFeature(string featureName, bool status) + { + JObject settingsJson = JObject.Parse(File.ReadAllText(TestCommon.SettingsJsonFilePath)); + + if (!settingsJson.ContainsKey("experimentalFeatures")) + { + settingsJson["experimentalFeatures"] = new JObject(); + } + + var experimentalFeatures = settingsJson["experimentalFeatures"]; + experimentalFeatures[featureName] = status; + + File.WriteAllText(TestCommon.SettingsJsonFilePath, settingsJson.ToString()); + } + + /// <summary> + /// Configure the install behavior. + /// </summary> + /// <param name="settingName">Setting name.</param> + /// <param name="value">Setting value.</param> + public static void ConfigureInstallBehavior(string settingName, string value) + { + JObject settingsJson = JObject.Parse(File.ReadAllText(TestCommon.SettingsJsonFilePath)); + + if (!settingsJson.ContainsKey("installBehavior")) + { + settingsJson["installBehavior"] = new JObject(); + } + + var installBehavior = settingsJson["installBehavior"]; + installBehavior[settingName] = value; + + File.WriteAllText(TestCommon.SettingsJsonFilePath, settingsJson.ToString()); + } + + /// <summary> + /// Initialize all features. + /// </summary> + /// <param name="status">Initialized feature value.</param> + public static void InitializeAllFeatures(bool status) + { + ConfigureFeature("experimentalArg", status); + ConfigureFeature("experimentalCmd", status); + ConfigureFeature("dependencies", status); + ConfigureFeature("directMSI", status); + ConfigureFeature("openLogsArgument", status); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseClientCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseClientCommand.cs @@ -0,0 +1,61 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BaseClientCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System; + using System.Collections.Generic; + using System.Runtime.InteropServices; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Exceptions; + using Microsoft.WinGet.Client.Factories; + + /// <summary> + /// This is the base class for all of the commands in this module that use the COM APIs. + /// </summary> + public abstract class BaseClientCommand : BaseCommand + { + static BaseClientCommand() + { + InitializeUndockedRegFreeWinRT(); + } + + /// <summary> + /// Gets the instance of the <see cref="ComObjectFactory" /> class. + /// </summary> + protected static Lazy<ComObjectFactory> ComObjectFactory { get; } = new (); + + /// <summary> + /// Gets the instance of the <see cref="PackageManager" /> class. + /// </summary> + protected static Lazy<PackageManager> PackageManager { get; } = new (() => ComObjectFactory.Value.CreatePackageManager()); + + /// <summary> + /// Retrieves the specified source or all sources if <paramref name="source" /> is null. + /// </summary> + /// <returns>A list of <see cref="PackageCatalogReference" /> instances.</returns> + /// <param name="source">The name of the source to retrieve. If null, then all sources are returned.</param> + /// <exception cref="ArgumentException">The source does not exist.</exception> + protected static IReadOnlyList<PackageCatalogReference> GetPackageCatalogReferences(string source) + { + if (source is null) + { + return PackageManager.Value.GetPackageCatalogs(); + } + else + { + return new List<PackageCatalogReference>() + { + PackageManager.Value.GetPackageCatalogByName(source) + ?? throw new InvalidSourceException(source), + }; + } + } + + [DllImport("winrtact.dll", EntryPoint = "winrtact_Initialize", ExactSpelling = true, PreserveSig = true)] + private static extern void InitializeUndockedRegFreeWinRT(); + } +}+ \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseCommand.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BaseCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Exceptions; + + /// <summary> + /// Base class for all Cmdlets. + /// </summary> + public abstract class BaseCommand : PSCmdlet + { + /// <summary> + /// Initializes a new instance of the <see cref="BaseCommand"/> class. + /// </summary> + public BaseCommand() + : base() + { + if (Utilities.ExecutingAsSystem) + { + throw new ExecuteAsSystemException(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseFinderCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseFinderCommand.cs @@ -0,0 +1,212 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BaseFinderCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Reflection; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Attributes; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Exceptions; + + /// <summary> + /// This is the base class for all commands that might need to search for a package. It contains an initial + /// set of parameters that corresponds to the intersection of i.e., the "install" and "search" commands. + /// </summary> + public abstract class BaseFinderCommand : BaseClientCommand + { + /// <summary> + /// Gets or sets the field that is matched against the identifier of a package. + /// </summary> + [Filter(Field = PackageMatchField.Id)] + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public string Id { get; set; } + + /// <summary> + /// Gets or sets the field that is matched against the name of a package. + /// </summary> + [Filter(Field = PackageMatchField.Name)] + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the field that is matched against the name of a package. + /// </summary> + [Filter(Field = PackageMatchField.Moniker)] + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public string Moniker { get; set; } + + /// <summary> + /// Gets or sets the name of the source to search for packages. If null, then all sources are searched. + /// </summary> + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public string Source { get; set; } + + /// <summary> + /// Gets or sets the strings that match against every field of a package. + /// </summary> + [Parameter( + ParameterSetName = Constants.FoundSet, + Position = 0, + ValueFromPipelineByPropertyName = true, + ValueFromRemainingArguments = true)] + public string[] Query { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to match exactly against package fields. + /// </summary> + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public SwitchParameter Exact { get; set; } + + private string QueryAsJoinedString + { + get + { + return this.Query is null + ? null + : string.Join(" ", this.Query); + } + } + + /// <summary> + /// Returns a <see cref="PackageFieldMatchOption" /> based on a parameter. + /// </summary> + /// <returns>A <see cref="PackageFieldMatchOption" /> value.</returns> + protected virtual PackageFieldMatchOption GetExactAsMatchOption() + { + return this.Exact.ToBool() + ? PackageFieldMatchOption.Equals + : PackageFieldMatchOption.ContainsCaseInsensitive; + } + + /// <summary> + /// Searches for packages based on the configured parameters. + /// </summary> + /// <param name="behavior">The <see cref="CompositeSearchBehavior" /> value.</param> + /// <param name="limit">The limit on the number of matches returned.</param> + /// <returns>A list of <see cref="MatchResult" /> objects.</returns> + protected IReadOnlyList<MatchResult> FindPackages( + CompositeSearchBehavior behavior, + uint limit) + { + PackageCatalog catalog = this.GetPackageCatalog(behavior); + FindPackagesOptions options = this.GetFindPackagesOptions(limit); + return GetMatchResults(catalog, options); + } + + private static void SetQueryInFindPackagesOptions( + ref FindPackagesOptions options, + PackageFieldMatchOption match, + string value) + { + var selector = ComObjectFactory.Value.CreatePackageMatchFilter(); + selector.Field = PackageMatchField.CatalogDefault; + selector.Value = value ?? string.Empty; + selector.Option = match; + options.Selectors.Add(selector); + } + + private static void AddFilterToFindPackagesOptionsIfNotNull( + ref FindPackagesOptions options, + PackageMatchField field, + PackageFieldMatchOption match, + string value) + { + if (value != null) + { + var filter = ComObjectFactory.Value.CreatePackageMatchFilter(); + filter.Field = field; + filter.Value = value; + filter.Option = match; + options.Filters.Add(filter); + } + } + + private static IReadOnlyList<MatchResult> GetMatchResults( + PackageCatalog catalog, + FindPackagesOptions options) + { + FindPackagesResult result = catalog.FindPackages(options); + if (result.Status == FindPackagesResultStatus.Ok) + { + return result.Matches; + } + else + { + throw new FindPackagesException(result.Status); + } + } + + private PackageCatalog GetPackageCatalog(CompositeSearchBehavior behavior) + { + PackageCatalogReference reference = this.GetPackageCatalogReference(behavior); + ConnectResult result = reference.Connect(); + if (result.Status == ConnectResultStatus.Ok) + { + return result.PackageCatalog; + } + else + { + throw new CatalogConnectException(); + } + } + + private PackageCatalogReference GetPackageCatalogReference(CompositeSearchBehavior behavior) + { + CreateCompositePackageCatalogOptions options = ComObjectFactory.Value.CreateCreateCompositePackageCatalogOptions(); + IReadOnlyList<PackageCatalogReference> references = GetPackageCatalogReferences(this.Source); + for (var i = 0; i < references.Count; i++) + { + options.Catalogs.Add(references[i]); + } + + options.CompositeSearchBehavior = behavior; + return PackageManager.Value.CreateCompositePackageCatalog(options); + } + + private FindPackagesOptions GetFindPackagesOptions(uint limit) + { + var options = ComObjectFactory.Value.CreateFindPackagesOptions(); + SetQueryInFindPackagesOptions(ref options, this.GetExactAsMatchOption(), this.QueryAsJoinedString); + this.AddAttributedFiltersToFindPackagesOptions(ref options, this.GetExactAsMatchOption()); + options.ResultLimit = limit; + return options; + } + + private void AddAttributedFiltersToFindPackagesOptions( + ref FindPackagesOptions options, + PackageFieldMatchOption match) + { + IEnumerable<PropertyInfo> properties = this.GetType() + .GetProperties() + .Where(property => Attribute.IsDefined(property, typeof(FilterAttribute))); + + foreach (PropertyInfo info in properties) + { + if (info.GetCustomAttribute(typeof(FilterAttribute), true) is FilterAttribute attribute) + { + PackageMatchField field = attribute.Field; + string value = info.GetValue(this, null) as string; + AddFilterToFindPackagesOptionsIfNotNull(ref options, field, match, value); + } + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseFinderExtendedCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseFinderExtendedCommand.cs @@ -0,0 +1,59 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BaseFinderExtendedCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Attributes; + using Microsoft.WinGet.Client.Common; + + /// <summary> + /// This is the base class for the commands whose sole purpose is to filter a list of packages i.e., + /// the "search" and "list" commands. This class contains an extended set of parameters suited for + /// that purpose. + /// </summary> + public abstract class BaseFinderExtendedCommand : BaseFinderCommand + { + /// <summary> + /// Gets or sets the filter that is matched against the tags of the package. + /// </summary> + [Filter(Field = PackageMatchField.Tag)] + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public string Tag { get; set; } + + /// <summary> + /// Gets or sets the filter that is matched against the commands of the package. + /// </summary> + [Filter(Field = PackageMatchField.Command)] + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public string Command { get; set; } + + /// <summary> + /// Gets or sets the maximum number of results returned. + /// </summary> + [ValidateRange(Constants.CountLowerBound, Constants.CountUpperBound)] + [Parameter( + ParameterSetName = Constants.FoundSet, + ValueFromPipelineByPropertyName = true)] + public uint Count { get; set; } + + /// <summary> + /// Searches for packages from configured sources. + /// </summary> + /// <param name="behavior">A <see cref="CompositeSearchBehavior" /> value.</param> + /// <returns>A list of <see cref="MatchResult" /> objects.</returns> + protected IReadOnlyList<MatchResult> FindPackages(CompositeSearchBehavior behavior) + { + return this.FindPackages(behavior, this.Count); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseInstallCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseInstallCommand.cs @@ -0,0 +1,153 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BaseInstallCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System.IO; + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Helpers; + using Windows.Foundation; + + /// <summary> + /// This is the base class for all commands that parse a <see cref="FindPackagesOptions" /> result + /// from the provided parameters i.e., the "install" and "upgrade" commands. + /// </summary> + public abstract class BaseInstallCommand : BasePackageCommand + { + private string location; + + /// <summary> + /// Gets or sets the mode to manipulate the package with. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public PackageInstallMode Mode { get; set; } = PackageInstallMode.Default; + + /// <summary> + /// Gets or sets the override arguments to be passed on to the installer. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Override { get; set; } + + /// <summary> + /// Gets or sets the installation location. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Location + { + get => this.location; + set + { + this.location = Path.IsPathRooted(value) + ? value + : this.SessionState.Path.CurrentFileSystemLocation + @"\" + value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether to skip the installer hash validation check. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AllowHashMismatch { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to continue upon non security related failures. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Force { get; set; } + + /// <summary> + /// Gets or sets the optional HTTP Header to pass on to the REST Source. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Header { get; set; } + + /// <summary> + /// Gets the install options from the configured parameters. + /// </summary> + /// <param name="version">The <see cref="PackageVersionId" /> to install.</param> + /// <returns>An <see cref="InstallOptions" /> instance.</returns> + protected virtual InstallOptions GetInstallOptions(PackageVersionId version) + { + InstallOptions options = ComObjectFactory.Value.CreateInstallOptions(); + options.AllowHashMismatch = this.AllowHashMismatch.ToBool(); + options.Force = this.Force.ToBool(); + options.PackageInstallMode = this.Mode; + if (version != null) + { + options.PackageVersionId = version; + } + + if (this.Log != null) + { + options.LogOutputPath = this.Log; + } + + if (this.Override != null) + { + options.ReplacementInstallerArguments = this.Override; + } + + if (this.Location != null) + { + options.PreferredInstallLocation = this.Location; + } + + if (this.Header != null) + { + options.AdditionalPackageCatalogArguments = this.Header; + } + + return options; + } + + /// <summary> + /// Registers callbacks on an asynchronous operation and waits for the results. + /// </summary> + /// <param name="operation">The asynchronous operation.</param> + /// <param name="activity">A <see cref="string" /> instance.</param> + /// <returns>A <see cref="InstallResult" /> instance.</returns> + protected InstallResult RegisterCallbacksAndWait( + IAsyncOperationWithProgress<InstallResult, InstallProgress> operation, + string activity) + { + WriteProgressAdapter adapter = new (this); + operation.Progress = (context, progress) => + { + ProgressRecord record = new (1, activity, progress.State.ToString()) + { + RecordType = ProgressRecordType.Processing, + }; + + if (progress.State == PackageInstallProgressState.Downloading && progress.BytesRequired != 0) + { + record.StatusDescription = $"{progress.BytesDownloaded / 1000000.0f:0.0} MB / {progress.BytesRequired / 1000000.0f:0.0} MB"; + record.PercentComplete = (int)(progress.DownloadProgress * 100); + } + else if (progress.State == PackageInstallProgressState.Installing) + { + record.PercentComplete = (int)(progress.InstallationProgress * 100); + } + + adapter.WriteProgress(record); + }; + operation.Completed = (context, status) => + { + adapter.WriteProgress(new ProgressRecord(1, activity, status.ToString()) + { + RecordType = ProgressRecordType.Completed, + }); + adapter.Completed = true; + }; + System.Console.CancelKeyPress += (sender, e) => + { + operation.Cancel(); + }; + adapter.Wait(); + return operation.GetResults(); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BasePackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BasePackageCommand.cs @@ -0,0 +1,134 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BasePackageCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Management.Automation; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Exceptions; + + /// <summary> + /// This is the base class for commands which operate on a specific package and version i.e., + /// the "install", "uninstall", and "upgrade" commands. + /// </summary> + public abstract class BasePackageCommand : BaseFinderCommand + { + private string log; + + /// <summary> + /// Gets or sets the package to directly install. + /// </summary> + /// <remarks> + /// Must match the name of the <see cref="CatalogPackage" /> field on the <see cref="MatchResult" /> class. + /// </remarks> + [Alias("InputObject")] + [ValidateNotNull] + [Parameter( + ParameterSetName = Constants.GivenSet, + Position = 0, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public CatalogPackage CatalogPackage { get; set; } + + /// <summary> + /// Gets or sets the version to install. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Version { get; set; } + + /// <summary> + /// Gets or sets the path to the logging file. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public string Log + { + get => this.log; + set + { + this.log = Path.IsPathRooted(value) + ? value + : this.SessionState.Path.CurrentFileSystemLocation + @"\" + value; + } + } + + /// <inheritdoc /> + protected override PackageFieldMatchOption GetExactAsMatchOption() + { + return this.Exact.ToBool() + ? PackageFieldMatchOption.Equals + : PackageFieldMatchOption.EqualsCaseInsensitive; + } + + /// <summary> + /// Executes a command targeting a specific package version. + /// </summary> + /// <param name="behavior">The <see cref="CompositeSearchBehavior" /> value.</param> + /// <param name="callback">The method to call after retrieving the package and version to operate upon.</param> + protected void GetPackageAndExecute( + CompositeSearchBehavior behavior, + Action<CatalogPackage, PackageVersionId> callback) + { + CatalogPackage package = this.GetCatalogPackage(behavior); + PackageVersionId version = this.GetPackageVersionId(package); + if (this.ShouldProcess(package.ToString(version))) + { + callback(package, version); + } + } + + private CatalogPackage GetCatalogPackage(CompositeSearchBehavior behavior) + { + if (this.ParameterSetName == Constants.GivenSet) + { + // The package was already provided via a parameter or the pipeline. + return this.CatalogPackage; + } + else + { + IReadOnlyList<MatchResult> results = this.FindPackages(behavior, 0); + if (results.Count == 1) + { + // Exactly one package matched, so we can just return it. + return results[0].CatalogPackage; + } + else if (results.Count == 0) + { + // No packages matched, we need to throw an error. + throw new NoPackageFoundException(); + } + else + { + // Too many packages matched! The user needs to refine their input. + throw new VagueCriteriaException(results); + } + } + } + + private PackageVersionId GetPackageVersionId(CatalogPackage package) + { + if (this.Version != null) + { + for (var i = 0; i < package.AvailableVersions.Count; i++) + { + if (package.AvailableVersions[i].Version.CompareTo(this.Version) == 0) + { + return package.AvailableVersions[i]; + } + } + + throw new InvalidVersionException(this.Version); + } + else + { + return null; + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseUserSettingsCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseUserSettingsCommand.cs @@ -0,0 +1,133 @@ +// ----------------------------------------------------------------------------- +// <copyright file="BaseUserSettingsCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands.Common +{ + using System; + using System.Collections; + using System.IO; + using System.Management.Automation; + using Microsoft.PowerShell.Commands; + using Microsoft.WinGet.Client.Exceptions; + using Microsoft.WinGet.Client.Helpers; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// <summary> + /// Base command for user settings cmdlets. + /// </summary> + public abstract class BaseUserSettingsCommand : BaseCommand + { + /// <summary> + /// The schema key. + /// </summary> + protected const string SchemaKey = "$schema"; + + /// <summary> + /// The default value of the schema property. + /// </summary> + protected const string SchemaValue = "https://aka.ms/winget-settings.schema.json"; + + /// <summary> + /// Gets the path for the winget settings. + /// </summary> + protected static string WinGetSettingsFilePath + { + get + { + return GetUserSettingsPath(); + } + } + + /// <summary> + /// Converts a Hashtable to a JObject object. + /// </summary> + /// <param name="hashtable">Hashtable.</param> + /// <returns>JObject.</returns> + protected static JObject HashtableToJObject(Hashtable hashtable) + { + return (JObject)JToken.FromObject(hashtable); + } + + /// <summary> + /// Returns the contents of the settings file as Hashtable. + /// </summary> + /// <returns>Contents of settings file.</returns> + protected Hashtable GetLocalSettingsAsHashtable() + { + var content = File.Exists(WinGetSettingsFilePath) ? + File.ReadAllText(WinGetSettingsFilePath) : + string.Empty; + + return this.ConvertToHashtable(content); + } + + /// <summary> + /// Converts the current local settings file into a JObject object. + /// </summary> + /// <returns>User settings as JObject.</returns> + protected JObject LocalSettingsFileToJObject() + { + try + { + return File.Exists(WinGetSettingsFilePath) ? + JObject.Parse(File.ReadAllText(WinGetSettingsFilePath)) : + new JObject(); + } + catch (JsonReaderException e) + { + this.WriteDebug(e.Message); + throw new UserSettingsReadException(e); + } + } + + /// <summary> + /// Uses Powershell ConvertFrom-Json to convert a JSON to a Hashtable. + /// </summary> + /// <param name="content">Content.</param> + /// <returns>Hashtable.</returns> + protected Hashtable ConvertToHashtable(string content) + { + if (string.IsNullOrEmpty(content)) + { + return new Hashtable(); + } + +#if POWERSHELL_WINDOWS + throw new PSNotImplementedException(); +#else + // Powershell's documentation says that the object being return is either a PSObject + // or a Hashtable depending on the value of returnHashtable, but is not true. The return + // type is a PSObject and the BaseObject is either a OrderedHashtable or a PSCustomObject + // depending on returnHashtable. + try + { + var result = JsonObject.ConvertFromJson(content, returnHashtable: true, out ErrorRecord error) as PSObject; + if (error is not null) + { + throw new UserSettingsReadException(error.Exception); + } + + return result.BaseObject as Hashtable; + } + catch (Exception e) + { + throw new UserSettingsReadException(e); + } +#endif + } + + private static string GetUserSettingsPath() + { + var wingetCliWrapper = new WingetCLIWrapper(); + var settingsResult = wingetCliWrapper.RunCommand("settings", "export"); + + // Read the user settings file property. + var serialized = JObject.Parse(settingsResult.StdOut); + return (string)serialized.GetValue("userSettingsFile"); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/FindPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/FindPackageCommand.cs @@ -7,13 +7,14 @@ namespace Microsoft.WinGet.Client.Commands { using System.Management.Automation; - using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Commands.Common; using Microsoft.WinGet.Client.Common; /// <summary> /// Searches configured sources for packages. /// </summary> - [Cmdlet(VerbsCommon.Find, Constants.PackageNoun)] + [Cmdlet(VerbsCommon.Find, Constants.WinGetNouns.Package)] [OutputType(typeof(MatchResult))] public sealed class FindPackageCommand : BaseFinderExtendedCommand { diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/GetPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/GetPackageCommand.cs @@ -7,13 +7,14 @@ namespace Microsoft.WinGet.Client.Commands { using System.Management.Automation; - using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Commands.Common; using Microsoft.WinGet.Client.Common; /// <summary> /// Searches configured sources for packages. /// </summary> - [Cmdlet(VerbsCommon.Get, Constants.PackageNoun)] + [Cmdlet(VerbsCommon.Get, Constants.WinGetNouns.Package)] [OutputType(typeof(CatalogPackage))] public sealed class GetPackageCommand : BaseFinderExtendedCommand { diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/GetSourceCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/GetSourceCommand.cs @@ -7,13 +7,14 @@ namespace Microsoft.WinGet.Client.Commands { using System.Management.Automation; - using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Commands.Common; using Microsoft.WinGet.Client.Common; /// <summary> /// Retrieves the list of configured sources. /// </summary> - [Cmdlet(VerbsCommon.Get, Constants.SourceNoun)] + [Cmdlet(VerbsCommon.Get, Constants.WinGetNouns.Source)] [OutputType(typeof(PackageCatalogReference))] public sealed class GetSourceCommand : BaseClientCommand { diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/GetUserSettingsCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/GetUserSettingsCommand.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetUserSettingsCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System.Collections; + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + + /// <summary> + /// Gets winget's user settings. + /// </summary> + [Cmdlet(VerbsCommon.Get, Constants.WinGetNouns.UserSettings)] + [OutputType(typeof(Hashtable))] + public sealed class GetUserSettingsCommand : BaseUserSettingsCommand + { + /// <summary> + /// Writes the settings file contents. + /// </summary> + protected override void ProcessRecord() + { + this.WriteObject(this.GetLocalSettingsAsHashtable()); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/InstallPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/InstallPackageCommand.cs @@ -5,10 +5,12 @@ // ----------------------------------------------------------------------------- namespace Microsoft.WinGet.Client.Commands -{ +{ using System.Management.Automation; - using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Commands.Common; using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Properties; using Windows.System; /// <summary> @@ -16,7 +18,7 @@ namespace Microsoft.WinGet.Client.Commands /// </summary> [Cmdlet( VerbsLifecycle.Install, - Constants.PackageNoun, + Constants.WinGetNouns.Package, DefaultParameterSetName = Constants.FoundSet, SupportsShouldProcess = true)] [OutputType(typeof(InstallResult))] @@ -89,7 +91,7 @@ namespace Microsoft.WinGet.Client.Commands { var operation = PackageManager.Value.InstallPackageAsync(package, options); return this.RegisterCallbacksAndWait(operation, string.Format( - Utilities.ResourceManager.GetString("ProgressRecordActivityInstalling"), + Resources.ProgressRecordActivityInstalling, package.Name)); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/SetUserSettingsCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/SetUserSettingsCommand.cs @@ -0,0 +1,107 @@ +// ----------------------------------------------------------------------------- +// <copyright file="SetUserSettingsCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System; + using System.Collections; + using System.IO; + using System.Linq; + using System.Management.Automation; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// <summary> + /// Sets the specified user settings into the winget user settings. If the merge switch is on, merges current user + /// settings with the input settings. Otherwise, overwrites the input settings. + /// </summary> + [Cmdlet(VerbsCommon.Set, Constants.WinGetNouns.UserSettings)] + [OutputType(typeof(Hashtable))] + public sealed class SetUserSettingsCommand : BaseUserSettingsCommand + { + /// <summary> + /// Gets or sets the input user settings. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipelineByPropertyName = true)] + public Hashtable UserSettings { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to merge the current user settings and the input settings. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter Merge { get; set; } + + /// <summary> + /// Process input of cmdlet. + /// </summary> + protected override void ProcessRecord() + { + var newSettings = HashtableToJObject(this.UserSettings); + + // Merge settings. + if (this.Merge.ToBool()) + { + var currentSettings = this.LocalSettingsFileToJObject(); + + // To make the input settings triumph, they need to be merged into the existing settings. + currentSettings.Merge(newSettings, new JsonMergeSettings + { + MergeArrayHandling = MergeArrayHandling.Union, + MergeNullValueHandling = MergeNullValueHandling.Ignore, + }); + + newSettings = currentSettings; + } + + // Add schema if not there. + if (!newSettings.ContainsKey(SchemaKey)) + { + newSettings.Add(SchemaKey, SchemaValue); + } + + var orderedSettings = CreateAlphabeticallyOrderedJObject(newSettings); + + // Write settings. + var settingsJson = orderedSettings.ToString(Formatting.Indented); + File.WriteAllText( + WinGetSettingsFilePath, + settingsJson); + + this.WriteObject(this.ConvertToHashtable(settingsJson)); + } + + /// <summary> + /// Helper method to order alphabetically properties. Newtonsoft doesn't have a nice way + /// to do it via a custom JsonConverter. + /// </summary> + /// <param name="jObject">JObject.</param> + /// <returns>New ordered JObject.</returns> + private static JObject CreateAlphabeticallyOrderedJObject(JObject jObject) + { + JObject newJObject = new (); + var orderedProperties = jObject.Properties().OrderBy(p => p.Name, StringComparer.Ordinal); + foreach (var property in orderedProperties) + { + if (property.Value.Type == JTokenType.Object) + { + newJObject.Add( + property.Name, + CreateAlphabeticallyOrderedJObject((JObject)property.Value)); + } + else + { + newJObject.Add(property); + } + } + + return newJObject; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/TestUserSettingsCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/TestUserSettingsCommand.cs @@ -0,0 +1,137 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestUserSettingsCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Commands +{ + using System; + using System.Collections; + using System.Management.Automation; + using System.Management.Automation.Language; + using Microsoft.WinGet.Client.Commands.Common; + using Microsoft.WinGet.Client.Common; + using Newtonsoft.Json.Linq; + + /// <summary> + /// Compare the specified user settings with the winget user settings. + /// </summary> + [Cmdlet(VerbsDiagnostic.Test, Constants.WinGetNouns.UserSettings)] + [OutputType(typeof(bool))] + public sealed class TestUserSettingsCommand : BaseUserSettingsCommand + { + /// <summary> + /// Gets or sets the input user settings. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipelineByPropertyName = true)] + public Hashtable UserSettings { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to ignore comparing settings that are not part of the input. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter IgnoreNotSet { get; set; } + + /// <summary> + /// Process the cmdlet and writes the result of the comparison. + /// </summary> + protected override void ProcessRecord() + { + this.WriteObject(this.CompareUserSettings()); + } + + private bool CompareUserSettings() + { + try + { + var currentSettings = this.LocalSettingsFileToJObject(); + var newSettings = HashtableToJObject(this.UserSettings); + + // Don't fail because of the schema. + if (currentSettings.ContainsKey(SchemaKey)) + { + currentSettings.Remove(SchemaKey); + } + + if (newSettings.ContainsKey(SchemaKey)) + { + newSettings.Remove(SchemaKey); + } + + if (this.IgnoreNotSet.ToBool()) + { + return this.PartialDeepEquals(newSettings, currentSettings); + } + + return JToken.DeepEquals(newSettings, currentSettings); + } + catch (Exception e) + { + this.WriteDebug(e.Message); + return false; + } + } + + /// <summary> + /// Partially compares json. All properties and values of json must exist and have the same value + /// as otherJson. + /// This doesn't support deep JArray object comparison, but we don't have arrays of type object so far :). + /// </summary> + /// <param name="json">Main json.</param> + /// <param name="otherJson">otherJson.</param> + /// <returns>True is otherJson partially contains json.</returns> + private bool PartialDeepEquals(JToken json, JToken otherJson) + { + if (JToken.DeepEquals(json, otherJson)) + { + return true; + } + + // If they are a JValue (string, integer, date, etc) or they are a JArray and DeepEquals fails then not equal. + if ((json is JValue && otherJson is JValue) || + (json is JArray && otherJson is JArray)) + { + this.WriteDebug($"'{json.ToString(Newtonsoft.Json.Formatting.None)}' != " + + $"'{otherJson.ToString(Newtonsoft.Json.Formatting.None)}'"); + return false; + } + + // If its not the same type then don't bother. + if (json.Type != otherJson.Type) + { + this.WriteDebug($"Mismatch types '{json.ToString(Newtonsoft.Json.Formatting.None)}' " + + $"'{otherJson.ToString(Newtonsoft.Json.Formatting.None)}'"); + return false; + } + + // Look deeply. + if (json.Type == JTokenType.Object) + { + var jObject = (JObject)json; + var otherJObject = (JObject)otherJson; + + var properties = jObject.Properties(); + foreach (var property in properties) + { + // If the property is not there then give up. + if (!otherJObject.ContainsKey(property.Name)) + { + this.WriteDebug($"{property.Name} not found."); + return false; + } + + if (!this.PartialDeepEquals(property.Value, otherJObject.GetValue(property.Name))) + { + // Found inequality within a property. We are done. + return false; + } + } + } + + return true; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/UninstallPackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/UninstallPackageCommand.cs @@ -8,16 +8,18 @@ namespace Microsoft.WinGet.Client.Commands { using System; using System.Management.Automation; - using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Commands.Common; using Microsoft.WinGet.Client.Common; using Microsoft.WinGet.Client.Helpers; + using Microsoft.WinGet.Client.Properties; /// <summary> /// Uninstalls a package from the local system. /// </summary> [Cmdlet( VerbsLifecycle.Uninstall, - Constants.PackageNoun, + Constants.WinGetNouns.Package, DefaultParameterSetName = Constants.FoundSet, SupportsShouldProcess = true)] [OutputType(typeof(UninstallResult))] @@ -71,7 +73,7 @@ namespace Microsoft.WinGet.Client.Commands UninstallOptions options) { string activity = string.Format( - Utilities.ResourceManager.GetString("ProgressRecordActivityUninstalling"), + Resources.ProgressRecordActivityUninstalling, package.Name); var operation = PackageManager.Value.UninstallPackageAsync(package, options); diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/UpdatePackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/UpdatePackageCommand.cs @@ -7,15 +7,17 @@ namespace Microsoft.WinGet.Client.Commands { using System.Management.Automation; - using Microsoft.Management.Deployment; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Commands.Common; using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Properties; /// <summary> /// This commands updates a package from the pipeline or from the local system. /// </summary> [Cmdlet( VerbsData.Update, - Constants.PackageNoun, + Constants.WinGetNouns.Package, DefaultParameterSetName = Constants.FoundSet, SupportsShouldProcess = true)] [OutputType(typeof(InstallResult))] @@ -53,10 +55,12 @@ namespace Microsoft.WinGet.Client.Commands CatalogPackage package, InstallOptions options) { - var operation = PackageManager.Value.UpgradePackageAsync(package, options); - return this.RegisterCallbacksAndWait(operation, string.Format( - Utilities.ResourceManager.GetString("ProgressRecordActivityUpdating"), - package.Name)); + var operation = PackageManager.Value.UpgradePackageAsync(package, options); + return this.RegisterCallbacksAndWait( + operation, + string.Format( + Resources.ProgressRecordActivityUpdating, + package.Name)); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/BaseClientCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/BaseClientCommand.cs @@ -1,75 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="BaseClientCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Common -{ - using System; - using System.Collections.Generic; - using System.Management.Automation; - using System.Runtime.InteropServices; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Factories; - - /// <summary> - /// This is the base class for all of the commands in this module. - /// </summary> - public class BaseClientCommand : PSCmdlet - { - static BaseClientCommand() - { - InitializeUndockedRegFreeWinRT(); - } - - /// <summary> - /// Initializes a new instance of the <see cref="BaseClientCommand"/> class. - /// </summary> - public BaseClientCommand() - : base() - { - if (Utilities.ExecutingAsSystem) - { - throw new Exception(Utilities.ResourceManager.GetString("ExceptionSystemDisabled")); - } - } - - /// <summary> - /// Gets the instance of the <see cref="ComObjectFactory" /> class. - /// </summary> - protected static Lazy<ComObjectFactory> ComObjectFactory { get; } = new (); - - /// <summary> - /// Gets the instance of the <see cref="PackageManager" /> class. - /// </summary> - protected static Lazy<PackageManager> PackageManager { get; } = new (() => ComObjectFactory.Value.CreatePackageManager()); - - /// <summary> - /// Retrieves the specified source or all sources if <paramref name="source" /> is null. - /// </summary> - /// <returns>A list of <see cref="PackageCatalogReference" /> instances.</returns> - /// <param name="source">The name of the source to retrieve. If null, then all sources are returned.</param> - /// <exception cref="ArgumentException">The source does not exist.</exception> - protected static IReadOnlyList<PackageCatalogReference> GetPackageCatalogReferences(string source) - { - if (source is null) - { - return PackageManager.Value.GetPackageCatalogs(); - } - else - { - return new List<PackageCatalogReference>() - { - PackageManager.Value.GetPackageCatalogByName(source) - ?? throw new ArgumentException(string.Format( - Utilities.ResourceManager.GetString("ArgumentExceptionInvalidSource"), - source)), - }; - } - } - - [DllImport("winrtact.dll", EntryPoint = "winrtact_Initialize", ExactSpelling = true, PreserveSig = true)] - private static extern void InitializeUndockedRegFreeWinRT(); - } -}- \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/BaseFinderCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/BaseFinderCommand.cs @@ -1,212 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="BaseFinderCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Common -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Management.Automation; - using System.Reflection; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Attributes; - using Microsoft.WinGet.Client.Errors; - - /// <summary> - /// This is the base class for all commands that might need to search for a package. It contains an initial - /// set of parameters that corresponds to the intersection of i.e., the "install" and "search" commands. - /// </summary> - public class BaseFinderCommand : BaseClientCommand - { - /// <summary> - /// Gets or sets the field that is matched against the identifier of a package. - /// </summary> - [Filter(Field = PackageMatchField.Id)] - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public string Id { get; set; } - - /// <summary> - /// Gets or sets the field that is matched against the name of a package. - /// </summary> - [Filter(Field = PackageMatchField.Name)] - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public string Name { get; set; } - - /// <summary> - /// Gets or sets the field that is matched against the name of a package. - /// </summary> - [Filter(Field = PackageMatchField.Moniker)] - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public string Moniker { get; set; } - - /// <summary> - /// Gets or sets the name of the source to search for packages. If null, then all sources are searched. - /// </summary> - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public string Source { get; set; } - - /// <summary> - /// Gets or sets the strings that match against every field of a package. - /// </summary> - [Parameter( - ParameterSetName = Constants.FoundSet, - Position = 0, - ValueFromPipelineByPropertyName = true, - ValueFromRemainingArguments = true)] - public string[] Query { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether to match exactly against package fields. - /// </summary> - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public SwitchParameter Exact { get; set; } - - private string QueryAsJoinedString - { - get - { - return (this.Query is null) - ? null - : string.Join(" ", this.Query); - } - } - - /// <summary> - /// Returns a <see cref="PackageFieldMatchOption" /> based on a parameter. - /// </summary> - /// <returns>A <see cref="PackageFieldMatchOption" /> value.</returns> - protected virtual PackageFieldMatchOption GetExactAsMatchOption() - { - return this.Exact.ToBool() - ? PackageFieldMatchOption.Equals - : PackageFieldMatchOption.ContainsCaseInsensitive; - } - - /// <summary> - /// Searches for packages based on the configured parameters. - /// </summary> - /// <param name="behavior">The <see cref="CompositeSearchBehavior" /> value.</param> - /// <param name="limit">The limit on the number of matches returned.</param> - /// <returns>A list of <see cref="MatchResult" /> objects.</returns> - protected IReadOnlyList<MatchResult> FindPackages( - CompositeSearchBehavior behavior, - uint limit) - { - PackageCatalog catalog = this.GetPackageCatalog(behavior); - FindPackagesOptions options = this.GetFindPackagesOptions(limit); - return GetMatchResults(catalog, options); - } - - private static void SetQueryInFindPackagesOptions( - ref FindPackagesOptions options, - PackageFieldMatchOption match, - string value) - { - var selector = ComObjectFactory.Value.CreatePackageMatchFilter(); - selector.Field = PackageMatchField.CatalogDefault; - selector.Value = value ?? string.Empty; - selector.Option = match; - options.Selectors.Add(selector); - } - - private static void AddFilterToFindPackagesOptionsIfNotNull( - ref FindPackagesOptions options, - PackageMatchField field, - PackageFieldMatchOption match, - string value) - { - if (value != null) - { - var filter = ComObjectFactory.Value.CreatePackageMatchFilter(); - filter.Field = field; - filter.Value = value; - filter.Option = match; - options.Filters.Add(filter); - } - } - - private static IReadOnlyList<MatchResult> GetMatchResults( - PackageCatalog catalog, - FindPackagesOptions options) - { - FindPackagesResult result = catalog.FindPackages(options); - if (result.Status == FindPackagesResultStatus.Ok) - { - return result.Matches; - } - else - { - throw new FindPackagesException(result.Status); - } - } - - private PackageCatalog GetPackageCatalog(CompositeSearchBehavior behavior) - { - PackageCatalogReference reference = this.GetPackageCatalogReference(behavior); - ConnectResult result = reference.Connect(); - if (result.Status == ConnectResultStatus.Ok) - { - return result.PackageCatalog; - } - else - { - throw new RuntimeException(Utilities.ResourceManager.GetString("RuntimeExceptionCatalogError")); - } - } - - private PackageCatalogReference GetPackageCatalogReference(CompositeSearchBehavior behavior) - { - CreateCompositePackageCatalogOptions options = ComObjectFactory.Value.CreateCreateCompositePackageCatalogOptions(); - IReadOnlyList<PackageCatalogReference> references = GetPackageCatalogReferences(this.Source); - for (var i = 0; i < references.Count; i++) - { - options.Catalogs.Add(references[i]); - } - - options.CompositeSearchBehavior = behavior; - return PackageManager.Value.CreateCompositePackageCatalog(options); - } - - private FindPackagesOptions GetFindPackagesOptions(uint limit) - { - var options = ComObjectFactory.Value.CreateFindPackagesOptions(); - SetQueryInFindPackagesOptions(ref options, this.GetExactAsMatchOption(), this.QueryAsJoinedString); - this.AddAttributedFiltersToFindPackagesOptions(ref options, this.GetExactAsMatchOption()); - options.ResultLimit = limit; - return options; - } - - private void AddAttributedFiltersToFindPackagesOptions( - ref FindPackagesOptions options, - PackageFieldMatchOption match) - { - IEnumerable<PropertyInfo> properties = this - .GetType() - .GetProperties() - .Where(property => Attribute.IsDefined(property, typeof(FilterAttribute))); - - foreach (PropertyInfo info in properties) - { - if (info.GetCustomAttribute(typeof(FilterAttribute), true) is FilterAttribute attribute) - { - PackageMatchField field = attribute.Field; - string value = info.GetValue(this, null) as string; - AddFilterToFindPackagesOptionsIfNotNull(ref options, field, match, value); - } - } - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/BaseFinderExtendedCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/BaseFinderExtendedCommand.cs @@ -1,58 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="BaseFinderExtendedCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Common -{ - using System.Collections.Generic; - using System.Management.Automation; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Attributes; - - /// <summary> - /// This is the base class for the commands whose sole purpose is to filter a list of packages i.e., - /// the "search" and "list" commands. This class contains an extended set of parameters suited for - /// that purpose. - /// </summary> - public class BaseFinderExtendedCommand : BaseFinderCommand - { - /// <summary> - /// Gets or sets the filter that is matched against the tags of the package. - /// </summary> - [Filter(Field = PackageMatchField.Tag)] - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public string Tag { get; set; } - - /// <summary> - /// Gets or sets the filter that is matched against the commands of the package. - /// </summary> - [Filter(Field = PackageMatchField.Command)] - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public string Command { get; set; } - - /// <summary> - /// Gets or sets the maximum number of results returned. - /// </summary> - [ValidateRange(Constants.CountLowerBound, Constants.CountUpperBound)] - [Parameter( - ParameterSetName = Constants.FoundSet, - ValueFromPipelineByPropertyName = true)] - public uint Count { get; set; } - - /// <summary> - /// Searches for packages from configured sources. - /// </summary> - /// <param name="behavior">A <see cref="CompositeSearchBehavior" /> value.</param> - /// <returns>A list of <see cref="MatchResult" /> objects.</returns> - protected IReadOnlyList<MatchResult> FindPackages(CompositeSearchBehavior behavior) - { - return this.FindPackages(behavior, this.Count); - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/BaseInstallCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/BaseInstallCommand.cs @@ -1,156 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="BaseInstallCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -#pragma warning disable SA1200 // Using directives should be placed correctly -using Windows.Foundation; -#pragma warning restore SA1200 // Using directives should be placed correctly - -namespace Microsoft.WinGet.Client.Common -{ - using System.IO; - using System.Management.Automation; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Helpers; - - /// <summary> - /// This is the base class for all commands that parse a <see cref="FindPackagesOptions" /> result - /// from the provided parameters i.e., the "install" and "upgrade" commands. - /// </summary> - public class BaseInstallCommand : BasePackageCommand - { - private string location; - - /// <summary> - /// Gets or sets the mode to manipulate the package with. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public PackageInstallMode Mode { get; set; } = PackageInstallMode.Default; - - /// <summary> - /// Gets or sets the override arguments to be passed on to the installer. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public string Override { get; set; } - - /// <summary> - /// Gets or sets the installation location. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public string Location - { - get => this.location; - set - { - this.location = Path.IsPathRooted(value) - ? value - : this.SessionState.Path.CurrentFileSystemLocation + @"\" + value; - } - } - - /// <summary> - /// Gets or sets a value indicating whether to skip the installer hash validation check. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public SwitchParameter AllowHashMismatch { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether to continue upon non security related failures. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public SwitchParameter Force { get; set; } - - /// <summary> - /// Gets or sets the optional HTTP Header to pass on to the REST Source. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public string Header { get; set; } - - /// <summary> - /// Gets the install options from the configured parameters. - /// </summary> - /// <param name="version">The <see cref="PackageVersionId" /> to install.</param> - /// <returns>An <see cref="InstallOptions" /> instance.</returns> - protected virtual InstallOptions GetInstallOptions(PackageVersionId version) - { - InstallOptions options = ComObjectFactory.Value.CreateInstallOptions(); - options.AllowHashMismatch = this.AllowHashMismatch.ToBool(); - options.Force = this.Force.ToBool(); - options.PackageInstallMode = this.Mode; - if (version != null) - { - options.PackageVersionId = version; - } - - if (this.Log != null) - { - options.LogOutputPath = this.Log; - } - - if (this.Override != null) - { - options.ReplacementInstallerArguments = this.Override; - } - - if (this.Location != null) - { - options.PreferredInstallLocation = this.Location; - } - - if (this.Header != null) - { - options.AdditionalPackageCatalogArguments = this.Header; - } - - return options; - } - - /// <summary> - /// Registers callbacks on an asynchronous operation and waits for the results. - /// </summary> - /// <param name="operation">The asynchronous operation.</param> - /// <param name="activity">A <see cref="string" /> instance.</param> - /// <returns>A <see cref="InstallResult" /> instance.</returns> - protected InstallResult RegisterCallbacksAndWait( - IAsyncOperationWithProgress<InstallResult, InstallProgress> operation, - string activity) - { - WriteProgressAdapter adapter = new (this); - operation.Progress = (context, progress) => - { - ProgressRecord record = new (1, activity, progress.State.ToString()) - { - RecordType = ProgressRecordType.Processing, - }; - - if ((progress.State == PackageInstallProgressState.Downloading) && (progress.BytesRequired != 0)) - { - record.StatusDescription = $"{progress.BytesDownloaded / 1000000.0f:0.0} MB / {progress.BytesRequired / 1000000.0f:0.0} MB"; - record.PercentComplete = (int)(progress.DownloadProgress * 100); - } - else if (progress.State == PackageInstallProgressState.Installing) - { - record.PercentComplete = (int)(progress.InstallationProgress * 100); - } - - adapter.WriteProgress(record); - }; - operation.Completed = (context, status) => - { - adapter.WriteProgress(new ProgressRecord(1, activity, status.ToString()) - { - RecordType = ProgressRecordType.Completed, - }); - adapter.Completed = true; - }; - System.Console.CancelKeyPress += (sender, e) => - { - operation.Cancel(); - }; - adapter.Wait(); - return operation.GetResults(); - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/BasePackageCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/BasePackageCommand.cs @@ -1,133 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="BasePackageCommand.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Common -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Management.Automation; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Errors; - - /// <summary> - /// This is the base class for commands which operate on a specific package and version i.e., - /// the "install", "uninstall", and "upgrade" commands. - /// </summary> - public class BasePackageCommand : BaseFinderCommand - { - private string log; - - /// <summary> - /// Gets or sets the package to directly install. - /// </summary> - /// <remarks> - /// Must match the name of the <see cref="CatalogPackage" /> field on the <see cref="MatchResult" /> class. - /// </remarks> - [Alias("InputObject")] - [ValidateNotNull] - [Parameter( - ParameterSetName = Constants.GivenSet, - Position = 0, - ValueFromPipeline = true, - ValueFromPipelineByPropertyName = true)] - public CatalogPackage CatalogPackage { get; set; } - - /// <summary> - /// Gets or sets the version to install. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public string Version { get; set; } - - /// <summary> - /// Gets or sets the path to the logging file. - /// </summary> - [Parameter(ValueFromPipelineByPropertyName = true)] - public string Log - { - get => this.log; - set - { - this.log = Path.IsPathRooted(value) - ? value - : this.SessionState.Path.CurrentFileSystemLocation + @"\" + value; - } - } - - /// <inheritdoc /> - protected override PackageFieldMatchOption GetExactAsMatchOption() - { - return this.Exact.ToBool() - ? PackageFieldMatchOption.Equals - : PackageFieldMatchOption.EqualsCaseInsensitive; - } - - /// <summary> - /// Executes a command targeting a specific package version. - /// </summary> - /// <param name="behavior">The <see cref="CompositeSearchBehavior" /> value.</param> - /// <param name="callback">The method to call after retrieving the package and version to operate upon.</param> - protected void GetPackageAndExecute( - CompositeSearchBehavior behavior, - Action<CatalogPackage, PackageVersionId> callback) - { - CatalogPackage package = this.GetCatalogPackage(behavior); - PackageVersionId version = this.GetPackageVersionId(package); - if (this.ShouldProcess(package.ToString(version))) - { - callback(package, version); - } - } - - private CatalogPackage GetCatalogPackage(CompositeSearchBehavior behavior) - { - if (this.ParameterSetName == Constants.GivenSet) - { - // The package was already provided via a parameter or the pipeline. - return this.CatalogPackage; - } - else - { - IReadOnlyList<MatchResult> results = this.FindPackages(behavior, 0); - if (results.Count == 1) - { - // Exactly one package matched, so we can just return it. - return results[0].CatalogPackage; - } - else if (results.Count == 0) - { - // No packages matched, we need to throw an error. - throw new RuntimeException(Utilities.ResourceManager.GetString("RuntimeExceptionNoPackagesFound")); - } - else - { - // Too many packages matched! The user needs to refine their input. - throw new VagueCriteriaException(results); - } - } - } - - private PackageVersionId GetPackageVersionId(CatalogPackage package) - { - if (this.Version != null) - { - for (var i = 0; i < package.AvailableVersions.Count; i++) - { - if (package.AvailableVersions[i].Version.CompareTo(this.Version) == 0) - { - return package.AvailableVersions[i]; - } - } - - throw new ArgumentException(Utilities.ResourceManager.GetString("RuntimeExceptionInvalidVersion")); - } - else - { - return null; - } - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs @@ -11,19 +11,9 @@ namespace Microsoft.WinGet.Client.Common /// <summary> /// This class contains all of the configurable constants for this project. /// </summary> - public static class Constants + internal static class Constants { /// <summary> - /// The noun analogue of the <see cref="CatalogPackage" /> class. Changing this will alter the names of the related commands. - /// </summary> - public const string PackageNoun = "WinGetPackage"; - - /// <summary> - /// The noun analogue of the <see cref="PackageCatalogReference" /> class. Changing this will alter the names of the related commands. - /// </summary> - public const string SourceNoun = "WinGetSource"; - - /// <summary> /// If a command allows the specification of the maximum number of results to return, this is the lower bound for that value. /// </summary> public const uint CountLowerBound = 1; @@ -42,6 +32,27 @@ namespace Microsoft.WinGet.Client.Common /// This parameter set indicates that a package was not provided via a parameter or the pipeline and it /// needs to be found by searching a package source. /// </summary> - public const string FoundSet = "FoundSet"; + public const string FoundSet = "FoundSet"; + + /// <summary> + /// Nouns used for different cmdlets. Changing this will alter the names of the related commands. + /// </summary> + public static class WinGetNouns + { + /// <summary> + /// The noun analogue of the <see cref="CatalogPackage" /> class. + /// </summary> + public const string Package = "WinGetPackage"; + + /// <summary> + /// The noun analogue of the <see cref="PackageCatalogReference" /> class. + /// </summary> + public const string Source = "WinGetSource"; + + /// <summary> + /// The noun for any user settings cmdlet. + /// </summary> + public const string UserSettings = "WinGetUserSettings"; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/ErrorCode.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/ErrorCode.cs @@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Client.Common /// <summary> /// Error code constants. /// </summary> - public class ErrorCode + public static class ErrorCode { /// <summary> /// Error code for ERROR_FILE_NOT_FOUND. diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/Utilities.cs @@ -12,20 +12,9 @@ namespace Microsoft.WinGet.Client.Common /// <summary> /// This class contains various helper methods for this project. /// </summary> - public static class Utilities + internal static class Utilities { /// <summary> - /// Gets the <see cref="ResourceManager" /> instance for the executing assembly. - /// </summary> - public static ResourceManager ResourceManager - { - get - { - return new ResourceManager(typeof(Properties.Resources)); - } - } - - /// <summary> /// Gets a value indicating whether the current assembly is executing in an administrative context. /// </summary> [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Windows only API")] diff --git a/src/PowerShell/Microsoft.WinGet.Client/Errors/FindPackagesException.cs b/src/PowerShell/Microsoft.WinGet.Client/Errors/FindPackagesException.cs @@ -1,36 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="FindPackagesException.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Errors -{ - using System; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Common; - - /// <summary> - /// Raised when there is an error searching for packages. - /// </summary> - [Serializable] - public class FindPackagesException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="FindPackagesException"/> class. - /// </summary> - /// <param name="status">A <see cref="FindPackagesResultStatus" /> value.</param> - public FindPackagesException(FindPackagesResultStatus status) - : base(string.Format( - Utilities.ResourceManager.GetString("FindPackagesExceptionMessage"), - status.ToString())) - { - this.Status = status; - } - - /// <summary> - /// Gets or sets the error status. - /// </summary> - public FindPackagesResultStatus Status { get; set; } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Errors/VagueCriteriaException.cs b/src/PowerShell/Microsoft.WinGet.Client/Errors/VagueCriteriaException.cs @@ -1,39 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="VagueCriteriaException.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Errors -{ - using System; - using System.Collections.Generic; - using Microsoft.Management.Deployment; - using Microsoft.WinGet.Client.Common; - - /// <summary> - /// Raised when search criteria for installing or updating a package is too vague. - /// </summary> - [Serializable] - public class VagueCriteriaException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="VagueCriteriaException"/> class. - /// </summary> - /// <param name="results">The list of conflicting packages of length at least two.</param> - public VagueCriteriaException(IReadOnlyList<MatchResult> results) - : base(string.Format( - Utilities.ResourceManager.GetString("VagueCriteriaExceptionMessage"), - results[0].CatalogPackage.ToString(null), - results[1].CatalogPackage.ToString(null), - results.Count - 2)) - { - this.MatchResults = results; - } - - /// <summary> - /// Gets or sets the list of conflicting packages. - /// </summary> - public IReadOnlyList<MatchResult> MatchResults { get; set; } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/CatalogConnectException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/CatalogConnectException.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------------- +// <copyright file="CatalogConnectException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Failed connecting to catalog. + /// </summary> + [Serializable] + public class CatalogConnectException : RuntimeException + { + /// <summary> + /// Initializes a new instance of the <see cref="CatalogConnectException"/> class. + /// </summary> + public CatalogConnectException() + : base(Resources.CatalogConnectExceptionMessage) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/ExecuteAsSystemException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/ExecuteAsSystemException.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ExecuteAsSystemException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Executing as system is disabled. + /// </summary> + [Serializable] + public class ExecuteAsSystemException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="ExecuteAsSystemException"/> class. + /// </summary> + public ExecuteAsSystemException() + : base(Resources.ExecuteAsSystemExceptionMessage) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/FindPackagesException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/FindPackagesException.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------------- +// <copyright file="FindPackagesException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Raised when there is an error searching for packages. + /// </summary> + [Serializable] + public class FindPackagesException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="FindPackagesException"/> class. + /// </summary> + /// <param name="status">A <see cref="FindPackagesResultStatus" /> value.</param> + public FindPackagesException(FindPackagesResultStatus status) + : base(string.Format( + Resources.FindPackagesExceptionMessage, + status.ToString())) + { + this.Status = status; + } + + /// <summary> + /// Gets the error status. + /// </summary> + public FindPackagesResultStatus Status { get; private set; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/InvalidSourceException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/InvalidSourceException.cs @@ -0,0 +1,33 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvalidSourceException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Invalid source. + /// </summary> + [Serializable] + public class InvalidSourceException : ArgumentException + { + /// <summary> + /// Initializes a new instance of the <see cref="InvalidSourceException"/> class. + /// </summary> + /// <param name="sourceName">Source name.</param> + public InvalidSourceException(string sourceName) + : base(string.Format(Resources.InvalidSourceExceptionMessage, sourceName)) + { + this.SourceName = sourceName; + } + + /// <summary> + /// Gets the source name. + /// </summary> + public string SourceName { get; private set; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/InvalidVersionException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/InvalidVersionException.cs @@ -0,0 +1,33 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvalidVersionException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Invalid version. + /// </summary> + [Serializable] + public class InvalidVersionException : ArgumentException + { + /// <summary> + /// Initializes a new instance of the <see cref="InvalidVersionException"/> class. + /// </summary> + /// <param name="version">Version.</param> + public InvalidVersionException(string version) + : base(string.Format(Resources.InvalidVersionExceptionMessage, version)) + { + this.Version = version; + } + + /// <summary> + /// Gets the version. + /// </summary> + public string Version { get; private set; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/NoPackageFoundException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/NoPackageFoundException.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------------- +// <copyright file="NoPackageFoundException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// No package found. + /// </summary> + [Serializable] + public class NoPackageFoundException : RuntimeException + { + /// <summary> + /// Initializes a new instance of the <see cref="NoPackageFoundException"/> class. + /// </summary> + public NoPackageFoundException() + : base(Resources.NoPackageFoundExceptionMessage) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/UserSettingsReadException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/UserSettingsReadException.cs @@ -0,0 +1,35 @@ +// ----------------------------------------------------------------------------- +// <copyright file="UserSettingsReadException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Settings.json file is invalid. + /// </summary> + [Serializable] + public class UserSettingsReadException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="UserSettingsReadException"/> class. + /// </summary> + public UserSettingsReadException() + : base(Resources.UserSettingsReadException) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="UserSettingsReadException"/> class. + /// </summary> + /// <param name="inner">Inner exception.</param> + public UserSettingsReadException(Exception inner) + : base(Resources.UserSettingsReadException, inner) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/VagueCriteriaException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/VagueCriteriaException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="VagueCriteriaException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Deployment; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// Raised when search criteria for installing or updating a package is too vague. + /// </summary> + [Serializable] + public class VagueCriteriaException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="VagueCriteriaException"/> class. + /// </summary> + /// <param name="results">The list of conflicting packages of length at least two.</param> + public VagueCriteriaException(IReadOnlyList<MatchResult> results) + : base(string.Format( + Resources.VagueCriteriaExceptionMessage, + results[0].CatalogPackage.ToString(null), + results[1].CatalogPackage.ToString(null), + results.Count - 2)) + { + this.MatchResults = results; + } + + /// <summary> + /// Gets the list of conflicting packages. + /// </summary> + public IReadOnlyList<MatchResult> MatchResults { get; private set; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetCLIException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetCLIException.cs @@ -0,0 +1,60 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetCLIException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// WinGet cli exception. + /// </summary> + public class WinGetCLIException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="WinGetCLIException"/> class. + /// </summary> + /// <param name="command">Command.</param> + /// <param name="parameters">Parameters.</param> + /// <param name="exitCode">Exit code.</param> + /// <param name="stdOut">Standard output.</param> + /// <param name="stdErr">Standard error.</param> + public WinGetCLIException(string command, string parameters, int exitCode, string stdOut, string stdErr) + : base(string.Format(Resources.WinGetCLIExceptionMessage, command, exitCode)) + { + this.Command = command; + this.Parameters = parameters; + this.ExitCode = exitCode; + this.StdOut = stdOut; + this.StdErr = stdErr; + } + + /// <summary> + /// Gets the command. + /// </summary> + public string Command { get; private set; } + + /// <summary> + /// Gets the parameters. + /// </summary> + public string Parameters { get; private set; } + + /// <summary> + /// Gets the exit code. + /// </summary> + public int ExitCode { get; private set; } + + /// <summary> + /// Gets the standard output. + /// </summary> + public string StdOut { get; private set; } + + /// <summary> + /// Gets the standard error. + /// </summary> + public string StdErr { get; private set; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetPackageNotInstalledException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetPackageNotInstalledException.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetPackageNotInstalledException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Exceptions +{ + using System; + using Microsoft.WinGet.Client.Properties; + + /// <summary> + /// No package found. + /// </summary> + [Serializable] + public class WinGetPackageNotInstalledException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="WinGetPackageNotInstalledException"/> class. + /// </summary> + public WinGetPackageNotInstalledException() + : base(Resources.WinGetPackageNotInstalledMessage) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs b/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs @@ -6,11 +6,12 @@ namespace Microsoft.WinGet.Client.Factories { - using System; - using System.Runtime.InteropServices; - using Microsoft.Management.Deployment; + using System; + using System.Runtime.InteropServices; + using Microsoft.Management.Deployment; using Microsoft.WinGet.Client.Common; - + using Microsoft.WinGet.Client.Exceptions; + #if NET using WinRT; #endif @@ -26,14 +27,14 @@ namespace Microsoft.WinGet.Client.Factories private static readonly Guid CreateCompositePackageCatalogOptionsClsid = Guid.Parse("526534B8-7E46-47C8-8416-B1685C327D37"); private static readonly Guid InstallOptionsClsid = Guid.Parse("1095F097-EB96-453B-B4E6-1613637F3B14"); private static readonly Guid UninstallOptionsClsid = Guid.Parse("E1D9A11E-9F85-4D87-9C17-2B93143ADB8D"); - private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000"); + private static readonly Guid PackageMatchFilterClsid = Guid.Parse("D02C9DAF-99DC-429C-B503-4E504E4AB000"); #else private static readonly Guid PackageManagerClsid = Guid.Parse("74CB3139-B7C5-4B9E-9388-E6616DEA288C"); private static readonly Guid FindPackagesOptionsClsid = Guid.Parse("1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96"); private static readonly Guid CreateCompositePackageCatalogOptionsClsid = Guid.Parse("EE160901-B317-4EA7-9CC6-5355C6D7D8A7"); private static readonly Guid InstallOptionsClsid = Guid.Parse("44FE0580-62F7-44D4-9E91-AA9614AB3E86"); private static readonly Guid UninstallOptionsClsid = Guid.Parse("AA2A5C04-1AD9-46C4-B74F-6B334AD7EB8C"); - private static readonly Guid PackageMatchFilterClsid = Guid.Parse("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"); + private static readonly Guid PackageMatchFilterClsid = Guid.Parse("3F85B9F4-487A-4C48-9035-2903F8A6D9E8"); #endif [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type PackageManagerType = Type.GetTypeFromCLSID(PackageManagerClsid); @@ -46,8 +47,8 @@ namespace Microsoft.WinGet.Client.Factories [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static readonly Type UninstallOptionsType = Type.GetTypeFromCLSID(UninstallOptionsClsid); [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] - private static readonly Type PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid); - + private static readonly Type PackageMatchFilterType = Type.GetTypeFromCLSID(PackageMatchFilterClsid); + private static readonly Guid PackageManagerIid = Guid.Parse("B375E3B9-F2E0-5C93-87A7-B67497F7E593"); private static readonly Guid FindPackagesOptionsIid = Guid.Parse("A5270EDD-7DA7-57A3-BACE-F2593553561F"); private static readonly Guid CreateCompositePackageCatalogOptionsIid = Guid.Parse("21ABAA76-089D-51C5-A745-C85EEFE70116"); @@ -112,27 +113,27 @@ namespace Microsoft.WinGet.Client.Factories [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "COM only usage.")] private static T Create<T>(Type type, in Guid iid) { - object instance = null; - - if (Utilities.ExecutingAsAdministrator) - { - int hr = WinGetServerManualActivation_CreateInstance(type.GUID, iid, 0, out instance); - - if (hr < 0) - { - if (hr == ErrorCode.FileNotFound) - { - throw new Exception(Utilities.ResourceManager.GetString("WinGetPackageNotInstalled")); - } - else - { - throw new COMException($"Failed to create instance: {hr}", hr); - } - } + object instance = null; + + if (Utilities.ExecutingAsAdministrator) + { + int hr = WinGetServerManualActivation_CreateInstance(type.GUID, iid, 0, out instance); + + if (hr < 0) + { + if (hr == ErrorCode.FileNotFound) + { + throw new WinGetPackageNotInstalledException(); + } + else + { + throw new COMException($"Failed to create instance: {hr}", hr); + } + } } - else - { - instance = Activator.CreateInstance(type); + else + { + instance = Activator.CreateInstance(type); } #if NET diff --git a/src/PowerShell/Microsoft.WinGet.Client/Helpers/WinGetCLICommandResult.cs b/src/PowerShell/Microsoft.WinGet.Client/Helpers/WinGetCLICommandResult.cs @@ -0,0 +1,75 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetCLICommandResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Helpers +{ + using Microsoft.WinGet.Client.Exceptions; + + /// <summary> + /// Winget cli command result. + /// </summary> + internal class WinGetCLICommandResult + { + /// <summary> + /// Initializes a new instance of the <see cref="WinGetCLICommandResult"/> class. + /// </summary> + /// <param name="command">Command.</param> + /// <param name="parameters">Parameters.</param> + /// <param name="exitCode">Exit code.</param> + /// <param name="stdOut">Standard output.</param> + /// <param name="stdErr">Standard error.</param> + public WinGetCLICommandResult(string command, string parameters, int exitCode, string stdOut, string stdErr) + { + this.Command = command; + this.Parameters = parameters; + this.ExitCode = exitCode; + this.StdOut = stdOut; + this.StdErr = stdErr; + } + + /// <summary> + /// Gets the command. + /// </summary> + public string Command { get; private set; } + + /// <summary> + /// Gets the parameters. + /// </summary> + public string Parameters { get; private set; } + + /// <summary> + /// Gets the exit code. + /// </summary> + public int ExitCode { get; private set; } + + /// <summary> + /// Gets the standard output. + /// </summary> + public string StdOut { get; private set; } + + /// <summary> + /// Gets the standard error. + /// </summary> + public string StdErr { get; private set; } + + /// <summary> + /// Verifies exit code. + /// </summary> + /// <param name="exitCode">Optional exit code.</param> + public void VerifyExitCode(int exitCode = 0) + { + if (this.ExitCode != exitCode) + { + throw new WinGetCLIException( + this.Command, + this.Parameters, + this.ExitCode, + this.StdOut, + this.StdErr); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Helpers/WingetCLIWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client/Helpers/WingetCLIWrapper.cs @@ -0,0 +1,90 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WingetCLIWrapper.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Helpers +{ + using System; + using System.Diagnostics; + using System.IO; + using Microsoft.WinGet.Client.Exceptions; + + /// <summary> + /// Calls winget directly. + /// </summary> + internal class WingetCLIWrapper + { + private static readonly string WingetCliPath; + + /// <summary> + /// Initializes static members of the <see cref="WingetCLIWrapper"/> class. + /// When app execution alias is disabled the path of the exe is + /// in the package family name directory in the local app data windows app directory. If its enabled then there's + /// link in the windows app data directory. To avoid checking if its enabled or not, just look in the package + /// family name directory. + /// For test, point to the wingetdev executable. + /// </summary> + static WingetCLIWrapper() + { + string windowsAppPath = Environment.ExpandEnvironmentVariables("%LOCALAPPDATA%\\Microsoft\\WindowsApps"); +#if USE_PROD_CLSIDS + WingetCliPath = Path.Combine( + windowsAppPath, + "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe", + "winget.exe"); +#else + WingetCliPath = Path.Combine( + windowsAppPath, + "WinGetDevCLI_8wekyb3d8bbwe", + "wingetdev.exe"); +#endif + } + + /// <summary> + /// Initializes a new instance of the <see cref="WingetCLIWrapper"/> class. + /// </summary> + public WingetCLIWrapper() + { + if (!File.Exists(WingetCliPath)) + { + throw new WinGetPackageNotInstalledException(); + } + } + + /// <summary> + /// Runs winget command with parameters. + /// </summary> + /// <param name="command">Command.</param> + /// <param name="parameters">Parameters.</param> + /// <param name="timeOut">Time out.</param> + /// <returns>WinGetCommandResult.</returns> + public WinGetCLICommandResult RunCommand(string command, string parameters, int timeOut = 60000) + { + Process p = new () + { + StartInfo = new (WingetCliPath, command + ' ' + parameters) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }, + }; + + p.Start(); + + if (p.WaitForExit(timeOut)) + { + return new WinGetCLICommandResult( + command, + parameters, + p.ExitCode, + p.StandardOutput.ReadToEnd(), + p.StandardError.ReadToEnd()); + } + + throw new TimeoutException($"Direct winget command run timed out: {command} {parameters}"); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client/Microsoft.WinGet.Client.csproj b/src/PowerShell/Microsoft.WinGet.Client/Microsoft.WinGet.Client.csproj @@ -32,6 +32,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> </ItemGroup> <ItemGroup> @@ -57,12 +58,18 @@ <ItemGroup Condition="'$(TargetFramework)' == '$(CoreFramework)'"> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="1.6.5" /> + <PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.8" /> </ItemGroup> <ItemGroup Condition="'$(TargetFramework)' == '$(DesktopFramework)'"> <PackageReference Include="Microsoft.Windows.SDK.Contracts" Version="10.0.22000.196" PrivateAssets="all" /> + <PackageReference Include="Microsoft.PowerShell.5.1.ReferenceAssemblies" Version="1.0.0" /> </ItemGroup> + <PropertyGroup Condition="'$(TargetFramework)' == '$(DesktopFramework)'"> + <DefineConstants>POWERSHELL_WINDOWS</DefineConstants> + </PropertyGroup> + <ItemGroup> <Compile Update="Properties\Resources.Designer.cs"> <DesignTime>True</DesignTime> @@ -110,18 +117,18 @@ <Target Name="CopyCoreBinaries" AfterTargets="AfterBuild" Condition="'$(TargetFramework)' == '$(CoreFramework)'"> <ItemGroup> - <CoreBinaries Include="$(OutputPath)*" /> + <CoreBinaries Include="$(OutputPath)\**\*.*" /> </ItemGroup> <Message Importance="high" Text="Copying @(CoreBinaries) to '$(PowerShellModuleOutputDirectory)\$(Platform)\Core'" /> - <Copy SourceFiles="@(CoreBinaries)" DestinationFolder="$(PowerShellModuleOutputDirectory)\$(Platform)\Core" /> + <Copy SourceFiles="@(CoreBinaries)" DestinationFolder="$(PowerShellModuleOutputDirectory)\$(Platform)\Core\%(RecursiveDir)" /> </Target> <Target Name="CopyDesktopBinaries" AfterTargets="AfterBuild" Condition="'$(TargetFramework)' == '$(DesktopFramework)'"> <ItemGroup> - <DesktopBinaries Include="$(OutputPath)*" /> + <DesktopBinaries Include="$(OutputPath)\**\*.*" /> </ItemGroup> <Message Importance="high" Text="Copying @(DesktopBinaries) to '$(PowerShellModuleOutputDirectory)\$(Platform)\Desktop'" /> - <Copy SourceFiles="@(DesktopBinaries)" DestinationFolder="$(PowerShellModuleOutputDirectory)\$(Platform)\Desktop" /> + <Copy SourceFiles="@(DesktopBinaries)" DestinationFolder="$(PowerShellModuleOutputDirectory)\$(Platform)\Desktop\%(RecursiveDir)" /> </Target> </Project> diff --git a/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/Module/Microsoft.WinGet.Client.psd1 @@ -90,7 +90,10 @@ CmdletsToExport = @( 'Get-WinGetSource', 'Install-WinGetPackage', 'Uninstall-WinGetPackage', - 'Update-WinGetPackage' + 'Update-WinGetPackage', + 'Get-WinGetUserSettings', + 'Set-WinGetUserSettings', + 'Test-WinGetUserSettings' ) # Variables to export from this module diff --git a/src/PowerShell/Microsoft.WinGet.Client/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client/Properties/Resources.Designer.cs @@ -61,20 +61,20 @@ namespace Microsoft.WinGet.Client.Properties { } /// <summary> - /// Looks up a localized string similar to No source matches the given value: {0}. + /// Looks up a localized string similar to An error occurred while connecting to the catalog.. /// </summary> - internal static string ArgumentExceptionInvalidSource { + internal static string CatalogConnectExceptionMessage { get { - return ResourceManager.GetString("ArgumentExceptionInvalidSource", resourceCulture); + return ResourceManager.GetString("CatalogConnectExceptionMessage", resourceCulture); } } /// <summary> /// Looks up a localized string similar to This cmdlet is currently disabled for SYSTEM.. /// </summary> - internal static string ExceptionSystemDisabled { + internal static string ExecuteAsSystemExceptionMessage { get { - return ResourceManager.GetString("ExceptionSystemDisabled", resourceCulture); + return ResourceManager.GetString("ExecuteAsSystemExceptionMessage", resourceCulture); } } @@ -88,56 +88,65 @@ namespace Microsoft.WinGet.Client.Properties { } /// <summary> - /// Looks up a localized string similar to Installing '{0}'. + /// Looks up a localized string similar to No source matches the given value: {0}. /// </summary> - internal static string ProgressRecordActivityInstalling { + internal static string InvalidSourceExceptionMessage { get { - return ResourceManager.GetString("ProgressRecordActivityInstalling", resourceCulture); + return ResourceManager.GetString("InvalidSourceExceptionMessage", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to Uninstalling '{0}'. + /// Looks up a localized string similar to No versions matched the given value: {0}. /// </summary> - internal static string ProgressRecordActivityUninstalling { + internal static string InvalidVersionExceptionMessage { get { - return ResourceManager.GetString("ProgressRecordActivityUninstalling", resourceCulture); + return ResourceManager.GetString("InvalidVersionExceptionMessage", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to Updating '{0}'. + /// Looks up a localized string similar to No packages matched the given input criteria.. /// </summary> - internal static string ProgressRecordActivityUpdating { + internal static string NoPackageFoundExceptionMessage { get { - return ResourceManager.GetString("ProgressRecordActivityUpdating", resourceCulture); + return ResourceManager.GetString("NoPackageFoundExceptionMessage", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to An error occurred while connecting to the catalog.. + /// Looks up a localized string similar to Installing '{0}'. /// </summary> - internal static string RuntimeExceptionCatalogError { + internal static string ProgressRecordActivityInstalling { get { - return ResourceManager.GetString("RuntimeExceptionCatalogError", resourceCulture); + return ResourceManager.GetString("ProgressRecordActivityInstalling", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to No versions matched the given value: {0}. + /// Looks up a localized string similar to Uninstalling '{0}'. /// </summary> - internal static string RuntimeExceptionInvalidVersion { + internal static string ProgressRecordActivityUninstalling { get { - return ResourceManager.GetString("RuntimeExceptionInvalidVersion", resourceCulture); + return ResourceManager.GetString("ProgressRecordActivityUninstalling", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to No packages matched the given input criteria.. + /// Looks up a localized string similar to Updating '{0}'. /// </summary> - internal static string RuntimeExceptionNoPackagesFound { + internal static string ProgressRecordActivityUpdating { get { - return ResourceManager.GetString("RuntimeExceptionNoPackagesFound", resourceCulture); + return ResourceManager.GetString("ProgressRecordActivityUpdating", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to User settings file is invalid.. + /// </summary> + internal static string UserSettingsReadException { + get { + return ResourceManager.GetString("UserSettingsReadException", resourceCulture); } } @@ -151,11 +160,20 @@ namespace Microsoft.WinGet.Client.Properties { } /// <summary> + /// Looks up a localized string similar to Command {0} failed with exit code {1}. + /// </summary> + internal static string WinGetCLIExceptionMessage { + get { + return ResourceManager.GetString("WinGetCLIExceptionMessage", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Unable to execute command; WinGet package not installed.. /// </summary> - internal static string WinGetPackageNotInstalled { + internal static string WinGetPackageNotInstalledMessage { get { - return ResourceManager.GetString("WinGetPackageNotInstalled", resourceCulture); + return ResourceManager.GetString("WinGetPackageNotInstalledMessage", resourceCulture); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client/Properties/Resources.resx @@ -117,11 +117,11 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> - <data name="ArgumentExceptionInvalidSource" xml:space="preserve"> + <data name="InvalidSourceExceptionMessage" xml:space="preserve"> <value>No source matches the given value: {0}</value> <comment>{0} - The name of the source that was not found.</comment> </data> - <data name="ExceptionSystemDisabled" xml:space="preserve"> + <data name="ExecuteAsSystemExceptionMessage" xml:space="preserve"> <value>This cmdlet is currently disabled for SYSTEM.</value> </data> <data name="FindPackagesExceptionMessage" xml:space="preserve"> @@ -140,21 +140,27 @@ <value>Updating '{0}'</value> <comment>{0} - The name of the package being updated.</comment> </data> - <data name="RuntimeExceptionCatalogError" xml:space="preserve"> + <data name="CatalogConnectExceptionMessage" xml:space="preserve"> <value>An error occurred while connecting to the catalog.</value> </data> - <data name="RuntimeExceptionInvalidVersion" xml:space="preserve"> + <data name="InvalidVersionExceptionMessage" xml:space="preserve"> <value>No versions matched the given value: {0}</value> <comment>{0} - The version string provided by the user.</comment> </data> - <data name="RuntimeExceptionNoPackagesFound" xml:space="preserve"> + <data name="NoPackageFoundExceptionMessage" xml:space="preserve"> <value>No packages matched the given input criteria.</value> </data> <data name="VagueCriteriaExceptionMessage" xml:space="preserve"> <value>{0}, {1}, and {2} other packages matched the input criteria. Please refine the input.</value> <comment>{0} - The first conflicting package as a string. {1} - The second conflicting package. {2} - The number of other packages that also matched the input criteria.</comment> </data> - <data name="WinGetPackageNotInstalled" xml:space="preserve"> + <data name="WinGetPackageNotInstalledMessage" xml:space="preserve"> <value>Unable to execute command; WinGet package not installed.</value> </data> + <data name="WinGetCLIExceptionMessage" xml:space="preserve"> + <value>Command {0} failed with exit code {1}</value> + </data> + <data name="UserSettingsReadException" xml:space="preserve"> + <value>User settings file is invalid.</value> + </data> </root> \ No newline at end of file