winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 890e667e5c5907a93593b6c2ac1d09934a4c91d9
parent 04fb4cc1e74c530f4549d1c966b5673978e2c40e
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Tue,  1 Aug 2023 10:51:55 -0700

Move functions to cmdlets for Microsoft.WinGet.Client (#3469)

This PR moves the Crescendo generated functions to cmdlets for the Microsoft.WinGet.Client module. This is to have a centralized project that manages all the cmdlets and facilitate E2E.

For example, when the client module is built wingetdev was not used in the functions because winget.exe was hardcoded by the Crescendo tool. To properly test it, Initialize-LocalWinGetModules.ps1 find and replaced winget.exe with wingetdev

Funtions to Cmdlets moved:
- Add-WinGetSource
- Disable-WinGetSetting
- Enable-WinGetSetting
- Get-WinGetSettings
- Remove-WinGetSource
- Reset-WinGetSource

I added a `-AsPlainText` Get-WinGetSettings. If enabled it will just print the json string from `winget settings export`, if disabled it will return a HashTable. I also added running as admin check in all the cmdlets that needed and throw an not supported exception if these cmdlets are run in a system context.
Diffstat:
Msrc/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs | 2+-
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/DisableSettingCmdlet.cs | 38++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/EnableSettingCmdlet.cs | 38++++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetSettingsCmdlet.cs | 34++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetUserSettingsCmdlet.cs | 2+-
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemoveSourceCmdlet.cs | 37+++++++++++++++++++++++++++++++++++++
Asrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetSourceCmdlet.cs | 37+++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs | 10++++++++++
Asrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UserSettingsCommand.cs | 113+++----------------------------------------------------------------------------
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Common/Utilities.cs | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLIException.cs | 3+--
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WingetCLIWrapper.cs | 6++++++
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs | 13+++++++++++--
Msrc/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx | 9++++++---
Dsrc/PowerShell/Microsoft.WinGet.Client/Crescendo/Create-CrescendoFunctions.ps1 | 53-----------------------------------------------------
Dsrc/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json | 149-------------------------------------------------------------------------------
Msrc/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 | 82++++++++++++++++++++++++++++++++++++++-----------------------------------------
Dsrc/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psm1 | 661-------------------------------------------------------------------------------
Msrc/PowerShell/Microsoft.WinGet.Client/README.md | 34++++++++++++++--------------------
Msrc/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 | 2+-
Msrc/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 | 20++------------------
23 files changed, 568 insertions(+), 1063 deletions(-)

diff --git a/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs b/src/AppInstallerCLIE2ETests/PowerShell/WinGetClientModule.cs @@ -213,7 +213,7 @@ namespace AppInstallerCLIE2ETests.PowerShell // 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.Engine.Exceptions.UserSettingsReadException"); + Assert.AreEqual(exception.GetType().ToString(), "Newtonsoft.Json.JsonReaderException"); } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/AddSourceCmdlet.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------------- +// <copyright file="AddSourceCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Cmdlets.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + + /// <summary> + /// Adds a source. Requires admin. + /// </summary> + [Cmdlet(VerbsCommon.Add, Constants.WinGetNouns.Source)] + public sealed class AddSourceCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the name of the source to add. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the argument of the source to add. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Argument { get; set; } + + /// <summary> + /// Gets or sets the type of the source to add. + /// </summary> + [Parameter( + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Type { get; set; } + + /// <summary> + /// Adds source. + /// </summary> + protected override void ProcessRecord() + { + var command = new CliCommand(this); + command.AddSource(this.Name, this.Argument, this.Type); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/DisableSettingCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/DisableSettingCmdlet.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DisableSettingCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Cmdlets.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + + /// <summary> + /// Disables an admin setting. Requires admin. + /// </summary> + [Cmdlet(VerbsLifecycle.Disable, Constants.WinGetNouns.Setting)] + public sealed class DisableSettingCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the name of the setting to disable. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Name { get; set; } + + /// <summary> + /// Disables the admin setting. + /// </summary> + protected override void ProcessRecord() + { + var command = new CliCommand(this); + command.DisableSetting(this.Name); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/EnableSettingCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/EnableSettingCmdlet.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------------- +// <copyright file="EnableSettingCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Cmdlets.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + + /// <summary> + /// Enables an admin setting. Requires admin. + /// </summary> + [Cmdlet(VerbsLifecycle.Enable, Constants.WinGetNouns.Setting)] + public sealed class EnableSettingCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the name of the setting to enable. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Name { get; set; } + + /// <summary> + /// Enables the admin setting. + /// </summary> + protected override void ProcessRecord() + { + var command = new CliCommand(this); + command.EnableSetting(this.Name); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetSettingsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetSettingsCmdlet.cs @@ -0,0 +1,34 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetSettingsCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Cmdlets.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + + /// <summary> + /// Gets winget settings. + /// </summary> + [Cmdlet(VerbsCommon.Get, Constants.WinGetNouns.Settings)] + public sealed class GetSettingsCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets a value indicating whether to output a string or a hashtable. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AsPlainText { get; set; } + + /// <summary> + /// Get settings. + /// </summary> + protected override void ProcessRecord() + { + var command = new CliCommand(this); + command.GetSettings(this.AsPlainText.ToBool()); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetUserSettingsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/GetUserSettingsCmdlet.cs @@ -19,7 +19,7 @@ namespace Microsoft.WinGet.Client.Commands public sealed class GetUserSettingsCmdlet : PSCmdlet { /// <summary> - /// Writes the settings file contents. + /// Gets the settings file contents. /// </summary> protected override void ProcessRecord() { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemoveSourceCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RemoveSourceCmdlet.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------------- +// <copyright file="RemoveSourceCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + + /// <summary> + /// Removes a source. Requires admin. + /// </summary> + [Cmdlet(VerbsCommon.Remove, Constants.WinGetNouns.Source)] + public sealed class RemoveSourceCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the name of the source to remove. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Name { get; set; } + + /// <summary> + /// Removes source. + /// </summary> + protected override void ProcessRecord() + { + var command = new CliCommand(this); + command.RemoveSource(this.Name); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetSourceCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/ResetSourceCmdlet.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResetSourceCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Cmdlets.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Common; + using Microsoft.WinGet.Client.Engine.Commands; + + /// <summary> + /// Resets a source. Requires admin. + /// </summary> + [Cmdlet(VerbsCommon.Reset, Constants.WinGetNouns.Source)] + public sealed class ResetSourceCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the name of the source to reset. + /// </summary> + [Parameter( + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public string Name { get; set; } + + /// <summary> + /// Resets source. + /// </summary> + protected override void ProcessRecord() + { + var command = new CliCommand(this); + command.ResetSource(this.Name); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Common/Constants.cs @@ -71,6 +71,16 @@ namespace Microsoft.WinGet.Client.Common /// The noun for winget version. /// </summary> public const string Version = "WinGetVersion"; + + /// <summary> + /// The noun for enable/disable winget admin settings. + /// </summary> + public const string Setting = "WinGetSetting"; + + /// <summary> + /// The noun to get the winget settings. + /// </summary> + public const string Settings = "WinGetSettings"; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/CliCommand.cs @@ -0,0 +1,114 @@ +// ----------------------------------------------------------------------------- +// <copyright file="CliCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Commands +{ + using System.Management.Automation; + using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Helpers; + + /// <summary> + /// Commands that just calls winget.exe underneath. + /// </summary> + public sealed class CliCommand : BaseCommand + { + /// <summary> + /// Initializes a new instance of the <see cref="CliCommand"/> class. + /// </summary> + /// <param name="psCmdlet">PSCmdlet.</param> + public CliCommand(PSCmdlet psCmdlet) + : base(psCmdlet) + { + } + + /// <summary> + /// Enables admin setting. + /// </summary> + /// <param name="name">Setting name.</param> + public void EnableSetting(string name) + { + Utilities.VerifyAdmin(); + _ = this.Run("settings", $"--enable {name}"); + } + + /// <summary> + /// Disables admin setting. + /// </summary> + /// <param name="name">Setting name.</param> + public void DisableSetting(string name) + { + Utilities.VerifyAdmin(); + _ = this.Run("settings", $"--disable {name}"); + } + + /// <summary> + /// Gets winget settings. + /// </summary> + /// <param name="asPlainText">Return as string.</param> + public void GetSettings(bool asPlainText) + { + var result = this.Run("settings", "export"); + + if (asPlainText) + { + this.PsCmdlet.WriteObject(result.StdOut); + } + else + { + this.PsCmdlet.WriteObject(Utilities.ConvertToHashtable(result.StdOut)); + } + } + + /// <summary> + /// Adds source. + /// </summary> + /// <param name="name">Name of source.</param> + /// <param name="arg">Arg of source.</param> + /// <param name="type">Type of source.</param> + public void AddSource(string name, string arg, string type) + { + Utilities.VerifyAdmin(); + if (string.IsNullOrEmpty(type)) + { + _ = this.Run("source", $"add --name {name} --arg {arg}", 300000); + } + else + { + _ = this.Run("source", $"add --name {name} --arg {arg} --type {type}", 300000); + } + } + + /// <summary> + /// Removes source. + /// </summary> + /// <param name="name">Name of source.</param> + public void RemoveSource(string name) + { + Utilities.VerifyAdmin(); + _ = this.Run("source", $"remove --name {name}"); + } + + /// <summary> + /// Resets source. + /// </summary> + /// <param name="name">Name of source.</param> + public void ResetSource(string name) + { + Utilities.VerifyAdmin(); + _ = this.Run("source", $"reset --name {name} --force"); + } + + private WinGetCLICommandResult Run(string command, string parameters, int timeOut = 60000) + { + var wingetCliWrapper = new WingetCLIWrapper(); + var result = wingetCliWrapper.RunCommand(command, parameters, timeOut); + result.VerifyExitCode(); + + return result; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UserSettingsCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/UserSettingsCommand.cs @@ -13,6 +13,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands using System.Linq; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Commands.Common; + using Microsoft.WinGet.Client.Engine.Common; using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; using Newtonsoft.Json; @@ -34,8 +35,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands var settingsResult = wingetCliWrapper.RunCommand("settings", "export"); // Read the user settings file property. - var serialized = JObject.Parse(settingsResult.StdOut); - WinGetSettingsFilePath = (string)serialized.GetValue("userSettingsFile"); + WinGetSettingsFilePath = (string)Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"]; } /// <summary> @@ -103,7 +103,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands WinGetSettingsFilePath, settingsJson); - this.PsCmdlet.WriteObject(this.ConvertToHashtable(settingsJson)); + this.PsCmdlet.WriteObject(Utilities.ConvertToHashtable(settingsJson)); } private static JObject HashtableToJObject(Hashtable hashtable) @@ -117,7 +117,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands File.ReadAllText(WinGetSettingsFilePath) : string.Empty; - return this.ConvertToHashtable(content); + return Utilities.ConvertToHashtable(content); } private JObject LocalSettingsFileToJObject() @@ -135,111 +135,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands } } - private Hashtable ConvertToHashtable(string content) - { - if (string.IsNullOrEmpty(content)) - { - return new Hashtable(); - } - - // This is based of https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs. - // So we can convert JSON to Hashtable for Windows PowerShell and PowerShell Core. - try - { - var obj = JsonConvert.DeserializeObject( - content, - new JsonSerializerSettings - { - // This TypeNameHandling setting is required to be secure. - TypeNameHandling = TypeNameHandling.None, - MetadataPropertyHandling = MetadataPropertyHandling.Ignore, - MaxDepth = 1024, - }); - - // It only makes sense that the deserialized object is a dictionary to start. - return obj switch - { - JObject dictionary => this.PopulateHashTableFromJDictionary(dictionary), - _ => throw new UserSettingsReadException() - }; - } - catch (Exception e) - { - throw new UserSettingsReadException(e); - } - } - - private Hashtable PopulateHashTableFromJDictionary(JObject entries) - { - Hashtable result = new (entries.Count); - foreach (var entry in entries) - { - switch (entry.Value) - { - case JArray list: - { - // Array - var listResult = this.PopulateHashTableFromJArray(list); - result.Add(entry.Key, listResult); - break; - } - - case JObject dic: - { - // Dictionary - var dicResult = this.PopulateHashTableFromJDictionary(dic); - result.Add(entry.Key, dicResult); - break; - } - - case JValue value: - { - result.Add(entry.Key, value.Value); - break; - } - } - } - - return result; - } - - private ICollection<object> PopulateHashTableFromJArray(JArray list) - { - var result = new object[list.Count]; - - for (var index = 0; index < list.Count; index++) - { - var element = list[index]; - - switch (element) - { - case JArray array: - { - // Array - var listResult = this.PopulateHashTableFromJArray(array); - result[index] = listResult; - break; - } - - case JObject dic: - { - // Dictionary - var dicResult = this.PopulateHashTableFromJDictionary(dic); - result[index] = dicResult; - break; - } - - case JValue value: - { - result[index] = value.Value; - break; - } - } - } - - return result; - } - private bool CompareUserSettings(Hashtable userSettings, bool ignoreNotSet) { try diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Utilities.cs @@ -7,8 +7,15 @@ namespace Microsoft.WinGet.Client.Engine.Common { using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Management.Automation; using System.Security.Principal; using System.Threading; + using Microsoft.WinGet.Client.Engine.Properties; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; /// <summary> /// This class contains various helper methods for this project. @@ -87,6 +94,17 @@ namespace Microsoft.WinGet.Client.Engine.Common } /// <summary> + /// Throws if not running as admin. + /// </summary> + public static void VerifyAdmin() + { + if (!Utilities.ExecutingAsAdministrator) + { + throw new PSNotSupportedException(Resources.RequiresAdminMessage); + } + } + + /// <summary> /// Adds the WindowsApp local app data path to the user environment path. /// </summary> public static void AddWindowsAppToPath() @@ -101,5 +119,107 @@ namespace Microsoft.WinGet.Client.Engine.Common scope); } } + + /// <summary> + /// This is based of https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs. + /// So we can convert JSON to Hashtable for Windows PowerShell and PowerShell Core. + /// </summary> + /// <param name="content">String content.</param> + /// <returns>The hashtable.</returns> + public static Hashtable ConvertToHashtable(string content) + { + if (string.IsNullOrEmpty(content)) + { + return new Hashtable(); + } + + var obj = JsonConvert.DeserializeObject( + content, + new JsonSerializerSettings + { + // This TypeNameHandling setting is required to be secure. + TypeNameHandling = TypeNameHandling.None, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + MaxDepth = 1024, + }); + + // It only makes sense that the deserialized object is a dictionary to start. + return obj switch + { + JObject dictionary => PopulateHashTableFromJDictionary(dictionary), + _ => throw new InvalidDataException() + }; + } + + private static Hashtable PopulateHashTableFromJDictionary(JObject entries) + { + Hashtable result = new (entries.Count); + foreach (var entry in entries) + { + switch (entry.Value) + { + case JArray list: + { + // Array + var listResult = PopulateHashTableFromJArray(list); + result.Add(entry.Key, listResult); + break; + } + + case JObject dic: + { + // Dictionary + var dicResult = PopulateHashTableFromJDictionary(dic); + result.Add(entry.Key, dicResult); + break; + } + + case JValue value: + { + result.Add(entry.Key, value.Value); + break; + } + } + } + + return result; + } + + private static ICollection<object> PopulateHashTableFromJArray(JArray list) + { + var result = new object[list.Count]; + + for (var index = 0; index < list.Count; index++) + { + var element = list[index]; + + switch (element) + { + case JArray array: + { + // Array + var listResult = PopulateHashTableFromJArray(array); + result[index] = listResult; + break; + } + + case JObject dic: + { + // Dictionary + var dicResult = PopulateHashTableFromJDictionary(dic); + result[index] = dicResult; + break; + } + + case JValue value: + { + result[index] = value.Value; + break; + } + } + } + + return result; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLIException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetCLIException.cs @@ -6,7 +6,6 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions { - using System; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Properties; @@ -24,7 +23,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions /// <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)) + : base(string.Format(Resources.WinGetCLIExceptionMessage, command, parameters, exitCode)) { this.Command = command; this.Parameters = parameters; diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WingetCLIWrapper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WingetCLIWrapper.cs @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { + using System; using System.Diagnostics; using System.IO; using Microsoft.WinGet.Client.Engine.Common; @@ -32,6 +33,11 @@ namespace Microsoft.WinGet.Client.Engine.Helpers /// <param name="fullPath">Use full path or not.</param> public WingetCLIWrapper(bool fullPath = true) { + if (Utilities.ExecutingAsSystem) + { + throw new NotSupportedException(); + } + if (fullPath) { this.wingetPath = WinGetFullPath; diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -268,6 +268,15 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> + /// Looks up a localized string similar to This cmdlet requires administrator privileges to execute.. + /// </summary> + internal static string RequiresAdminMessage { + get { + return ResourceManager.GetString("RequiresAdminMessage", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Single threaded apartment (STA) is not currently supported in this context; run PowerShell in Multi-threaded apartment mode (MTA).. /// </summary> internal static string SingleThreadedApartmentNotSupportedMessage { @@ -304,7 +313,7 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> - /// Looks up a localized string similar to Command {0} failed with exit code {1}. + /// Looks up a localized string similar to Winget command &apos;{0}&apos; with parameters &apos;{1}&apos; failed with exit code &apos;{2}&apos;.. /// </summary> internal static string WinGetCLIExceptionMessage { get { @@ -313,7 +322,7 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> - /// Looks up a localized string similar to Winget command run timed out: {0} {1}. + /// Looks up a localized string similar to Winget command timed out: {0} {1}. /// </summary> internal static string WinGetCLITimeoutExceptionMessage { get { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -155,8 +155,8 @@ <value>Unable to execute winget command.</value> </data> <data name="WinGetCLIExceptionMessage" xml:space="preserve"> - <value>Command {0} failed with exit code {1}</value> - <comment>{Locked="{0}","{1}"} {0} - The winget command executed. {1} - The exit code.</comment> + <value>Winget command '{0}' with parameters '{1}' failed with exit code '{2}'.</value> + <comment>{Locked="{0}","{1}","{2}"} {0} - The winget command executed. {1} - The parameters of the command. {2} - The exit code.</comment> </data> <data name="UserSettingsReadException" xml:space="preserve"> <value>User settings file is invalid.</value> @@ -180,7 +180,7 @@ <value>The Windows Package Manager requires Windows Version 1809 (October 2018 Update) or later.</value> </data> <data name="WinGetCLITimeoutExceptionMessage" xml:space="preserve"> - <value>Winget command run timed out: {0} {1}</value> + <value>Winget command timed out: {0} {1}</value> <comment>{Locked="{0}","{1}"} {0} - The winget command executed. {1} - The parameters of the command.</comment> </data> <data name="IntegrityAppInstallerNotRegisteredMessage" xml:space="preserve"> @@ -221,4 +221,7 @@ <data name="RepairFailureMessage" xml:space="preserve"> <value>Failed to repair winget.</value> </data> + <data name="RequiresAdminMessage" xml:space="preserve"> + <value>This cmdlet requires administrator privileges to execute.</value> + </data> </root> \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Create-CrescendoFunctions.ps1 b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Create-CrescendoFunctions.ps1 @@ -1,53 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -<# - .SYNOPSIS - Creates crescendo module for Microsoft.WinGet.Client and merge module manifests. - - .PARAMETER ConfigurationFile - The crescendo configuration file. - - .PARAMETER ModuleName - The name of the module to be created. - - .PARAMETER ModuleOutputDirectory - Where to output the crescendo output files. -#> - -[CmdletBinding()] -param ( - [Parameter(Mandatory)] - [string] - $ConfigurationFile, - - [Parameter(Mandatory)] - [string] - $ModuleName, - - [Parameter(Mandatory)] - [string] - $ModuleOutputDirectory -) - -if (-not (Get-Module Microsoft.PowerShell.Crescendo)) -{ - Install-Module Microsoft.PowerShell.Crescendo -Force -} - -$dir = $pwd -Set-Location $PSScriptRoot -Write-Host "Generating crescendo module" -Export-CrescendoModule -ConfigurationFile $ConfigurationFile -ModuleName $ModuleName -Force -Set-Location $dir - -Copy-Item "$PSScriptRoot\$ModuleName.psm1" "$ModuleOutputDirectory\Microsoft.WinGet.Client.psm1" -Force -ErrorAction Stop - -# In a perfect world we would check if $ModuleOutputDirectory\$ModuleName.psd1 exists and if it does then load the data -# via Import-PowerShellDataFile and make sure FunctionsToExport contains all the exported functions from the generated -# psd1 file of the Export-CrescendoModule command. We have dynamic expressions on ..\Module\Microsoft.WinGet.Client.psd1 -# so that can't happen easily, so we will just nicely remind you :( -$config = Import-PowerShellDataFile -Path "$PSScriptRoot\$ModuleName.psd1" - -Write-Host "Crescendo module generated. Please verify the FunctionsToExport is updated in ..\Module\Microsoft.WinGet.Client.psd1 if needed" -Write-Host "Generated FunctionsToExport $($config.FunctionsToExport)" diff --git a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json @@ -1,148 +0,0 @@ -{ - "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11", - "Commands": [ - { - "Verb": "Enable", - "Noun": "WinGetSetting", - "Platform": [ - "Windows" - ], - "OriginalName": "winget.exe", - "OriginalCommandElements": [ - "settings", - "--enable" - ], - "Parameters": [ - { - "Mandatory": true, - "Name": "Name", - "OriginalPosition": 0, - "Position": 0, - "ValueFromPipeline": true, - "ValueFromPipelineByPropertyName": true, - "ParameterType": "string" - } - ] - }, - { - "Verb": "Disable", - "Noun": "WinGetSetting", - "Platform": [ - "Windows" - ], - "OriginalName": "winget.exe", - "OriginalCommandElements": [ - "settings", - "--disable" - ], - "Parameters": [ - { - "Mandatory": true, - "Name": "Name", - "OriginalPosition": 0, - "Position": 0, - "ValueFromPipeline": true, - "ValueFromPipelineByPropertyName": true, - "ParameterType": "string" - } - ] - }, - { - "Verb": "Get", - "Noun": "WinGetSettings", - "Platform": [ - "Windows" - ], - "OriginalName": "winget.exe", - "OriginalCommandElements": [ - "settings", - "export" - ] - }, - { - "Verb": "Add", - "Noun": "WinGetSource", - "Platform": [ - "Windows" - ], - "OriginalName": "winget.exe", - "OriginalCommandElements": [ - "source", - "add" - ], - "Parameters": [ - { - "Name": "Name", - "OriginalName": "--name", - "Mandatory": true, - "ParameterType": "string", - "ValueFromPipelineByPropertyName": true, - "Position": 0 - }, - { - "Name": "Argument", - "OriginalName": "--arg", - "Mandatory": true, - "ParameterType": "string", - "ValueFromPipelineByPropertyName": true, - "Position": 1 - }, - { - "Name": "Type", - "OriginalName": "--type", - "Mandatory": false, - "ParameterType": "string", - "ValueFromPipelineByPropertyName": true, - "Position": 2 - } - ] - }, - { - "Verb": "Remove", - "Noun": "WinGetSource", - "Platform": [ - "Windows" - ], - "OriginalName": "winget.exe", - "OriginalCommandElements": [ - "source", - "remove" - ], - "Parameters": [ - { - "Name": "Name", - "OriginalName": "--name", - "Mandatory": true, - "ParameterType": "string", - "ValueFromPipeline": true, - "ValueFromPipelineByPropertyName": true, - "Position": 0 - } - ] - }, - { - "Verb": "Reset", - "Noun": "WinGetSource", - "Platform": [ - "Windows" - ], - "OriginalName": "winget.exe", - "OriginalCommandElements": [ - "source", - "reset", - "--force" - ], - "Parameters": [ - { - "Name": "Name", - "OriginalName": "--name", - "Mandatory": false, - "ParameterType": "string", - "ValueFromPipeline": true, - "ValueFromPipelineByPropertyName": true, - "Position": 0 - } - ] - } - ] -}- \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psd1 @@ -9,7 +9,28 @@ @{ # Script module or binary module file associated with this manifest. -RootModule = 'Microsoft.WinGet.Client.psm1' +RootModule = if ($env:PROCESSOR_ARCHITECTURE -like 'x86') +{ + if ($PSEdition -eq 'Core') + { + "runtimes\win10-x86\lib\net6.0-windows10.0.22000.0\Microsoft.WinGet.Client.Cmdlets.dll" + } + else + { + "runtimes\win10-x86\lib\net48\Microsoft.WinGet.Client.Cmdlets.dll" + } +} +else +{ + if ($PSEdition -eq 'Core') + { + "runtimes\win10-x64\lib\net6.0-windows10.0.22000.0\Microsoft.WinGet.Client.Cmdlets.dll" + } + else + { + "runtimes\win10-x64\lib\net48\Microsoft.WinGet.Client.Cmdlets.dll" + } +} # Version number of this module. ModuleVersion = '0.1.0' @@ -65,54 +86,29 @@ PowerShellVersion = '5.1.0' # Format files (.ps1xml) to be loaded when importing this module FormatsToProcess = 'Format.ps1xml' -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -NestedModules = if ($env:PROCESSOR_ARCHITECTURE -like 'x86') -{ - if ($PSEdition -eq 'Core') - { - "runtimes\win10-x86\lib\net6.0-windows10.0.22000.0\Microsoft.WinGet.Client.Cmdlets.dll" - } - else - { - "runtimes\win10-x86\lib\net48\Microsoft.WinGet.Client.Cmdlets.dll" - } -} -else -{ - if ($PSEdition -eq 'Core') - { - "runtimes\win10-x64\lib\net6.0-windows10.0.22000.0\Microsoft.WinGet.Client.Cmdlets.dll" - } - else - { - "runtimes\win10-x64\lib\net48\Microsoft.WinGet.Client.Cmdlets.dll" - } -} - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = @( - 'Enable-WinGetSetting', - 'Disable-WinGetSetting', - 'Add-WinGetSource', - 'Remove-WinGetSource', - 'Reset-WinGetSource', - 'Get-WinGetSettings' -) +FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @( 'Get-WinGetVersion' - 'Find-WinGetPackage', - 'Get-WinGetPackage', - 'Get-WinGetSource', - 'Install-WinGetPackage', - 'Uninstall-WinGetPackage', - 'Update-WinGetPackage', - 'Get-WinGetUserSettings', - 'Set-WinGetUserSettings', - 'Test-WinGetUserSettings', - 'Assert-WinGetPackageManager', + 'Find-WinGetPackage' + 'Get-WinGetPackage' + 'Get-WinGetSource' + 'Install-WinGetPackage' + 'Uninstall-WinGetPackage' + 'Update-WinGetPackage' + 'Get-WinGetUserSettings' + 'Set-WinGetUserSettings' + 'Test-WinGetUserSettings' + 'Assert-WinGetPackageManager' 'Repair-WinGetPackageManager' + 'Enable-WinGetSetting' + 'Disable-WinGetSetting' + 'Get-WinGetSettings' + 'Add-WinGetSource' + 'Remove-WinGetSource' + 'Reset-WinGetSource' ) # Variables to export from this module diff --git a/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psm1 b/src/PowerShell/Microsoft.WinGet.Client/ModuleFiles/Microsoft.WinGet.Client.psm1 @@ -1,661 +0,0 @@ -# Module created by Microsoft.PowerShell.Crescendo -class PowerShellCustomFunctionAttribute : System.Attribute { - [bool]$RequiresElevation - [string]$Source - PowerShellCustomFunctionAttribute() { $this.RequiresElevation = $false; $this.Source = "Microsoft.PowerShell.Crescendo" } - PowerShellCustomFunctionAttribute([bool]$rElevation) { - $this.RequiresElevation = $rElevation - $this.Source = "Microsoft.PowerShell.Crescendo" - } -} - -<# -.SYNOPSIS -Enables the WinGet setting specified by the `Name` parameter. - -.DESCRIPTION -Enables the WinGet setting specified by the `Name` parameter. -Supported settings: - - LocalManifestFiles - - BypassCertificatePinningForMicrosoftStore - - InstallerHashOverride - - LocalArchiveMalwareScanOverride - -.PARAMETER Name -Specifies the name of the setting to be enabled. - -.INPUTS -None. - -.OUTPUTS -None - -.EXAMPLE -PS> Enable-WinGetSetting -name LocalManifestFiles -#> -function Enable-WinGetSetting -{ -[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] -[CmdletBinding(SupportsShouldProcess)] - -param( -[Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)] -[string]$Name - ) - -BEGIN { - $__PARAMETERMAP = @{ - Name = @{ - OriginalName = '' - OriginalPosition = '0' - Position = '0' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - } - - $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } -} - -PROCESS { - $__boundParameters = $PSBoundParameters - $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name - $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) - $__commandArgs = @() - $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) - if ($__boundParameters["Debug"]){wait-debugger} - $__commandArgs += 'settings' - $__commandArgs += '--enable' - foreach ($paramName in $__boundParameters.Keys| - Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| - Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { - $value = $__boundParameters[$paramName] - $param = $__PARAMETERMAP[$paramName] - if ($param) { - if ($value -is [switch]) { - if ($value.IsPresent) { - if ($param.OriginalName) { $__commandArgs += $param.OriginalName } - } - elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } - } - elseif ( $param.NoGap ) { - $pFmt = "{0}{1}" - if($value -match "\s") { $pFmt = "{0}""{1}""" } - $__commandArgs += $pFmt -f $param.OriginalName, $value - } - else { - if($param.OriginalName) { $__commandArgs += $param.OriginalName } - $__commandArgs += $value | Foreach-Object {$_} - } - } - } - $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} - if ($__boundParameters["Debug"]){wait-debugger} - if ( $__boundParameters["Verbose"]) { - Write-Verbose -Verbose -Message winget.exe - $__commandArgs | Write-Verbose -Verbose - } - $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] - if (! $__handlerInfo ) { - $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present - } - $__handler = $__handlerInfo.Handler - if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { - # check for the application and throw if it cannot be found - if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { - throw "Cannot find executable 'winget.exe'" - } - if ( $__handlerInfo.StreamOutput ) { - & "winget.exe" $__commandArgs | & $__handler - } - else { - $result = & "winget.exe" $__commandArgs - & $__handler $result - } - } -} # end PROCESS -} - -<# -.SYNOPSIS -Disables the WinGet setting specified by the `Name` parameter. - -.DESCRIPTION -Disables the WinGet setting specified by the `Name` parameter. -Supported settings: - - LocalManifestFiles - - BypassCertificatePinningForMicrosoftStore - - InstallerHashOverride - - LocalArchiveMalwareScanOverride - -.PARAMETER Name -Specifies the name of the setting to be disabled. - -.INPUTS -None. - -.OUTPUTS -None - -.EXAMPLE -PS> Disable-WinGetSetting -name LocalManifestFiles -#> -function Disable-WinGetSetting -{ -[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] -[CmdletBinding(SupportsShouldProcess)] - -param( -[Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)] -[string]$Name - ) - -BEGIN { - $__PARAMETERMAP = @{ - Name = @{ - OriginalName = '' - OriginalPosition = '0' - Position = '0' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - } - - $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } -} - -PROCESS { - $__boundParameters = $PSBoundParameters - $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name - $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) - $__commandArgs = @() - $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) - if ($__boundParameters["Debug"]){wait-debugger} - $__commandArgs += 'settings' - $__commandArgs += '--disable' - foreach ($paramName in $__boundParameters.Keys| - Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| - Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { - $value = $__boundParameters[$paramName] - $param = $__PARAMETERMAP[$paramName] - if ($param) { - if ($value -is [switch]) { - if ($value.IsPresent) { - if ($param.OriginalName) { $__commandArgs += $param.OriginalName } - } - elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } - } - elseif ( $param.NoGap ) { - $pFmt = "{0}{1}" - if($value -match "\s") { $pFmt = "{0}""{1}""" } - $__commandArgs += $pFmt -f $param.OriginalName, $value - } - else { - if($param.OriginalName) { $__commandArgs += $param.OriginalName } - $__commandArgs += $value | Foreach-Object {$_} - } - } - } - $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} - if ($__boundParameters["Debug"]){wait-debugger} - if ( $__boundParameters["Verbose"]) { - Write-Verbose -Verbose -Message winget.exe - $__commandArgs | Write-Verbose -Verbose - } - $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] - if (! $__handlerInfo ) { - $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present - } - $__handler = $__handlerInfo.Handler - if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { - # check for the application and throw if it cannot be found - if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { - throw "Cannot find executable 'winget.exe'" - } - if ( $__handlerInfo.StreamOutput ) { - & "winget.exe" $__commandArgs | & $__handler - } - else { - $result = & "winget.exe" $__commandArgs - & $__handler $result - } - } -} # end PROCESS -} - -<# -.SYNOPSIS -Get winget settings. - -.DESCRIPTION -Get the administrator settings values as well as the location of the user settings as json string - -.PARAMETER Name -None - -.INPUTS -None. - -.OUTPUTS -Prints the export settings json. - -.EXAMPLE -PS> Get-WinGetSettings -#> -function Get-WinGetSettings -{ -[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] -[CmdletBinding(SupportsShouldProcess)] - -param( ) - -BEGIN { - $__PARAMETERMAP = @{} - $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } -} - -PROCESS { - $__boundParameters = $PSBoundParameters - $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name - $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) - $__commandArgs = @() - $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) - if ($__boundParameters["Debug"]){wait-debugger} - $__commandArgs += 'settings' - $__commandArgs += 'export' - foreach ($paramName in $__boundParameters.Keys| - Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| - Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { - $value = $__boundParameters[$paramName] - $param = $__PARAMETERMAP[$paramName] - if ($param) { - if ($value -is [switch]) { - if ($value.IsPresent) { - if ($param.OriginalName) { $__commandArgs += $param.OriginalName } - } - elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } - } - elseif ( $param.NoGap ) { - $pFmt = "{0}{1}" - if($value -match "\s") { $pFmt = "{0}""{1}""" } - $__commandArgs += $pFmt -f $param.OriginalName, $value - } - else { - if($param.OriginalName) { $__commandArgs += $param.OriginalName } - $__commandArgs += $value | Foreach-Object {$_} - } - } - } - $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} - if ($__boundParameters["Debug"]){wait-debugger} - if ( $__boundParameters["Verbose"]) { - Write-Verbose -Verbose -Message winget.exe - $__commandArgs | Write-Verbose -Verbose - } - $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] - if (! $__handlerInfo ) { - $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present - } - $__handler = $__handlerInfo.Handler - if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { - # check for the application and throw if it cannot be found - if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { - throw "Cannot find executable 'winget.exe'" - } - if ( $__handlerInfo.StreamOutput ) { - & "winget.exe" $__commandArgs | & $__handler - } - else { - $result = & "winget.exe" $__commandArgs - & $__handler $result - } - } -} # end PROCESS -} - -<# -.SYNOPSIS -Add a new source. - -.DESCRIPTION -Add a new source. A source provides the data for you to discover and install packages. -Only add a new source if you trust it as a secure location. - -.PARAMETER Name -Name of the source. - -.PARAMETER Argument -Argument to be given to the source. - -.PARAMETER Type -Type of the source. - -.INPUTS -None. - -.OUTPUTS -None. - -.EXAMPLE -PS> Add-WinGetSource -Name Contoso -Argument https://www.contoso.com/cache - -#> -function Add-WinGetSource -{ -[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] -[CmdletBinding(SupportsShouldProcess)] - -param( -[Parameter(Position=0,ValueFromPipelineByPropertyName=$true,Mandatory=$true)] -[string]$Name, -[Parameter(Position=1,ValueFromPipelineByPropertyName=$true,Mandatory=$true)] -[string]$Argument, -[Parameter(Position=2,ValueFromPipelineByPropertyName=$true)] -[string]$Type - ) - -BEGIN { - $__PARAMETERMAP = @{ - Name = @{ - OriginalName = '--name' - OriginalPosition = '0' - Position = '0' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - Argument = @{ - OriginalName = '--arg' - OriginalPosition = '0' - Position = '1' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - Type = @{ - OriginalName = '--type' - OriginalPosition = '0' - Position = '2' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - } - - $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } -} - -PROCESS { - $__boundParameters = $PSBoundParameters - $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name - $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) - $__commandArgs = @() - $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) - if ($__boundParameters["Debug"]){wait-debugger} - $__commandArgs += 'source' - $__commandArgs += 'add' - foreach ($paramName in $__boundParameters.Keys| - Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| - Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { - $value = $__boundParameters[$paramName] - $param = $__PARAMETERMAP[$paramName] - if ($param) { - if ($value -is [switch]) { - if ($value.IsPresent) { - if ($param.OriginalName) { $__commandArgs += $param.OriginalName } - } - elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } - } - elseif ( $param.NoGap ) { - $pFmt = "{0}{1}" - if($value -match "\s") { $pFmt = "{0}""{1}""" } - $__commandArgs += $pFmt -f $param.OriginalName, $value - } - else { - if($param.OriginalName) { $__commandArgs += $param.OriginalName } - $__commandArgs += $value | Foreach-Object {$_} - } - } - } - $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} - if ($__boundParameters["Debug"]){wait-debugger} - if ( $__boundParameters["Verbose"]) { - Write-Verbose -Verbose -Message winget.exe - $__commandArgs | Write-Verbose -Verbose - } - $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] - if (! $__handlerInfo ) { - $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present - } - $__handler = $__handlerInfo.Handler - if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { - # check for the application and throw if it cannot be found - if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { - throw "Cannot find executable 'winget.exe'" - } - if ( $__handlerInfo.StreamOutput ) { - & "winget.exe" $__commandArgs | & $__handler - } - else { - $result = & "winget.exe" $__commandArgs - & $__handler $result - } - } -} # end PROCESS -} - -<# -.SYNOPSIS -Remove a specific source. - -.DESCRIPTION -Remove a specific source. The source must already exist to be removed. - -.PARAMETER Name -Name of the source. - -.INPUTS -None. - -.OUTPUTS -None. - -.EXAMPLE -PS> Remove-WinGetSource -Name Contoso - -#> -function Remove-WinGetSource -{ -[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] -[CmdletBinding(SupportsShouldProcess)] - -param( -[Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)] -[string]$Name - ) - -BEGIN { - $__PARAMETERMAP = @{ - Name = @{ - OriginalName = '--name' - OriginalPosition = '0' - Position = '0' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - } - - $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } -} - -PROCESS { - $__boundParameters = $PSBoundParameters - $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name - $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) - $__commandArgs = @() - $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) - if ($__boundParameters["Debug"]){wait-debugger} - $__commandArgs += 'source' - $__commandArgs += 'remove' - foreach ($paramName in $__boundParameters.Keys| - Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| - Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { - $value = $__boundParameters[$paramName] - $param = $__PARAMETERMAP[$paramName] - if ($param) { - if ($value -is [switch]) { - if ($value.IsPresent) { - if ($param.OriginalName) { $__commandArgs += $param.OriginalName } - } - elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } - } - elseif ( $param.NoGap ) { - $pFmt = "{0}{1}" - if($value -match "\s") { $pFmt = "{0}""{1}""" } - $__commandArgs += $pFmt -f $param.OriginalName, $value - } - else { - if($param.OriginalName) { $__commandArgs += $param.OriginalName } - $__commandArgs += $value | Foreach-Object {$_} - } - } - } - $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} - if ($__boundParameters["Debug"]){wait-debugger} - if ( $__boundParameters["Verbose"]) { - Write-Verbose -Verbose -Message winget.exe - $__commandArgs | Write-Verbose -Verbose - } - $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] - if (! $__handlerInfo ) { - $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present - } - $__handler = $__handlerInfo.Handler - if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { - # check for the application and throw if it cannot be found - if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { - throw "Cannot find executable 'winget.exe'" - } - if ( $__handlerInfo.StreamOutput ) { - & "winget.exe" $__commandArgs | & $__handler - } - else { - $result = & "winget.exe" $__commandArgs - & $__handler $result - } - } -} # end PROCESS -} - -<# -.SYNOPSIS -Drops existing sources. Without any argument, this command will drop all sources and add the defaults. - -.DESCRIPTION -Drops existing sources, potentially leaving any local data behind. Without any argument, it will drop all sources and add the defaults. -If a named source is provided, only that source will be dropped. - -.PARAMETER Name -Name of the source. - -.INPUTS -None. - -.OUTPUTS -None. - -.EXAMPLE -PS> Reset-WinGetSource - -.EXAMPLE -PS> Reset-WinGetSource -Name Contoso - -#> -function Reset-WinGetSource -{ -[PowerShellCustomFunctionAttribute(RequiresElevation=$False)] -[CmdletBinding(SupportsShouldProcess)] - -param( -[Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] -[string]$Name - ) - -BEGIN { - $__PARAMETERMAP = @{ - Name = @{ - OriginalName = '--name' - OriginalPosition = '0' - Position = '0' - ParameterType = 'string' - ApplyToExecutable = $False - NoGap = $False - } - } - - $__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } } -} - -PROCESS { - $__boundParameters = $PSBoundParameters - $__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name - $__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_}) - $__commandArgs = @() - $MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)}) - if ($__boundParameters["Debug"]){wait-debugger} - $__commandArgs += 'source' - $__commandArgs += 'reset' - $__commandArgs += '--force' - foreach ($paramName in $__boundParameters.Keys| - Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}| - Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) { - $value = $__boundParameters[$paramName] - $param = $__PARAMETERMAP[$paramName] - if ($param) { - if ($value -is [switch]) { - if ($value.IsPresent) { - if ($param.OriginalName) { $__commandArgs += $param.OriginalName } - } - elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue } - } - elseif ( $param.NoGap ) { - $pFmt = "{0}{1}" - if($value -match "\s") { $pFmt = "{0}""{1}""" } - $__commandArgs += $pFmt -f $param.OriginalName, $value - } - else { - if($param.OriginalName) { $__commandArgs += $param.OriginalName } - $__commandArgs += $value | Foreach-Object {$_} - } - } - } - $__commandArgs = $__commandArgs | Where-Object {$_ -ne $null} - if ($__boundParameters["Debug"]){wait-debugger} - if ( $__boundParameters["Verbose"]) { - Write-Verbose -Verbose -Message winget.exe - $__commandArgs | Write-Verbose -Verbose - } - $__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName] - if (! $__handlerInfo ) { - $__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present - } - $__handler = $__handlerInfo.Handler - if ( $PSCmdlet.ShouldProcess("winget.exe $__commandArgs")) { - # check for the application and throw if it cannot be found - if ( -not (Get-Command -ErrorAction Ignore "winget.exe")) { - throw "Cannot find executable 'winget.exe'" - } - if ( $__handlerInfo.StreamOutput ) { - & "winget.exe" $__commandArgs | & $__handler - } - else { - $result = & "winget.exe" $__commandArgs - & $__handler $result - } - } -} # end PROCESS -} - diff --git a/src/PowerShell/Microsoft.WinGet.Client/README.md b/src/PowerShell/Microsoft.WinGet.Client/README.md @@ -1,10 +1,9 @@ # Windows Package Manager PowerShell Module -The Windows Package Manager PowerShell Module is made up on three components +The Windows Package Manager PowerShell Module is made up on two components -1. Generated functions using `Crescendo` -2. The `Microsoft.WinGet.Client.Cmdlets` project which contains cmdlet implementations. -3. The `Microsoft.WinGet.Client.Engine` project which contain the real logic for the cmdlets. +1. The `Microsoft.WinGet.Client.Cmdlets` project which contains cmdlet implementations. +2. The `Microsoft.WinGet.Client.Engine` project which contain the real logic for the cmdlets. ## Building the PowerShell Module Locally @@ -12,28 +11,17 @@ After building the Microsoft.WinGet.Client.Cmdlets project, the `Microsoft.WinGe This project has after build targets that will copy all the necessary files in the correct location. -## Adding a new function +## Adding a new cmdlet +In order to avoid [assembly dependency conflicts](https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts?view=powershell-7.3) this project uses a custom `AssemblyLoadContext` that load all dependencies. -We don't have an automatic way of producing the psm1 from Crescendo. If a new function is going to be added: -1. Modify `Crescendo\Crescendo.json` -2. Run `Crescendo\Create-CrescendoFunctions.ps1` -3. Copy the new psm1 in `ModulesFiles\Microsoft.WinGet.Client.psm1` -4. Add new function in `ModulesFiles\Microsoft.WinGet.Client.psd1` +Microsoft.WinGet.Client.Cmdlets.dll is the binary that gets loaded when the module is imported. When Microsoft.WinGet.Client.Engine.dll is getting loaded the resolving handler use the custom ALC to load it. Then all the dependencies of that binary will be loaded using that custom context. -## Adding a new cmdlet +The dependencies are laid out in two directories: `DirectDependencies` and `SharedDependencies`. The resolving handler looks for binaries under `DirectDependencies` and uses the custom ALC to load them. The custom ALC load any binaries in `DirectDependencies` and `SharedDependencies`. -This project uses a custom `AssemblyLoadContext` that handles all dependencies loading. The only two binaries that are loaded in the default context are Microsoft.WinGet.Client.Cmdlets.dll and Microsoft.WinGet.Client.Engine.dll. This is to handle [assembly dependency conflicts](https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts?view=powershell-7.3). Because of that, the cmdlet must be defined in Microsoft.WinGet.Client.Cmdlets but the actual implementation in Microsoft.WinGet.Client.Engine. +Exception: WinRT.Runtime.dll doesn't support getting loaded in multiple times in the same process, because it affects static state in the CLR itself. We special case it to get loaded in by the default loader. If the new cmdlet introduces a new dependency, please make sure to add it in the after build targets to copy it in the Dependencies directory. -## Functions -- Add-WinGetSource -- Disable-WinGetSetting -- Enable-WinGetSetting -- Get-WinGetSettings -- Remove-WinGetSource -- Reset-WinGetSource - ## Cmdlets - Assert-WinGetPackageManager - Find-WinGetPackage @@ -47,6 +35,12 @@ If the new cmdlet introduces a new dependency, please make sure to add it in the - Test-WinGetUserSettings - Uninstall-WinGetPackage - Update-WinGetPackage +- Add-WinGetSource +- Disable-WinGetSetting +- Enable-WinGetSetting +- Get-WinGetSettings +- Remove-WinGetSource +- Reset-WinGetSource ## Quick Start Guide diff --git a/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 b/src/PowerShell/Microsoft.WinGet.DSC/Microsoft.WinGet.DSC.psm1 @@ -135,7 +135,7 @@ class WinGetAdminSettings [WinGetAdminSettings] Get() { Assert-WinGetCommand "Get-WinGetSettings" - $settingsJson = Get-WinGetSettings | ConvertFrom-Json -AsHashtable + $settingsJson = Get-WinGetSettings # Get admin setting values. $result = @{ diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -41,14 +41,12 @@ class WinGetModule [string]$Name [string]$ModuleRoot [bool]$HasBinary - [bool]$ForceWinGetDev - WinGetModule([string]$n, [string]$m, [bool]$b, [bool]$d) + WinGetModule([string]$n, [string]$m, [bool]$b) { $this.Name = $n $this.ModuleRoot = $m $this.HasBinary = $b - $this.ForceWinGetDev = $d } } @@ -66,18 +64,15 @@ if ($BuildRoot -eq "") [WinGetModule]::new( "Microsoft.WinGet.DSC", "$PSScriptRoot\..\Microsoft.WinGet.DSC\", - $false, $false), [WinGetModule]::new( "Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\ModuleFiles\", - $true, $true), [WinGetModule]::new( "Microsoft.WinGet.Configuration", "$PSScriptRoot\..\Microsoft.WinGet.Configuration\ModuleFiles\", - $true, - $false) + $true) foreach($module in $modules) { @@ -101,17 +96,6 @@ foreach($module in $modules) # VS won't update the files if there's nothing to build... Write-Host "Copying module $($module.Name)" -ForegroundColor Green xcopy $module.ModuleRoot "$moduleRootOutput\$($module.Name)\" /d /s /f /y - - if ($module.ForceWinGetDev) - { - # This is a terrible and shouldn't be used for real things. We must consider making something smarter and prettier. - # We could make the build system always take the crescendo json and generated the functions from it. The - # build system would know if the original name to be winget.exe or wingetdev.exe, set it on the json and produce - # the psm1 one. We could add a VS after build task that calls powershell and does it, or we could move away - # from crescendo and let the internal implementation knows which one to use based on the build preprocessor macro. - $psm1File = "$moduleRootOutput\$($module.Name)\$($module.Name).psm1" - (Get-Content $psm1File).replace("winget.exe", "wingetdev") | Set-Content $psm1File - } } # Add it to module path if not there.