diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index b5d725bbf0..e02b4b6b9b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -38,6 +38,7 @@ attr AType AUrl Authenticode +azurefd azurewebsites bcp BEFACEF @@ -97,6 +98,7 @@ createmanifestmetadata cswinrt ctc currentuser +cxfsgwfxarb DACL datetimeoffset Dbg @@ -122,8 +124,10 @@ ecfrbrowse ECustom EFGH EFile +efileresource endregion ENDSESSION +EPester epth EQU errmsg @@ -180,6 +184,7 @@ Howto hre hresults hrow +hwg hwnd IARP IAttachment @@ -350,6 +355,7 @@ pidlist pkgmgr pkindex pkix +pme PMS positionals powershellgallery @@ -423,6 +429,7 @@ SMTO sortof sourceforge SOURCESDIRECTORY +sourceversion spamming SPAPI Srinivasan @@ -445,6 +452,7 @@ Syncy sysrefcomp systemnotsupported Tagit +taskhostw TCpp tcs TEMPDIRECTORY @@ -458,6 +466,7 @@ timespan timezone Tlg tombstoned +TOperation TOptions TProgress transitioning diff --git a/Localization/Policies/de-DE/DesktopAppInstaller.adml b/Localization/Policies/de-DE/DesktopAppInstaller.adml index f22b6b9668..5514a33447 100644 --- a/Localization/Policies/de-DE/DesktopAppInstaller.adml +++ b/Localization/Policies/de-DE/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ Wenn Sie diese Einstellung deaktivieren oder nicht konfigurieren, können Benutz Wenn Sie diese Richtlinie aktivieren oder nicht konfigurieren, können Benutzer die Windows Package Manager CLI-Befehle und PowerShell-Cmdlets ausführen. (Vorausgesetzt, die Richtlinie "App-Installationsprogramm aktivieren" ist nicht deaktiviert). Diese Richtlinie hat keinen Einfluss auf die Richtlinie "App-Installationsprogramm aktivieren". + Aktivieren der Windows-Paket-Manager-Konfiguration + Diese Richtlinie steuert, ob das Windows-Paket-Manager Konfigurationsfeature von Benutzern verwendet werden kann. + +Wenn Sie diese Einstellung aktivieren oder nicht konfigurieren, können Benutzer das Konfigurationsfeature Windows-Paket-Manager verwenden. + +Wenn Sie diese Einstellung deaktivieren, können Benutzer das Konfigurationsfeature Windows-Paket-Manager nicht verwenden. diff --git a/Localization/Policies/es-ES/DesktopAppInstaller.adml b/Localization/Policies/es-ES/DesktopAppInstaller.adml index 138e17c671..38c92029c1 100644 --- a/Localization/Policies/es-ES/DesktopAppInstaller.adml +++ b/Localization/Policies/es-ES/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ Si deshabilita esta directiva, los usuarios no podrán ejecutar la CLI de Admini Si habilita o no configura esta directiva, los usuarios podrán ejecutar los comandos de la CLI de Administrador de paquetes de Windows y los cmdlets de PowerShell. (La directiva "Habilitar Instalador de aplicación" proporcionada no está deshabilitada). Esta directiva no invalida la directiva "Habilitar Instalador de aplicación". + Habilitar configuración de Administrador de paquetes de Windows + Esta directiva controla si los usuarios pueden usar la característica de configuración Administrador de paquetes de Windows. + +Si habilita o no establece esta configuración, los usuarios podrán usar la característica de configuración Administrador de paquetes de Windows. + +Si deshabilita esta configuración, los usuarios no podrán usar la característica de configuración Administrador de paquetes de Windows. diff --git a/Localization/Policies/fr-FR/DesktopAppInstaller.adml b/Localization/Policies/fr-FR/DesktopAppInstaller.adml index 82374139c8..cb470a500e 100644 --- a/Localization/Policies/fr-FR/DesktopAppInstaller.adml +++ b/Localization/Policies/fr-FR/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ Si vous désactivez cette stratégie, les utilisateurs ne peuvent pas exécuter Si vous activez ou ne configurez pas cette stratégie, les utilisateurs peuvent exécuter les commandes Gestionnaire de package Windows CLI et PowerShell. (La stratégie « Activer Programme d'installation d'application » fournie n’est pas désactivée). Cette stratégie ne remplace pas la stratégie « Activer Programme d'installation d'application ». + Activer la configuration Gestionnaire de package Windows + Cette stratégie contrôle si la fonctionnalité de configuration Gestionnaire de package Windows peut être utilisée par les utilisateurs. + +Si vous activez ou ne configurez pas ce paramètre, les utilisateurs peuvent utiliser la fonctionnalité de configuration Gestionnaire de package Windows. + +Si vous désactivez ce paramètre, les utilisateurs ne peuvent pas utiliser la fonctionnalité de configuration Gestionnaire de package Windows. diff --git a/Localization/Policies/it-IT/DesktopAppInstaller.adml b/Localization/Policies/it-IT/DesktopAppInstaller.adml index ffdb7f9c96..1304355c59 100644 --- a/Localization/Policies/it-IT/DesktopAppInstaller.adml +++ b/Localization/Policies/it-IT/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ Se disabiliti questo criterio, gli utenti non potranno eseguire l'interfaccia de Se abiliti o non configuri questo criterio, gli utenti potranno eseguire i comandi dell'interfaccia della riga di comando Gestione pacchetti Windows e i cmdlet di PowerShell. (Il criterio "Abilita Programma di installazione app" specificato non è disabilitato). Questo criterio non esegue l'override del criterio "Abilita Programma di installazione app". + Abilita la configurazione di Gestione pacchetti + Questo criterio controlla se la funzionalità di configurazione Gestione pacchetti Windows può essere usata dagli utenti. + +Se si abilita o non si configura questa impostazione, gli utenti potranno usare la funzionalità di configurazione Gestione pacchetti Windows. + +Se si disabilita questa impostazione, gli utenti non potranno usare la funzionalità di configurazione Gestione pacchetti Windows. diff --git a/Localization/Policies/ja-JP/DesktopAppInstaller.adml b/Localization/Policies/ja-JP/DesktopAppInstaller.adml index 8988ae1869..bf3160c5a9 100644 --- a/Localization/Policies/ja-JP/DesktopAppInstaller.adml +++ b/Localization/Policies/ja-JP/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ このポリシーを有効にした場合、または構成しなかった場合、ユーザーは Windows パッケージ マネージャー CLI コマンドと PowerShell コマンドレットを実行できます ([アプリ インストーラーを有効にする] ポリシーが無効になっている場合に限る)。 このポリシーは、[アプリ インストーラーを有効にする] ポリシーをオーバーライドしません。 + Windows パッケージ マネージャーの構成を有効にする + このポリシーでは、Windows パッケージ マネージャー構成機能をユーザーが使用できるかどうかを制御します。 + +この設定を有効にした場合、または構成しなかった場合、ユーザーはWindows パッケージ マネージャー構成機能を使用できます。 + +この設定を無効にすると、ユーザーはWindows パッケージ マネージャー構成機能を使用できなくなります。 diff --git a/Localization/Policies/ko-KR/DesktopAppInstaller.adml b/Localization/Policies/ko-KR/DesktopAppInstaller.adml index 57735b08f0..3e0bc48966 100644 --- a/Localization/Policies/ko-KR/DesktopAppInstaller.adml +++ b/Localization/Policies/ko-KR/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ 이 정책을 활성화하거나 구성하지 않으면 사용자는 Windows 패키지 관리자 CLI 명령 및 PowerShell cmdlet을 실행할 수 있습니다. ('앱 설치 프로그램 활성화' 정책이 비활성화되지 않은 경우). 이 정책은 '앱 설치 프로그램 활성화' 정책보다 우선 적용되지 않습니다. + Windows 패키지 관리자 구성 사용 + 이 정책은 사용자가 Windows 패키지 관리자 구성 기능을 사용할 수 있는지 여부를 제어합니다. + +이 설정을 사용하거나 구성하지 않으면 사용자가 Windows 패키지 관리자 구성 기능을 사용할 수 있습니다. + +이 설정을 사용하지 않으면 사용자가 Windows 패키지 관리자 구성 기능을 사용할 수 없습니다. diff --git a/Localization/Policies/pt-BR/DesktopAppInstaller.adml b/Localization/Policies/pt-BR/DesktopAppInstaller.adml index 5526461bfa..98eaf861a3 100644 --- a/Localization/Policies/pt-BR/DesktopAppInstaller.adml +++ b/Localization/Policies/pt-BR/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ Se você desabilitar essa política, os usuários não poderão executar o Geren Se você habilitar ou não configurar essa política, os usuários poderão executar os comandos Gerenciador de Pacotes do Windows CLI e os cmdlets do PowerShell. (A política "Habilitar Instalador de Aplicativo" fornecida não está desabilitada). Esta política não substitui a política "Habilitar Instalador de Aplicativo". + Habilitar as Configurações do Gerenciador de Pacotes do Windows + Esta política controla se o Gerenciador de Pacotes do Windows de configuração pode ser usado pelos usuários. + +Se você habilitar ou não definir essa configuração, os usuários poderão usar o recurso Gerenciador de Pacotes do Windows configuração. + +Se você desabilitar essa configuração, os usuários não poderão usar o recurso Gerenciador de Pacotes do Windows configuração. diff --git a/Localization/Policies/ru-RU/DesktopAppInstaller.adml b/Localization/Policies/ru-RU/DesktopAppInstaller.adml index a7c05553b3..60d6c11a44 100644 --- a/Localization/Policies/ru-RU/DesktopAppInstaller.adml +++ b/Localization/Policies/ru-RU/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ Если эта политика включена или не настроена, пользователи смогут выполнять команды CLI Диспетчера пакетов Windows и командлеты PowerShell. (Указанная политика "Включить установщик приложений" не отключена.) Эта политика не переопределяет политику "Включить установщик приложений". + Включить конфигурацию Диспетчера пакетов Windows + Эта политика определяет, Диспетчер пакетов Windows ли пользователи могут использовать эту Диспетчер пакетов Windows конфигурацию. + +Если этот параметр включен или не настроен, пользователи смогут использовать Диспетчер пакетов Windows конфигурации. + +Если этот параметр отключен, пользователи не смогут использовать Диспетчер пакетов Windows конфигурации. diff --git a/Localization/Policies/zh-CN/DesktopAppInstaller.adml b/Localization/Policies/zh-CN/DesktopAppInstaller.adml index f86c0a0e94..c0de8f0205 100644 --- a/Localization/Policies/zh-CN/DesktopAppInstaller.adml +++ b/Localization/Policies/zh-CN/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ 如果启用或未配置此策略,用户将能够执行 Windows 程序包管理器 CLI 命令和 PowerShell cmdlet。(未禁用提供的“启用应用安装程序”策略)。 此策略不会替代“启用应用安装程序”策略。 + 启用 Windows 程序包管理器配置 + 此策略控制用户是否可以使用Windows 程序包管理器配置功能。 + +如果启用或未配置此设置,则用户将能够使用Windows 程序包管理器配置功能。 + +如果禁用此设置,则用户将无法使用Windows 程序包管理器配置功能。 diff --git a/Localization/Policies/zh-TW/DesktopAppInstaller.adml b/Localization/Policies/zh-TW/DesktopAppInstaller.adml index 264cda03de..faca09c4db 100644 --- a/Localization/Policies/zh-TW/DesktopAppInstaller.adml +++ b/Localization/Policies/zh-TW/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ 如果您啟用或未設定此原則,使用者將可以執行 Windows 封裝管理員 CLI 命令和 PowerShell Cmdlet。(提供的 [啟用應用程式安裝程式] 原則將不會停用)。 此原則不會覆寫 [啟用應用程式安裝程式] 原則。 + 啟用 Windows 封裝管理員設定 + 此原則控制使用者是否可以使用Windows 封裝管理員設定功能。 + +如果您啟用或未設定這個設定,使用者將可以使用Windows 封裝管理員設定功能。 + +如果您停用這個設定,使用者將無法使用Windows 封裝管理員設定功能。 diff --git a/Localization/Resources/de-DE/winget.resw b/Localization/Resources/de-DE/winget.resw index 699e76ab6e..fb08636cf3 100644 --- a/Localization/Resources/de-DE/winget.resw +++ b/Localization/Resources/de-DE/winget.resw @@ -1697,6 +1697,9 @@ Geben Sie eine Option für --source an, um den Vorgang fortzusetzen. {0} Pakete verfügen über einen Pin, der vor dem Upgrade entfernt werden muss {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + Das Paket kann nicht mit winget aktualisiert werden. Verwenden Sie die vom Herausgeber bereitgestellte Methode zum Aktualisieren dieses Pakets. + Aktualisieren von Paketen auch dann, wenn sie über einen nicht blockierenden Pin verfügen @@ -2074,4 +2077,7 @@ Geben Sie eine Option für --source an, um den Vorgang fortzusetzen. Der Wert "--module-path" muss "currentuser", "allusers", "default" oder ein absoluter Pfad sein. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Aktivieren der Windows-Paket-Manager-Konfiguration + \ No newline at end of file diff --git a/Localization/Resources/es-ES/winget.resw b/Localization/Resources/es-ES/winget.resw index 5a8afbc041..567ea6a4c7 100644 --- a/Localization/Resources/es-ES/winget.resw +++ b/Localization/Resources/es-ES/winget.resw @@ -1697,6 +1697,9 @@ Especifique uno de ellos con la opción --source para continuar. {0} paquetes tienen un pin que debe quitarse antes de la actualización {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + No se puede actualizar el paquete con winget. Use el método proporcionado por el publicador para actualizar este paquete. + Actualizar paquetes aunque tengan un PIN que no sea de bloqueo @@ -2074,4 +2077,7 @@ Especifique uno de ellos con la opción --source para continuar. El valor de `--module-path allusers` debe ser `currentuser`, `allusers`, `default` o una ruta de acceso absoluta. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Habilitar configuración de Administrador de paquetes de Windows + \ No newline at end of file diff --git a/Localization/Resources/fr-FR/winget.resw b/Localization/Resources/fr-FR/winget.resw index e928729a83..4a2303058c 100644 --- a/Localization/Resources/fr-FR/winget.resw +++ b/Localization/Resources/fr-FR/winget.resw @@ -1697,6 +1697,9 @@ Spécifiez l’un d’entre eux à l’aide de l’option --source pour continue {0} package(s) ont une épingle qui doit être supprimée avant la mise à niveau {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + Impossible de mettre à niveau le package à l’aide de Winget. Utilisez la méthode fournie par l’éditeur pour mettre à niveau ce package. + Mettre à niveau les packages même s’ils ont une épingle non bloquante @@ -2074,4 +2077,7 @@ Spécifiez l’un d’entre eux à l’aide de l’option --source pour continue La valeur '--module-path' doit être 'currentuser', 'allusers', 'default' ou un chemin absolu. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Activer la configuration Gestionnaire de package Windows + \ No newline at end of file diff --git a/Localization/Resources/it-IT/winget.resw b/Localization/Resources/it-IT/winget.resw index 8b45f97e9e..260e04d2da 100644 --- a/Localization/Resources/it-IT/winget.resw +++ b/Localization/Resources/it-IT/winget.resw @@ -1697,6 +1697,9 @@ Specificarne uno utilizzando l'opzione --source per continuare. {0} pacchetti hanno un PIN che deve essere rimosso prima dell'aggiornamento {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + Non è possibile aggiornare il pacchetto con WinGet. Usare il metodo fornito dal server di pubblicazione per l'aggiornamento di questo pacchetto. + Aggiorna i pacchetti anche se hanno un PIN non bloccante @@ -2074,4 +2077,7 @@ Specificarne uno utilizzando l'opzione --source per continuare. Il valore di '--module-path' deve essere 'currentuser', 'allusers', 'default' o un percorso assoluto. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Abilita la configurazione di Gestione pacchetti + \ No newline at end of file diff --git a/Localization/Resources/ja-JP/winget.resw b/Localization/Resources/ja-JP/winget.resw index 595bb21b82..cb56fd1077 100644 --- a/Localization/Resources/ja-JP/winget.resw +++ b/Localization/Resources/ja-JP/winget.resw @@ -1697,6 +1697,9 @@ {0} 個のパッケージには、アップグレードする前に削除する必要があるピンがあります {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + winget を使用してパッケージをアップグレードすることはできません。このパッケージをアップグレードするには、発行元から提供された方法を使用してください。 + ブロックでないピンを持っている場合でもパッケージをアップグレードする @@ -2074,4 +2077,7 @@ `--module-path` 値は、`currentuser`、`allusers`、`default`、または絶対パスである必要があります。 {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Windows パッケージ マネージャーの構成を有効にする + \ No newline at end of file diff --git a/Localization/Resources/ko-KR/winget.resw b/Localization/Resources/ko-KR/winget.resw index de853c03ff..8bad81dc58 100644 --- a/Localization/Resources/ko-KR/winget.resw +++ b/Localization/Resources/ko-KR/winget.resw @@ -1697,6 +1697,9 @@ {0} 패키지에 업그레이드 전에 제거해야 하는 PIN이 있습니다. {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + winget을 사용하여 패키지를 업그레이드할 수 없습니다. 게시자가 제공한 메서드를 사용하여 이 패키지를 업그레이드하세요. + 차단되지 않는 PIN이 있는 경우에도 패키지 업그레이드 @@ -2074,4 +2077,7 @@ '--module-path' 값은 'currentuser', 'allusers', 'default' 또는 절대 경로여야 합니다. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Windows 패키지 관리자 구성 사용 + \ No newline at end of file diff --git a/Localization/Resources/pt-BR/winget.resw b/Localization/Resources/pt-BR/winget.resw index 13d0f0769c..a1e191fb3d 100644 --- a/Localization/Resources/pt-BR/winget.resw +++ b/Localization/Resources/pt-BR/winget.resw @@ -1697,6 +1697,9 @@ Especifique um deles usando a opção --source para continuar. {0} pacote(s) têm um pin que precisa ser removido antes da atualização {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + O pacote não pode ser atualizado usando winget. Use o método fornecido pelo editor para atualizar este pacote. + Atualizar pacotes mesmo que eles tenham um marcador sem bloqueio @@ -2074,4 +2077,7 @@ Especifique um deles usando a opção --source para continuar. O valor '--module-path' deve ser 'currentuser', 'allusers', 'default' ou um caminho absoluto. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Habilitar as Configurações do Gerenciador de Pacotes do Windows + \ No newline at end of file diff --git a/Localization/Resources/ru-RU/winget.resw b/Localization/Resources/ru-RU/winget.resw index db17620598..104d2e6d19 100644 --- a/Localization/Resources/ru-RU/winget.resw +++ b/Localization/Resources/ru-RU/winget.resw @@ -1697,6 +1697,9 @@ Несколько ({0}) пакетов используют закрепление, которое необходимо удалить перед обновлением {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + Невозможно обновить пакет с помощью winget. Для обновления этого пакета используйте метод, предоставленный издателем. + Обновление пакетов, даже если они используют неблокирующее закрепление @@ -2074,4 +2077,7 @@ Для --module-path должно быть задано значение currentuser, allusers, default или абсолютный путь. {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + Включить конфигурацию Диспетчера пакетов Windows + \ No newline at end of file diff --git a/Localization/Resources/zh-CN/winget.resw b/Localization/Resources/zh-CN/winget.resw index 720e748757..d306342534 100644 --- a/Localization/Resources/zh-CN/winget.resw +++ b/Localization/Resources/zh-CN/winget.resw @@ -1697,6 +1697,9 @@ {0} 程序包拥有需要在升级前移除的包钉 {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + 无法使用 winget 升级包。请使用发布者提供的方法升级此包。 + 即使程序包拥有非阻止性包钉,也要升级程序包 @@ -2074,4 +2077,7 @@ “--module-path”值必须是“currentuser”、“allusers”、“default”或绝对路径。 {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + 启用 Windows 程序包管理器配置 + \ No newline at end of file diff --git a/Localization/Resources/zh-TW/winget.resw b/Localization/Resources/zh-TW/winget.resw index bf877d133c..d01640b05b 100644 --- a/Localization/Resources/zh-TW/winget.resw +++ b/Localization/Resources/zh-TW/winget.resw @@ -1697,6 +1697,9 @@ {0} 個包裹具有更新前需要先移除的釘選 {Locked="{0}"} {0} is a placeholder that is replaced by an integer number of packages with pins that prevent upgrade + + 無法使用 winget 升級封裝。請使用發行者提供的方法來升級此套件。 + 即使有非封鎖釘選仍要更新套件 @@ -2074,4 +2077,7 @@ '--module-path' 值必須是 'currentuser'、'allusers'、'default' 或絕對路徑。 {Locked="{--module-path}, {currentuser}, {allusers}, {default}} + + 啟用 Windows 封裝管理員設定 + \ No newline at end of file diff --git a/doc/Settings.md b/doc/Settings.md index 96faa510a3..1e56b714dd 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -272,4 +272,15 @@ You can enable the feature as shown below. "experimentalFeatures": { "configuration": true }, +``` + +### windowsFeature + +This feature enables the ability to enable Windows Feature dependencies during installation. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "windowsFeature": true + }, ``` \ No newline at end of file diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json index 279b9ed486..3911e4e8f7 100644 --- a/schemas/JSON/settings/settings.schema.0.2.json +++ b/schemas/JSON/settings/settings.schema.0.2.json @@ -241,6 +241,11 @@ "description": "Enable support for configuration", "type": "boolean", "default": false + }, + "windowsFeature": { + "description": "Enable support for enabling Windows Feature(s)", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln index 9ec79883cd..924fc183e5 100644 --- a/src/AppInstallerCLI.sln +++ b/src/AppInstallerCLI.sln @@ -1577,13 +1577,9 @@ Global {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|x86.ActiveCfg = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Fuzzing|x86.Build.0 = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU - {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|Any CPU.Build.0 = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|ARM64.ActiveCfg = Release|Any CPU - {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|ARM64.Build.0 = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x64.ActiveCfg = Release|Any CPU - {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x64.Build.0 = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x86.ActiveCfg = Release|Any CPU - {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.PowerShell|x86.Build.0 = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|Any CPU.Build.0 = Release|Any CPU {52EC37D6-088C-40D3-AD0B-BDE8F8DAF9EB}.Release|ARM64.ActiveCfg = Release|Any CPU @@ -1616,14 +1612,14 @@ Global {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x64.Build.0 = Debug|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x86.ActiveCfg = Debug|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Fuzzing|x86.Build.0 = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|Any CPU.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|Any CPU.Build.0 = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.Build.0 = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.Build.0 = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.ActiveCfg = Debug|Any CPU - {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.Build.0 = Debug|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|Any CPU.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|Any CPU.Build.0 = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|ARM64.Build.0 = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x64.Build.0 = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.ActiveCfg = Release|Any CPU + {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.PowerShell|x86.Build.0 = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Release|Any CPU.ActiveCfg = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Release|Any CPU.Build.0 = Release|Any CPU {272B2B0E-40D4-4F0F-B187-519A6EF89B10}.Release|ARM64.ActiveCfg = Release|Any CPU diff --git a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp index 876b5ddaa5..2a92bb19ee 100644 --- a/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp @@ -129,6 +129,11 @@ namespace AppInstaller::CLI::Workflow void EnableWindowsFeaturesDependencies(Execution::Context& context) { + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::WindowsFeature)) + { + return; + } + const auto& rootDependencies = context.Get()->Dependencies; if (rootDependencies.Empty()) diff --git a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp index 8749da1009..34a2668286 100644 --- a/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp +++ b/src/AppInstallerCLICore/Workflows/DependencyNodeProcessor.cpp @@ -25,14 +25,14 @@ namespace AppInstaller::CLI::Workflow if (matches.empty()) { - error << Resource::String::DependenciesFlowNoMatches; + error << Resource::String::DependenciesFlowNoMatches << std::endl; return DependencyNodeProcessorResult::Error; } if (matches.size() > 1) { auto dependencyNodeId = Utility::LocIndString{ Utility::Normalize(dependencyNode.Id()) }; - error << Resource::String::DependenciesFlowSourceTooManyMatches(dependencyNodeId); + error << Resource::String::DependenciesFlowSourceTooManyMatches(dependencyNodeId) << std::endl; AICLI_LOG(CLI, Error, << "Too many matches for package " << dependencyNode.Id()); return DependencyNodeProcessorResult::Error; } @@ -60,17 +60,17 @@ namespace AppInstaller::CLI::Workflow // as we won't keep searching for dependencies for installed packages return DependencyNodeProcessorResult::Skipped; } - + if (!m_nodePackageLatestVersion) { - error << Resource::String::DependenciesFlowPackageVersionNotFound(Utility::LocIndView{ Utility::Normalize(packageId) }); + error << Resource::String::DependenciesFlowPackageVersionNotFound(Utility::LocIndView{ Utility::Normalize(packageId) }) << std::endl; AICLI_LOG(CLI, Error, << "Latest available version not found for package " << packageId); return DependencyNodeProcessorResult::Error; } if (!dependencyNode.IsVersionOk(Utility::Version(m_nodePackageLatestVersion->GetProperty(PackageVersionProperty::Version)))) { - error << Resource::String::DependenciesFlowNoMinVersion(Utility::LocIndView{ Utility::Normalize(packageId) }); + error << Resource::String::DependenciesFlowNoMinVersion(Utility::LocIndView{ Utility::Normalize(packageId) }) << std::endl; AICLI_LOG(CLI, Error, << "No suitable min version found for package " << packageId); return DependencyNodeProcessorResult::Error; } @@ -80,7 +80,7 @@ namespace AppInstaller::CLI::Workflow if (m_nodeManifest.Installers.empty()) { - error << Resource::String::DependenciesFlowNoInstallerFound(Utility::LocIndView{ Utility::Normalize(m_nodeManifest.Id) }); + error << Resource::String::DependenciesFlowNoInstallerFound(Utility::LocIndView{ Utility::Normalize(m_nodeManifest.Id) }) << std::endl; AICLI_LOG(CLI, Error, << "Installer not found for manifest " << m_nodeManifest.Id << " with version" << m_nodeManifest.Version); return DependencyNodeProcessorResult::Error; } @@ -98,7 +98,7 @@ namespace AppInstaller::CLI::Workflow { auto manifestId = Utility::LocIndString{ Utility::Normalize(m_nodeManifest.Id) }; auto manifestVersion = Utility::LocIndString{ m_nodeManifest.Version }; - error << Resource::String::DependenciesFlowNoSuitableInstallerFound(manifestId, manifestVersion); + error << Resource::String::DependenciesFlowNoSuitableInstallerFound(manifestId, manifestVersion) << std::endl; AICLI_LOG(CLI, Error, << "No suitable installer found for manifest " << m_nodeManifest.Id << " with version " << m_nodeManifest.Version); return DependencyNodeProcessorResult::Error; } diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs index 5b4fa2cc55..7153e511fb 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -154,6 +154,23 @@ public void ConfigServerUnexpectedExit() FileAssert.DoesNotExist(targetFilePath); } + /// + /// Resource name case insensitive test. + /// + [Test] + public void ResourceCaseInsensitive() + { + TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); + + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\ResourceCaseInsensitive.yml")); + Assert.AreEqual(0, result.ExitCode); + + // The configuration creates a file next to itself with the given contents + string targetFilePath = TestCommon.GetTestDataFile("Configuration\\ResourceCaseInsensitive.txt"); + FileAssert.Exists(targetFilePath); + Assert.AreEqual("Contents!", System.IO.File.ReadAllText(targetFilePath)); + } + private void DeleteTxtFiles() { // Delete all .txt files in the test directory; they are placed there by the tests diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs index ce6abf9b0a..04770c4690 100644 --- a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs +++ b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -53,6 +53,7 @@ public void EnableExperimentalFeatures() WinGetSettingsHelper.ConfigureFeature("experimentalArg", true); WinGetSettingsHelper.ConfigureFeature("experimentalCmd", true); WinGetSettingsHelper.ConfigureFeature("directMSI", true); + WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); var result = TestCommon.RunAICLICommand("features", string.Empty); Assert.True(result.StdOut.Contains("Enabled")); } diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 6f4fe24bfa..8746cae1af 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -916,7 +916,8 @@ public static void EnsureModuleState(string moduleName, bool present, string rep else { string path = customPath; - if (location == TestModuleLocation.WinGetModulePath) + if (location == TestModuleLocation.WinGetModulePath || + location == TestModuleLocation.Default) { path = wingetModulePath; } diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs index 0a6927f844..186e8a7515 100644 --- a/src/AppInstallerCLIE2ETests/InstallCommand.cs +++ b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -16,6 +16,15 @@ namespace AppInstallerCLIE2ETests /// public class InstallCommand : BaseCommand { + /// + /// One time setup. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); + } + /// /// Set up. /// diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/ResourceCaseInsensitive.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/ResourceCaseInsensitive.yml new file mode 100644 index 0000000000..32cec1bade --- /dev/null +++ b/src/AppInstallerCLIE2ETests/TestData/Configuration/ResourceCaseInsensitive.yml @@ -0,0 +1,9 @@ +properties: + configurationVersion: 0.2 + resources: + - resource: xE2ETestResource/e2efileresource + directives: + repository: AppInstallerCLIE2ETestsRepo + settings: + Path: ${WinGetConfigRoot}\ResourceCaseInsensitive.txt + Content: Contents! diff --git a/src/AppInstallerCLITests/WindowsFeature.cpp b/src/AppInstallerCLITests/WindowsFeature.cpp index 4f4d1a308c..df84e23207 100644 --- a/src/AppInstallerCLITests/WindowsFeature.cpp +++ b/src/AppInstallerCLITests/WindowsFeature.cpp @@ -24,6 +24,9 @@ TEST_CASE("InstallFlow_WindowsFeatureDoesNotExist", "[windowsFeature]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -57,6 +60,9 @@ TEST_CASE("InstallFlow_FailedToEnableWindowsFeature", "[windowsFeature]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); @@ -92,6 +98,9 @@ TEST_CASE("InstallFlow_FailedToEnableWindowsFeature_Force", "[windowsFeature]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + // Override with arbitrary DISM api error (DISMAPI_E_DISMAPI_NOT_INITIALIZED) and make windows feature discoverable. HRESULT dismErrorResult = 0xc0040001; LocIndString testFeatureDisplayName = LocIndString{ "Test Windows Feature"_liv }; @@ -139,6 +148,9 @@ TEST_CASE("InstallFlow_RebootRequired", "[windowsFeature]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + // Override with reboot required HRESULT. auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); auto setEnableFeatureOverride = TestHook::SetEnableWindowsFeatureResult_Override(ERROR_SUCCESS_REBOOT_REQUIRED); @@ -173,6 +185,9 @@ TEST_CASE("InstallFlow_RebootRequired_Force", "[windowsFeature]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + // Override with reboot required HRESULT. auto mockDismHelperOverride = TestHook::MockDismHelper_Override(); auto setEnableFeatureOverride = TestHook::SetEnableWindowsFeatureResult_Override(ERROR_SUCCESS_REBOOT_REQUIRED); diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index cb22039a53..dfff527c8a 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #include "pch.h" #include "Public/AppInstallerErrors.h" #include "Public/AppInstallerRuntime.h" @@ -17,6 +16,9 @@ using namespace AppInstaller::Runtime; using namespace AppInstaller::Settings; using namespace AppInstaller::Filesystem; +using namespace winrt::Windows::Web::Http; +using namespace winrt::Windows::Web::Http::Headers; +using namespace winrt::Windows::Web::Http::Filters; namespace AppInstaller::Utility { @@ -141,6 +143,37 @@ namespace AppInstaller::Utility return result; } + std::map GetHeaders(std::string_view url) + { + AICLI_LOG(Core, Verbose, << "Retrieving headers from url: " << url); + + HttpBaseProtocolFilter filter; + filter.CacheControl().ReadBehavior(HttpCacheReadBehavior::MostRecent); + + HttpClient client(filter); + client.DefaultRequestHeaders().Connection().Clear(); + client.DefaultRequestHeaders().Append(L"Connection", L"close"); + client.DefaultRequestHeaders().UserAgent().ParseAdd(Utility::ConvertToUTF16(Runtime::GetDefaultUserAgent().get())); + + winrt::Windows::Foundation::Uri uri{ Utility::ConvertToUTF16(url) }; + HttpRequestMessage request(HttpMethod::Head(), uri); + + HttpResponseMessage response = client.SendRequestAsync(request, HttpCompletionOption::ResponseHeadersRead).get(); + + THROW_HR_IF( + MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.StatusCode()), + response.StatusCode() != HttpStatusCode::Ok); + + std::map result; + + for (const auto& header : response.Headers()) + { + result.emplace(Utility::FoldCase(static_cast(Utility::ConvertToUTF8(header.Key()))), Utility::ConvertToUTF8(header.Value())); + } + + return result; + } + std::optional> DownloadToStream( const std::string& url, std::ostream& dest, diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 33766e9102..066c380d00 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -40,6 +40,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::DirectMSI: return userSettings.Get(); + case ExperimentalFeature::Feature::WindowsFeature: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -69,6 +71,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Argument Sample", "experimentalArg", "https://aka.ms/winget-settings", Feature::ExperimentalArg }; case Feature::DirectMSI: return ExperimentalFeature{ "Direct MSI Installation", "directMSI", "https://aka.ms/winget-settings", Feature::DirectMSI }; + case Feature::WindowsFeature: + return ExperimentalFeature{ "Windows Feature Dependencies", "windowsFeature", "https://aka.ms/winget-settings", Feature::WindowsFeature }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp index cb5ea7843b..3f5a536b8e 100644 --- a/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp +++ b/src/AppInstallerCommonCore/HttpStream/HttpClientWrapper.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include "Public/AppInstallerStrings.h" #include "HttpClientWrapper.h" +#include "Public/AppInstallerRuntime.h" using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Security::Cryptography; @@ -32,6 +33,7 @@ namespace AppInstaller::Utility::HttpStream instance->m_httpClient.DefaultRequestHeaders().Connection().Clear(); instance->m_httpClient.DefaultRequestHeaders().Append(L"Connection", L"Keep-Alive"); + instance->m_httpClient.DefaultRequestHeaders().UserAgent().ParseAdd(Utility::ConvertToUTF16(Runtime::GetDefaultUserAgent().get())); co_await instance->PopulateInfoAsync(); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h index 9b96d18504..8434c6e981 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -59,6 +60,9 @@ namespace AppInstaller::Utility bool computeHash = false, std::optional info = {}); + // Gets the headers for the given URL. + std::map GetHeaders(std::string_view url); + // Determines if the given url is a remote location. bool IsUrlRemote(std::string_view url); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 5c6da86354..73b28ff650 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -23,6 +23,7 @@ namespace AppInstaller::Settings None = 0x0, // Before making DirectMSI non-experimental, it should be part of manifest validation. DirectMSI = 0x1, + WindowsFeature = 0x2, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 4578064e17..9c17f50f10 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -70,6 +70,7 @@ namespace AppInstaller::Settings EFExperimentalCmd, EFExperimentalArg, EFDirectMSI, + EFWindowsFeature, // Telemetry TelemetryDisable, // Install behavior @@ -145,6 +146,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFWindowsFeature, bool, bool, false, ".experimentalFeatures.windowsFeature"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 5dfb83ecff..bc841557fd 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -259,6 +259,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExperimentalCmd) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) + WINGET_VALIDATE_PASS_THROUGH(EFWindowsFeature) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp index aed522316b..a052de76b0 100644 --- a/src/AppInstallerRepositoryCore/CompositeSource.cpp +++ b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -1412,15 +1412,12 @@ namespace AppInstaller::Repository } } - bool addedAvailablePackage = false; - // Directly search for the available package from tracking information. if (trackingPackage) { auto availablePackage = GetTrackedPackageFromAvailableSource(result, trackedSource, trackingPackage->GetProperty(PackageProperty::Id)); if (availablePackage) { - addedAvailablePackage = true; compositePackage->AddAvailablePackage(std::move(availablePackage)); } compositePackage->SetTracking(std::move(trackedSource), std::move(trackingPackage), std::move(trackingPackageVersion)); @@ -1453,7 +1450,6 @@ namespace AppInstaller::Repository }); // For non pinning cases. We found some matching packages here, don't keep going. - addedAvailablePackage = true; compositePackage->AddAvailablePackage(std::move(availablePackage)); } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp index 7e3bb4b1e8..26a60f56a1 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -77,28 +77,39 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Info, << "Reading MSI UpgradeCodes"); std::map upgradeCodes; - // There is no UpgradeCodes key on the x86 view of the registry - Registry::Key upgradeCodesKey = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UpgradeCodes", 0, KEY_READ | KEY_WOW64_64KEY); - - if (upgradeCodesKey) + try { - for (const auto& upgradeCodeKeyRef : upgradeCodesKey) + // There is no UpgradeCodes key on the x86 view of the registry + Registry::Key upgradeCodesKey = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UpgradeCodes", 0, KEY_READ | KEY_WOW64_64KEY); + + if (upgradeCodesKey) { - auto upgradeCode = TryUnpackUpgradeCodeGuid(upgradeCodeKeyRef.Name()); - if (upgradeCode) + for (const auto& upgradeCodeKeyRef : upgradeCodesKey) { - auto upgradeCodeKey = upgradeCodeKeyRef.Open(); - for (const auto& productCodeValue : upgradeCodeKey.Values()) + std::string keyName; + + try { - auto productCode = TryUnpackUpgradeCodeGuid(productCodeValue.Name()); - if (productCode) + keyName = upgradeCodeKeyRef.Name(); + auto upgradeCode = TryUnpackUpgradeCodeGuid(keyName); + if (upgradeCode) { - upgradeCodes[*productCode] = *upgradeCode; + auto upgradeCodeKey = upgradeCodeKeyRef.Open(); + for (const auto& productCodeValue : upgradeCodeKey.Values()) + { + auto productCode = TryUnpackUpgradeCodeGuid(productCodeValue.Name()); + if (productCode) + { + upgradeCodes[*productCode] = *upgradeCode; + } + } } } + CATCH_LOG_MSG("Failed to read upgrade code: %hs", keyName.c_str()); } } - } + } + CATCH_LOG_MSG("Failed to read upgrade codes."); return upgradeCodes; } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index b7952d7ea6..fc863abd8d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -6,6 +6,7 @@ #include "Microsoft/SQLiteIndexSource.h" #include +#include #include #include @@ -17,20 +18,48 @@ namespace AppInstaller::Repository::Microsoft namespace { static constexpr std::string_view s_PreIndexedPackageSourceFactory_PackageFileName = "source.msix"sv; + static constexpr std::string_view s_PreIndexedPackageSourceFactory_PackageVersionHeader = "x-ms-meta-sourceversion"sv; static constexpr std::string_view s_PreIndexedPackageSourceFactory_IndexFileName = "index.db"sv; // TODO: This being hard coded to force using the Public directory name is not ideal. static constexpr std::string_view s_PreIndexedPackageSourceFactory_IndexFilePath = "Public\\index.db"sv; + // Construct the package location from the given details. + // Currently expects that the arg is an https uri pointing to the root of the data. + std::string GetPackageLocation(const std::string& basePath, std::string_view fileName) + { + std::string result = basePath; + if (result.back() != '/') + { + result += '/'; + } + result += fileName; + return result; + } + + std::string GetPrimaryPackageLocation(const SourceDetails& details, std::string_view fileName) + { + THROW_HR_IF(E_INVALIDARG, details.Arg.empty()); + return GetPackageLocation(details.Arg, fileName); + } + + std::string GetAlternatePackageLocation(const SourceDetails& details, std::string_view fileName) + { + return (details.AlternateArg.empty() ? + std::string{} : + GetPackageLocation(details.AlternateArg, fileName)); + } + + // Abstracts the fallback for package location when the MsixInfo is needed. struct PreIndexedPackageInfo { template PreIndexedPackageInfo(const SourceDetails& details, LocationCheck&& locationCheck) { // Get both locations to force the alternate location check - m_packageLocation = GetPrimaryPackageLocation(details); + m_packageLocation = GetPrimaryPackageLocation(details, s_PreIndexedPackageSourceFactory_PackageFileName); locationCheck(m_packageLocation); - std::string alternateLocation = GetAlternatePackageLocation(details); + std::string alternateLocation = GetAlternatePackageLocation(details, s_PreIndexedPackageSourceFactory_PackageFileName); if (!alternateLocation.empty()) { locationCheck(alternateLocation); @@ -72,29 +101,89 @@ namespace AppInstaller::Repository::Microsoft private: std::string m_packageLocation; std::unique_ptr m_msixInfo; + }; - std::string GetPrimaryPackageLocation(const SourceDetails& details) + // Abstracts the fallback for package location when an update is being done. + struct PreIndexedPackageUpdateCheck + { + PreIndexedPackageUpdateCheck(const SourceDetails& details) { - THROW_HR_IF(E_INVALIDARG, details.Arg.empty()); - return GetPackageLocation(details.Arg); - } + m_packageLocation = GetPrimaryPackageLocation(details, s_PreIndexedPackageSourceFactory_PackageFileName); + std::string alternateLocation = GetAlternatePackageLocation(details, s_PreIndexedPackageSourceFactory_PackageFileName); - std::string GetAlternatePackageLocation(const SourceDetails& details) - { - return (details.AlternateArg.empty() ? std::string{} : GetPackageLocation(details.AlternateArg)); + // Try getting the primary location's info + HRESULT primaryHR = S_OK; + + try + { + m_availableVersion = GetAvailableVersionFrom(m_packageLocation); + return; + } + catch (...) + { + if (alternateLocation.empty()) + { + throw; + } + primaryHR = LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageUpdateCheck failed on primary location"); + } + + // Try alternate location + m_packageLocation = std::move(alternateLocation); + + try + { + m_availableVersion = GetAvailableVersionFrom(m_packageLocation); + return; + } + CATCH_LOG_MSG("PreIndexedPackageUpdateCheck failed on alternate location"); + + THROW_HR(primaryHR); } - // Construct the package location from the given details. - // Currently expects that the arg is an https uri pointing to the root of the data. - std::string GetPackageLocation(const std::string& basePath) + const std::string& PackageLocation() const { return m_packageLocation; } + const Msix::PackageVersion& AvailableVersion() const { return m_availableVersion; } + + private: + std::string m_packageLocation; + Msix::PackageVersion m_availableVersion; + + Msix::PackageVersion GetAvailableVersionFrom(const std::string& packageLocation) { - std::string result = basePath; - if (result.back() != '/') + if (Utility::IsUrlRemote(packageLocation)) { - result += '/'; + try + { + std::map headers = Utility::GetHeaders(packageLocation); + auto itr = headers.find(std::string{ s_PreIndexedPackageSourceFactory_PackageVersionHeader }); + if (itr != headers.end()) + { + AICLI_LOG(Repo, Verbose, << "Header indicates version is: " << itr->second); + return { itr->second }; + } + + // We did not find the header we were looking for, log the ones we did find + AICLI_LOG(Repo, Verbose, << "Did not find " << s_PreIndexedPackageSourceFactory_PackageVersionHeader << " in:\n" << [&]() + { + std::ostringstream headerLog; + for (const auto& header : headers) + { + headerLog << " " << header.first << " : " << header.second << '\n'; + } + return std::move(headerLog).str(); + }()); + } + CATCH_LOG(); } - result += s_PreIndexedPackageSourceFactory_PackageFileName; - return result; + + AICLI_LOG(Repo, Verbose, << "Falling back to reading the package data"); + Msix::MsixInfo info{ packageLocation }; + auto manifest = info.GetAppPackageManifests(); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, manifest.size() > 1); + THROW_HR_IF(E_UNEXPECTED, manifest.size() == 0); + + return manifest[0].GetIdentity().GetVersion(); } }; @@ -164,7 +253,7 @@ namespace AppInstaller::Repository::Microsoft return false; } - return UpdateInternal(packageInfo.PackageLocation(), packageInfo.MsixInfo(), details, progress); + return UpdateInternal(packageInfo.PackageLocation(), details, progress); } bool Update(const SourceDetails& details, IProgressCallback& progress) override final @@ -177,7 +266,10 @@ namespace AppInstaller::Repository::Microsoft return UpdateBase(details, true, progress); } - virtual bool UpdateInternal(const std::string& packageLocation, Msix::MsixInfo& packageInfo, const SourceDetails& details, IProgressCallback& progress) = 0; + // Retrieves the currently cached version of the package. + virtual std::optional GetCurrentVersion(const SourceDetails& details) = 0; + + virtual bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress) = 0; bool Remove(const SourceDetails& details, IProgressCallback& progress) override final { @@ -215,14 +307,23 @@ namespace AppInstaller::Repository::Microsoft { THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); - PreIndexedPackageInfo packageInfo(details, [](const std::string&){}); + std::optional currentVersion = GetCurrentVersion(details); + PreIndexedPackageUpdateCheck updateCheck(details); - // The package should not be a bundle - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.MsixInfo().GetIsBundle()); - - // Ensure that family name has not changed - THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, - GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(packageInfo.MsixInfo().GetPackageFullName())); + if (currentVersion) + { + if (currentVersion.value() >= updateCheck.AvailableVersion()) + { + AICLI_LOG(Repo, Verbose, << "Remote source data (" << updateCheck.AvailableVersion().ToString() << + ") was not newer than existing (" << currentVersion.value().ToString() << "), no update needed"); + return true; + } + else + { + AICLI_LOG(Repo, Verbose, << "Remote source data (" << updateCheck.AvailableVersion().ToString() << + ") was newer than existing (" << currentVersion.value().ToString() << "), updating"); + } + } if (progress.IsCancelledBy(CancelReason::Any)) { @@ -236,7 +337,7 @@ namespace AppInstaller::Repository::Microsoft return false; } - return UpdateInternal(packageInfo.PackageLocation(), packageInfo.MsixInfo(), details, progress); + return UpdateInternal(updateCheck.PackageLocation(), details, progress); } }; @@ -305,46 +406,58 @@ namespace AppInstaller::Repository::Microsoft return std::make_shared(details); } - bool UpdateInternal(const std::string& packageLocation, Msix::MsixInfo& packageInfo, const SourceDetails& details, IProgressCallback& progress) override + std::optional GetCurrentVersion(const SourceDetails& details) override { - // Check if the package is newer before calling into deployment. - // This can save us a lot of time over letting deployment detect same version. auto extension = GetExtensionFromDetails(details); + if (extension) { - if (!packageInfo.IsNewerThan(extension->GetPackageVersion())) - { - AICLI_LOG(Repo, Info, << "Remote source data was not newer than existing, no update needed"); - return true; - } + auto version = extension->GetPackageVersion(); + return Msix::PackageVersion{ version.Major, version.Minor, version.Build, version.Revision }; } - - if (progress.IsCancelledBy(CancelReason::Any)) + else { - AICLI_LOG(Repo, Info, << "Cancelling update upon request"); - return false; + return std::nullopt; } + } + bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress) override + { // Due to complications with deployment, download the file and deploy from // a local source while we investigate further. bool download = Utility::IsUrlRemote(packageLocation); - std::filesystem::path tempFile; - winrt::Windows::Foundation::Uri uri = nullptr; + std::filesystem::path localFile; if (download) { - tempFile = Runtime::GetPathTo(Runtime::PathName::Temp); - tempFile /= GetPackageFamilyNameFromDetails(details) + ".msix"; - - Utility::Download(packageLocation, tempFile, Utility::DownloadType::Index, progress); + localFile = Runtime::GetPathTo(Runtime::PathName::Temp); + localFile /= GetPackageFamilyNameFromDetails(details) + ".msix"; - uri = winrt::Windows::Foundation::Uri(tempFile.c_str()); + Utility::Download(packageLocation, localFile, Utility::DownloadType::Index, progress); } else { - uri = winrt::Windows::Foundation::Uri(Utility::ConvertToUTF16(packageLocation)); + localFile = Utility::ConvertToUTF16(packageLocation); } + // Verify the local file + Msix::WriteLockedMsixFile fileLock{ localFile }; + Msix::MsixInfo localMsixInfo{ localFile }; + + // The package should not be a bundle + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, localMsixInfo.GetIsBundle()); + + // Ensure that family name has not changed + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, + GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(localMsixInfo.GetPackageFullName())); + + if (!fileLock.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) + { + AICLI_LOG(Repo, Error, << "Source update failed. Source package failed trust validation."); + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); + } + + winrt::Windows::Foundation::Uri uri = winrt::Windows::Foundation::Uri(localFile.c_str()); Deployment::AddPackage( uri, winrt::Windows::Management::Deployment::DeploymentOptions::None, @@ -356,29 +469,11 @@ namespace AppInstaller::Repository::Microsoft try { // If successful, delete the file - std::filesystem::remove(tempFile); + std::filesystem::remove(localFile); } CATCH_LOG(); } - // Ensure origin if necessary - // TODO: Move to checking this before deploying it. That requires significant code to be written though - // as there is no public API to check the origin directly. - if (WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin)) - { - std::wstring pfn = packageInfo.GetPackageFullNameWide(); - - PackageOrigin origin = PackageOrigin::PackageOrigin_Unknown; - if (SUCCEEDED_WIN32_LOG(GetStagedPackageOrigin(pfn.c_str(), &origin))) - { - if (origin != PackageOrigin::PackageOrigin_Store) - { - Deployment::RemovePackage(Utility::ConvertToUTF8(pfn), winrt::Windows::Management::Deployment::RemovalOptions::None, progress); - THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); - } - } - } - return true; } @@ -480,27 +575,51 @@ namespace AppInstaller::Repository::Microsoft return std::make_shared(details); } - bool UpdateInternal(const std::string& packageLocation, Msix::MsixInfo& packageInfo, const SourceDetails& details, IProgressCallback& progress) override + std::optional GetCurrentVersion(const SourceDetails& details) override { - // We will extract the manifest and index files directly to this location std::filesystem::path packageState = GetStatePathFromDetails(details); - std::filesystem::create_directories(packageState); - std::filesystem::path packagePath = packageState / s_PreIndexedPackageSourceFactory_PackageFileName; if (std::filesystem::exists(packagePath)) { // If we already have a trusted index package, use it to determine if we need to update or not. Msix::WriteLockedMsixFile indexPackage{ packagePath }; - if (indexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin)) && - !packageInfo.IsNewerThan(packagePath)) + if (indexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) { - AICLI_LOG(Repo, Info, << "Remote source data was not newer than existing, no update needed"); - return true; + Msix::MsixInfo msixInfo{ packagePath }; + auto manifest = msixInfo.GetAppPackageManifests(); + + if (manifest.size() == 1) + { + return manifest[0].GetIdentity().GetVersion(); + } } } + return std::nullopt; + } + + bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress) override + { + // We will extract the manifest and index files directly to this location + std::filesystem::path packageState = GetStatePathFromDetails(details); + std::filesystem::create_directories(packageState); + + std::filesystem::path packagePath = packageState / s_PreIndexedPackageSourceFactory_PackageFileName; + std::filesystem::path tempPackagePath = packagePath.u8string() + ".dnld.msix"; + auto removeTempFileOnExit = wil::scope_exit([&]() + { + try + { + std::filesystem::remove(tempPackagePath); + } + catch (...) + { + AICLI_LOG(Repo, Info, << "Failed to remove temp index file at: " << tempPackagePath); + } + }); + if (Utility::IsUrlRemote(packageLocation)) { AppInstaller::Utility::Download(packageLocation, tempPackagePath, AppInstaller::Utility::DownloadType::Index, progress); @@ -511,46 +630,37 @@ namespace AppInstaller::Repository::Microsoft progress.OnProgress(100, 100, ProgressType::Percent); } - bool updateSuccess = false; if (progress.IsCancelledBy(CancelReason::Any)) { AICLI_LOG(Repo, Info, << "Cancelling update upon request"); + return false; } - else + { - bool tempIndexPackageTrusted = false; + // Extra scope to release the file lock right after trust validation. + Msix::WriteLockedMsixFile tempIndexPackage{ tempPackagePath }; + Msix::MsixInfo tempMsixInfo{ tempPackagePath }; - { - // Extra scope to release the file lock right after trust validation. - Msix::WriteLockedMsixFile tempIndexPackage{ tempPackagePath }; - tempIndexPackageTrusted = tempIndexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin)); - } + // The package should not be a bundle + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, tempMsixInfo.GetIsBundle()); - if (tempIndexPackageTrusted) - { - std::filesystem::rename(tempPackagePath, packagePath); - AICLI_LOG(Repo, Info, << "Source update success."); - updateSuccess = true; - } - else + // Ensure that family name has not changed + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, + GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(tempMsixInfo.GetPackageFullName())); + + if (!tempIndexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) { AICLI_LOG(Repo, Error, << "Source update failed. Source package failed trust validation."); + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); } } - if (!updateSuccess) - { - try - { - std::filesystem::remove(tempPackagePath); - } - catch (...) - { - AICLI_LOG(Repo, Info, << "Failed to remove temp index file at: " << tempPackagePath); - } - } + std::filesystem::rename(tempPackagePath, packagePath); + AICLI_LOG(Repo, Info, << "Source update success."); - return updateSuccess; + removeTempFileOnExit.release(); + + return true; } bool RemoveInternal(const SourceDetails& details, IProgressCallback&) override diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index c565efac02..4e8d882830 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -17,6 +17,9 @@ namespace AppInstaller::Repository { + // The interval is of 100 nano seconds precision.This is used by file date period and the Windows::Foundation::TimeSpan exposed in COM api. + using TimeSpan = std::chrono::duration, std::nano>>; + struct ISourceReference; struct ISource; @@ -219,6 +222,16 @@ namespace AppInstaller::Repository // Set caller. void SetCaller(std::string caller); + // Set background update check interval. + void SetBackgroundUpdateInterval(TimeSpan interval); + + // Indicates that we are only interested in the PackageTrackingCatalog for the source. + // Must be set before Open to have effect, and will prevent the underlying source from being updated or opened. + void InstalledPackageInformationOnly(bool value); + + // Determines if this source refers to the given well known source. + bool IsWellKnownSource(WellKnownSource wellKnownSource); + // Execute a search on the source. SearchResult Search(const SearchRequest& request) const; @@ -280,6 +293,8 @@ namespace AppInstaller::Repository std::shared_ptr m_source; bool m_isSourceToBeAdded = false; bool m_isComposite = false; + std::optional m_backgroundUpdateInterval; + bool m_installedPackageInformationOnly = false; mutable PackageTrackingCatalog m_trackingCatalog; }; } diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index b3522a763b..a99e954181 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -53,7 +53,12 @@ namespace AppInstaller::Repository CATCH_LOG(); AICLI_LOG(Repo, Info, << "Source add/update failed, waiting a bit and retrying: " << details.Name); - std::this_thread::sleep_for(2s); + + // Add a bit of randomness to the retry wait time + std::default_random_engine randomEngine(std::random_device{}()); + std::uniform_int_distribution distribution(2000, 10000); + + std::this_thread::sleep_for(std::chrono::milliseconds(distribution(randomEngine))); // If this one fails, maybe the problem is persistent. result = (factory.get()->*member)(details, progress); @@ -92,25 +97,32 @@ namespace AppInstaller::Repository } // Determines whether (and logs why) a source should be updated before it is opened. - bool ShouldUpdateBeforeOpen(const SourceDetails& details) + bool ShouldUpdateBeforeOpen(const SourceDetails& details, std::optional backgroundUpdateInterval) { if (!ContainsAvailablePackagesInternal(details.Origin)) { return false; } - constexpr static auto s_ZeroMins = 0min; - auto autoUpdateTime = User().Get(); + constexpr static TimeSpan s_ZeroMins = 0min; + TimeSpan autoUpdateTime; + if (backgroundUpdateInterval.has_value()) + { + autoUpdateTime = backgroundUpdateInterval.value(); + } + else + { + autoUpdateTime = User().Get(); + } // A value of zero means no auto update, to get update the source run `winget update` if (autoUpdateTime != s_ZeroMins) { - auto autoUpdateTimeMins = std::chrono::minutes(autoUpdateTime); auto timeSinceLastUpdate = std::chrono::system_clock::now() - details.LastUpdateTime; - if (timeSinceLastUpdate > autoUpdateTimeMins) + if (timeSinceLastUpdate > autoUpdateTime) { AICLI_LOG(Repo, Info, << "Source past auto update time [" << - std::chrono::duration_cast(autoUpdateTimeMins).count() << " mins]; it has been at least " << + std::chrono::duration_cast(autoUpdateTime).count() << " mins]; it has been at least " << std::chrono::duration_cast(timeSinceLastUpdate).count() << " mins"); return true; } @@ -190,6 +202,53 @@ namespace AppInstaller::Repository SourceDetails m_details; std::exception_ptr m_exception; }; + + // A wrapper that doesn't actually forward the search requests. + struct TrackingOnlySourceWrapper : public ISource + { + TrackingOnlySourceWrapper(std::shared_ptr wrapped) : m_wrapped(std::move(wrapped)) + { + m_identifier = m_wrapped->GetIdentifier(); + } + + const std::string& GetIdentifier() const override { return m_identifier; } + + SourceDetails& GetDetails() const override { return m_wrapped->GetDetails(); } + + SourceInformation GetInformation() const override { return m_wrapped->GetInformation(); } + + SearchResult Search(const SearchRequest&) const override { return {}; } + + void* CastTo(ISourceType) override { return nullptr; } + + private: + std::shared_ptr m_wrapped; + std::string m_identifier; + }; + + // A wrapper to create another wrapper. + struct TrackingOnlyReferenceWrapper : public ISourceReference + { + TrackingOnlyReferenceWrapper(std::shared_ptr wrapped) : m_wrapped(std::move(wrapped)) {} + + std::string GetIdentifier() override { return m_wrapped->GetIdentifier(); } + + SourceDetails& GetDetails() override { return m_wrapped->GetDetails(); } + + SourceInformation GetInformation() override { return m_wrapped->GetInformation(); } + + bool SetCustomHeader(std::optional) override { return false; } + + void SetCaller(std::string caller) override { m_wrapped->SetCaller(std::move(caller)); } + + std::shared_ptr Open(IProgressCallback&) override + { + return std::make_shared(m_wrapped); + } + + private: + std::shared_ptr m_wrapped; + }; } std::unique_ptr ISourceFactory::GetForType(std::string_view type) @@ -273,7 +332,16 @@ namespace AppInstaller::Repository { THROW_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !IsWellKnownSourceEnabled(source)); - SourceDetails details = GetWellKnownSourceDetailsInternal(source); + auto details = GetWellKnownSourceDetailsInternal(source); + + // Populate metadata + SourceList sourceList; + auto sourceDetailsWithMetadata = sourceList.GetSource(details.Name); + if (sourceDetailsWithMetadata) + { + sourceDetailsWithMetadata->CopyMetadataFieldsTo(details); + } + m_sourceReferences.emplace_back(CreateSourceFromDetails(details)); } @@ -449,6 +517,23 @@ namespace AppInstaller::Repository } } + void Source::SetBackgroundUpdateInterval(TimeSpan interval) + { + m_backgroundUpdateInterval = interval; + } + + void Source::InstalledPackageInformationOnly(bool value) + { + m_installedPackageInformationOnly = value; + } + + bool Source::IsWellKnownSource(WellKnownSource wellKnownSource) + { + SourceDetails details = GetDetails(); + auto wellKnown = CheckForWellKnownSourceMatch(details.Name, details.Arg, details.Type); + return wellKnown && wellKnown.value() == wellKnownSource; + } + SearchResult Source::Search(const SearchRequest& request) const { THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_source); @@ -528,51 +613,68 @@ namespace AppInstaller::Repository if (!m_source) { + std::vector>* sourceReferencesToOpen = nullptr; + std::vector> sourceReferencesForTrackingOnly; std::unique_ptr sourceList; - // Check for updates before opening. - for (auto& sourceReference : m_sourceReferences) + if (m_installedPackageInformationOnly) + { + sourceReferencesToOpen = &sourceReferencesForTrackingOnly; + + // Create a wrapper for each reference + for (auto& sourceReference : m_sourceReferences) + { + sourceReferencesForTrackingOnly.emplace_back(std::make_shared(sourceReference)); + } + } + else { - auto& details = sourceReference->GetDetails(); - if (ShouldUpdateBeforeOpen(details)) + // Check for updates before opening. + for (auto& sourceReference : m_sourceReferences) { - try + auto& details = sourceReference->GetDetails(); + if (ShouldUpdateBeforeOpen(details, m_backgroundUpdateInterval)) { - // TODO: Consider adding a context callback to indicate we are doing the same action - // to avoid the progress bar fill up multiple times. - if (BackgroundUpdateSourceFromDetails(details, progress)) + try { - if (sourceList == nullptr) + // TODO: Consider adding a context callback to indicate we are doing the same action + // to avoid the progress bar fill up multiple times. + if (BackgroundUpdateSourceFromDetails(details, progress)) { - sourceList = std::make_unique(); + if (sourceList == nullptr) + { + sourceList = std::make_unique(); + } + + auto detailsInternal = sourceList->GetSource(details.Name); + detailsInternal->LastUpdateTime = details.LastUpdateTime; + sourceList->SaveMetadata(*detailsInternal); + } + else + { + AICLI_LOG(Repo, Error, << "Failed to update source: " << details.Name); + result.emplace_back(details); } - - auto detailsInternal = sourceList->GetSource(details.Name); - detailsInternal->LastUpdateTime = details.LastUpdateTime; - sourceList->SaveMetadata(*detailsInternal); } - else + catch (...) { - AICLI_LOG(Repo, Error, << "Failed to update source: " << details.Name); + LOG_CAUGHT_EXCEPTION(); + AICLI_LOG(Repo, Warning, << "Failed to update source: " << details.Name); result.emplace_back(details); } } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - AICLI_LOG(Repo, Warning, << "Failed to update source: " << details.Name); - result.emplace_back(details); - } } + + sourceReferencesToOpen = &m_sourceReferences; } - if (m_sourceReferences.size() > 1) + if (sourceReferencesToOpen->size() > 1) { AICLI_LOG(Repo, Info, << "Multiple sources available, creating aggregated source."); auto aggregatedSource = std::make_shared("*DefaultSource"); std::vector> openExceptionProxies; - for (auto& sourceReference : m_sourceReferences) + for (auto& sourceReference : *sourceReferencesToOpen) { AICLI_LOG(Repo, Info, << "Adding to aggregated source: " << sourceReference->GetDetails().Name); @@ -602,7 +704,7 @@ namespace AppInstaller::Repository } else { - m_source = m_sourceReferences[0]->Open(progress); + m_source = (*sourceReferencesToOpen)[0]->Open(progress); } } diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp index fec98c07be..fa27a558f0 100644 --- a/src/AppInstallerRepositoryCore/SourceList.cpp +++ b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -33,7 +33,7 @@ namespace AppInstaller::Repository constexpr std::string_view s_Source_WingetCommunityDefault_Name = "winget"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Arg = "https://cdn.winget.microsoft.com/cache"sv; - constexpr std::string_view s_Source_WingetCommunityDefault_AlternateArg = "https://winget.azureedge.net/cache"sv; + constexpr std::string_view s_Source_WingetCommunityDefault_AlternateArg = "https://winget-cache-pme-cxfsgwfxarb8hwg0.z01.azurefd.net/cache"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Data = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; constexpr std::string_view s_Source_WingetCommunityDefault_Identifier = "Microsoft.Winget.Source_8wekyb3d8bbwe"sv; @@ -43,7 +43,7 @@ namespace AppInstaller::Repository constexpr std::string_view s_Source_DesktopFrameworks_Name = "microsoft.builtin.desktop.frameworks"sv; constexpr std::string_view s_Source_DesktopFrameworks_Arg = "https://cdn.winget.microsoft.com/platform"sv; - constexpr std::string_view s_Source_DesktopFrameworks_AlternateArg = "https://winget.azureedge.net/platform"sv; + constexpr std::string_view s_Source_DesktopFrameworks_AlternateArg = "https://winget-cache-pme-cxfsgwfxarb8hwg0.z01.azurefd.net/platform"sv; constexpr std::string_view s_Source_DesktopFrameworks_Data = "Microsoft.Winget.Platform.Source_8wekyb3d8bbwe"sv; constexpr std::string_view s_Source_DesktopFrameworks_Identifier = "Microsoft.Winget.Platform.Source_8wekyb3d8bbwe"sv; diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h index 09c622c1b2..df1d971c19 100644 --- a/src/AppInstallerRepositoryCore/pch.h +++ b/src/AppInstallerRepositoryCore/pch.h @@ -55,6 +55,7 @@ #include #include #include +#include #include #include #include diff --git a/src/AppInstallerSharedLib/Public/AppInstallerVersions.h b/src/AppInstallerSharedLib/Public/AppInstallerVersions.h index 13f8e57a8f..aa98c64c3f 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerVersions.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerVersions.h @@ -127,12 +127,14 @@ namespace AppInstaller::Utility { UInt64Version() = default; UInt64Version(UINT64 version); + UInt64Version(uint16_t major, uint16_t minor, uint16_t build, uint16_t revision); UInt64Version(std::string&& version, std::string_view splitChars = DefaultSplitChars); UInt64Version(const std::string& version, std::string_view splitChars = DefaultSplitChars) : UInt64Version(std::string(version), splitChars) {} void Assign(std::string version, std::string_view splitChars = DefaultSplitChars) override; void Assign(UINT64 version); + void Assign(uint16_t major, uint16_t minor, uint16_t build, uint16_t revision); UINT64 Major() const { return m_parts.size() > 0 ? m_parts[0].Integer : 0; } UINT64 Minor() const { return m_parts.size() > 1 ? m_parts[1].Integer : 0; } diff --git a/src/AppInstallerSharedLib/Versions.cpp b/src/AppInstallerSharedLib/Versions.cpp index 86139ac6ba..abb097b724 100644 --- a/src/AppInstallerSharedLib/Versions.cpp +++ b/src/AppInstallerSharedLib/Versions.cpp @@ -396,14 +396,24 @@ namespace AppInstaller::Utility Assign(version); } + UInt64Version::UInt64Version(uint16_t major, uint16_t minor, uint16_t build, uint16_t revision) + { + Assign(major, minor, build, revision); + } + void UInt64Version::Assign(UINT64 version) { - const UINT64 mask16 = (1 << 16) - 1; - UINT64 revision = version & mask16; - UINT64 build = (version >> 0x10) & mask16; - UINT64 minor = (version >> 0x20) & mask16; - UINT64 major = (version >> 0x30) & mask16; + constexpr UINT64 mask16 = (1 << 16) - 1; + uint16_t revision = version & mask16; + uint16_t build = (version >> 0x10) & mask16; + uint16_t minor = (version >> 0x20) & mask16; + uint16_t major = (version >> 0x30) & mask16; + Assign(major, minor, build, revision); + } + + void UInt64Version::Assign(uint16_t major, uint16_t minor, uint16_t build, uint16_t revision) + { // Construct a string representation of the provided version std::stringstream ssVersion; ssVersion << major diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs index 25d311fa5d..c690e7ddae 100644 --- a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs +++ b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs @@ -29,7 +29,7 @@ public ConfigurationUnitAndResource( ConfigurationUnitInternal configurationUnitInternal, DscResourceInfoInternal dscResourceInfoInternal) { - if (configurationUnitInternal.Unit.Type != dscResourceInfoInternal.Name) + if (!configurationUnitInternal.Unit.Type.Equals(dscResourceInfoInternal.Name, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException(); } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs index f4b30655bc..c2f62d4192 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs @@ -78,6 +78,72 @@ public void CreateUnitProcessor_ResourceExists() processorEnvMock.Verify(); } + /// + /// Test CreateUnitProcessor case insensitive. + /// + [Fact] + public void CreateUnitProcessor_CaseInsensitive() + { + string resourceName = "name"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + var processorEnvMock = new Mock(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is(c => c.Unit.Type.Equals("Name", StringComparison.OrdinalIgnoreCase)))) + .Returns(new DscResourceInfoInternal("Name", moduleName, version)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + Type = resourceName, + }; + unit.Metadata.Add("module", moduleName); + unit.Metadata.Add("version", version.ToString()); + + var unitProcessor = configurationSetProcessor.CreateUnitProcessor(unit); + Assert.NotNull(unitProcessor); + Assert.Equal(unit.Type, unitProcessor.Unit.Type); + + processorEnvMock.Verify(); + } + + /// + /// Test CreateUnitProcessor case insensitive. + /// + [Fact] + public void CreateUnitProcessor_ResourceNameMismatch() + { + string resourceName = "name"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + var processorEnvMock = new Mock(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is(c => c.Unit.Type == resourceName))) + .Returns(new DscResourceInfoInternal("OtherName", moduleName, version)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + Type = resourceName, + }; + unit.Metadata.Add("module", moduleName); + unit.Metadata.Add("version", version.ToString()); + + Assert.Throws(() => configurationSetProcessor.CreateUnitProcessor(unit)); + + processorEnvMock.Verify(); + } + /// /// Test CreateUnitProcessor with no version directive. /// diff --git a/src/Microsoft.Management.Deployment/Helpers.cpp b/src/Microsoft.Management.Deployment/Helpers.cpp index 8adfb3fc36..f414e5738f 100644 --- a/src/Microsoft.Management.Deployment/Helpers.cpp +++ b/src/Microsoft.Management.Deployment/Helpers.cpp @@ -118,4 +118,54 @@ namespace winrt::Microsoft::Management::Deployment::implementation return {}; } + + std::string GetCallerName() + { + // See if caller name is set by caller + std::string callerName = GetComCallerName(""); + + // Get process string + if (callerName.empty()) + { + try + { + auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); + if (SUCCEEDED(hrGetCallerId)) + { + callerName = AppInstaller::Utility::ConvertToUTF8(TryGetCallerProcessInfo(callerProcessId)); + } + } + CATCH_LOG(); + } + + if (callerName.empty()) + { + callerName = "UnknownComCaller"; + } + + return callerName; + } + + bool IsBackgroundProcessForPolicy() + { + bool isBackgroundProcessForPolicy = false; + try + { + auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); + if (SUCCEEDED(hrGetCallerId) && callerProcessId != GetCurrentProcessId()) + { + // OutOfProc case, we check for explorer.exe + auto callerNameWide = AppInstaller::Utility::ConvertToUTF16(GetCallerName()); + auto processName = AppInstaller::Utility::ConvertToUTF8(std::filesystem::path{ callerNameWide }.filename().wstring()); + if (::AppInstaller::Utility::CaseInsensitiveEquals("explorer.exe", processName) || + ::AppInstaller::Utility::CaseInsensitiveEquals("taskhostw.exe", processName)) + { + isBackgroundProcessForPolicy = true; + } + } + } + CATCH_LOG(); + + return isBackgroundProcessForPolicy; + } } \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/Helpers.h b/src/Microsoft.Management.Deployment/Helpers.h index 8d9ea9cb2b..384dd3ad58 100644 --- a/src/Microsoft.Management.Deployment/Helpers.h +++ b/src/Microsoft.Management.Deployment/Helpers.h @@ -18,4 +18,6 @@ namespace winrt::Microsoft::Management::Deployment::implementation HRESULT EnsureComCallerHasCapability(Capability requiredCapability); std::pair GetCallerProcessId(); std::wstring TryGetCallerProcessInfo(DWORD callerProcessId); + std::string GetCallerName(); + bool IsBackgroundProcessForPolicy(); } diff --git a/src/Microsoft.Management.Deployment/PackageCatalogReference.cpp b/src/Microsoft.Management.Deployment/PackageCatalogReference.cpp index 11031cc5c7..43a9255547 100644 --- a/src/Microsoft.Management.Deployment/PackageCatalogReference.cpp +++ b/src/Microsoft.Management.Deployment/PackageCatalogReference.cpp @@ -15,46 +15,38 @@ #include #include #include +#include #include namespace winrt::Microsoft::Management::Deployment::implementation { - namespace + void PackageCatalogReference::Initialize(winrt::Microsoft::Management::Deployment::PackageCatalogInfo packageCatalogInfo, ::AppInstaller::Repository::Source sourceReference) { - std::string GetCallerName() + m_info = packageCatalogInfo; + m_sourceReference = std::move(sourceReference); + m_packageCatalogBackgroundUpdateInterval = ::AppInstaller::Settings::User().Get<::AppInstaller::Settings::Setting::AutoUpdateTimeInMinutes>(); + + if (IsBackgroundProcessForPolicy()) { - // See if caller name is set by caller - static auto callerName = GetComCallerName(""); + // Delay the default update interval for these background processes + static constexpr winrt::Windows::Foundation::TimeSpan s_PackageCatalogUpdateIntervalDelay_Base = 168h; //1 week - // Get process string - if (callerName.empty()) - { - try - { - auto [hrGetCallerId, callerProcessId] = GetCallerProcessId(); - THROW_IF_FAILED(hrGetCallerId); - callerName = AppInstaller::Utility::ConvertToUTF8(TryGetCallerProcessInfo(callerProcessId)); - } - CATCH_LOG(); - } + // Add a bit of randomness to the default interval time + std::default_random_engine randomEngine(std::random_device{}()); + std::uniform_int_distribution distribution(0, 604800); - if (callerName.empty()) - { - callerName = "UnknownComCaller"; - } + m_packageCatalogBackgroundUpdateInterval = s_PackageCatalogUpdateIntervalDelay_Base + std::chrono::seconds(distribution(randomEngine)); - return callerName; + // Prevent any update / data processing by default for these background processes for now + m_installedPackageInformationOnly = m_sourceReference.IsWellKnownSource(AppInstaller::Repository::WellKnownSource::WinGet); } } - void PackageCatalogReference::Initialize(winrt::Microsoft::Management::Deployment::PackageCatalogInfo packageCatalogInfo, ::AppInstaller::Repository::Source sourceReference) - { - m_info = packageCatalogInfo; - m_sourceReference = std::move(sourceReference); - } + void PackageCatalogReference::Initialize(winrt::Microsoft::Management::Deployment::CreateCompositePackageCatalogOptions options) { m_compositePackageCatalogOptions = options; } + bool PackageCatalogReference::IsComposite() { return (m_compositePackageCatalogOptions != nullptr); @@ -89,10 +81,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation return GetConnectCatalogErrorResult(); } - if (!m_acceptSourceAgreements && SourceAgreements().Size() != 0) - { - return GetConnectSourceAgreementsNotAcceptedErrorResult(); - } + std::string callerName = GetCallerName(); ::AppInstaller::ProgressCallback progress; ::AppInstaller::Repository::Source source; @@ -103,9 +92,16 @@ namespace winrt::Microsoft::Management::Deployment::implementation for (uint32_t i = 0; i < m_compositePackageCatalogOptions.Catalogs().Size(); ++i) { auto catalog = m_compositePackageCatalogOptions.Catalogs().GetAt(i); + if (!catalog.AcceptSourceAgreements() && catalog.SourceAgreements().Size() != 0) + { + return GetConnectSourceAgreementsNotAcceptedErrorResult(); + } + winrt::Microsoft::Management::Deployment::implementation::PackageCatalogReference* catalogImpl = get_self(catalog); auto copy = catalogImpl->m_sourceReference; - copy.SetCaller(GetCallerName()); + copy.SetCaller(callerName); + copy.SetBackgroundUpdateInterval(catalog.PackageCatalogBackgroundUpdateInterval()); + copy.InstalledPackageInformationOnly(catalog.InstalledPackageInformationOnly()); copy.Open(progress); remoteSources.emplace_back(std::move(copy)); } @@ -140,8 +136,15 @@ namespace winrt::Microsoft::Management::Deployment::implementation } else { + if (!m_acceptSourceAgreements && SourceAgreements().Size() != 0) + { + return GetConnectSourceAgreementsNotAcceptedErrorResult(); + } + source = m_sourceReference; - source.SetCaller(GetCallerName()); + source.SetCaller(callerName); + source.SetBackgroundUpdateInterval(PackageCatalogBackgroundUpdateInterval()); + source.InstalledPackageInformationOnly(m_installedPackageInformationOnly); source.Open(progress); } @@ -171,11 +174,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation std::call_once(m_sourceAgreementsOnceFlag, [&]() { - for (auto const& agreement : m_sourceReference.GetInformation().SourceAgreements) + if (!IsComposite()) { - auto sourceAgreement = winrt::make_self>(); - sourceAgreement->Initialize(agreement); - m_sourceAgreements.Append(*sourceAgreement); + for (auto const& agreement : m_sourceReference.GetInformation().SourceAgreements) + { + auto sourceAgreement = winrt::make_self>(); + sourceAgreement->Initialize(agreement); + m_sourceAgreements.Append(*sourceAgreement); + } } }); return m_sourceAgreements.GetView(); @@ -207,10 +213,44 @@ namespace winrt::Microsoft::Management::Deployment::implementation } void PackageCatalogReference::AcceptSourceAgreements(bool value) { + if (IsComposite()) + { + // Can't set AcceptSourceAgreements on a composite. Callers should set it on each non-composite PackageCatalogReference in the composite. + throw winrt::hresult_illegal_state_change(); + } m_acceptSourceAgreements = value; } bool PackageCatalogReference::AcceptSourceAgreements() { return m_acceptSourceAgreements; } + + void PackageCatalogReference::PackageCatalogBackgroundUpdateInterval(winrt::Windows::Foundation::TimeSpan const& value) + { + if (IsComposite()) + { + // Can't set PackageCatalogBackgroundUpdateInterval on a composite. Callers should set it on each non-composite PackageCatalogReference in the composite. + throw winrt::hresult_illegal_state_change(); + } + m_packageCatalogBackgroundUpdateInterval = value; + } + winrt::Windows::Foundation::TimeSpan PackageCatalogReference::PackageCatalogBackgroundUpdateInterval() + { + return m_packageCatalogBackgroundUpdateInterval; + } + + bool PackageCatalogReference::InstalledPackageInformationOnly() + { + return m_installedPackageInformationOnly; + } + + void PackageCatalogReference::InstalledPackageInformationOnly(bool value) + { + if (IsComposite()) + { + throw winrt::hresult_illegal_state_change(); + } + + m_installedPackageInformationOnly = value; + } } diff --git a/src/Microsoft.Management.Deployment/PackageCatalogReference.h b/src/Microsoft.Management.Deployment/PackageCatalogReference.h index 4dcb2e4dc0..ab18dbb655 100644 --- a/src/Microsoft.Management.Deployment/PackageCatalogReference.h +++ b/src/Microsoft.Management.Deployment/PackageCatalogReference.h @@ -11,8 +11,8 @@ namespace winrt::Microsoft::Management::Deployment::implementation PackageCatalogReference() = default; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) - void Initialize(winrt::Microsoft::Management::Deployment::PackageCatalogInfo packageCatalogInfo, ::AppInstaller::Repository::Source sourceReference); - void Initialize(winrt::Microsoft::Management::Deployment::CreateCompositePackageCatalogOptions options); + void Initialize(Deployment::PackageCatalogInfo packageCatalogInfo, ::AppInstaller::Repository::Source sourceReference); + void Initialize(Deployment::CreateCompositePackageCatalogOptions options); #endif bool IsComposite(); @@ -22,9 +22,14 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Windows::Foundation::Collections::IVectorView SourceAgreements(); hstring AdditionalPackageCatalogArguments(); void AdditionalPackageCatalogArguments(hstring const& value); - // Contract 6.0 + // Contract 6 bool AcceptSourceAgreements(); void AcceptSourceAgreements(bool value); + // Contract 8.0 + winrt::Windows::Foundation::TimeSpan PackageCatalogBackgroundUpdateInterval(); + void PackageCatalogBackgroundUpdateInterval(winrt::Windows::Foundation::TimeSpan const& value); + bool InstalledPackageInformationOnly(); + void InstalledPackageInformationOnly(bool value); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: @@ -34,7 +39,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation ::AppInstaller::Repository::Source m_sourceReference; std::optional m_additionalPackageCatalogArguments; bool m_acceptSourceAgreements = true; + bool m_installedPackageInformationOnly = false; std::once_flag m_sourceAgreementsOnceFlag; + winrt::Windows::Foundation::TimeSpan m_packageCatalogBackgroundUpdateInterval = winrt::Windows::Foundation::TimeSpan::zero(); #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.cpp b/src/Microsoft.Management.Deployment/PackageManager.cpp index 8d597b0bd7..efbb8e2cc3 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.cpp +++ b/src/Microsoft.Management.Deployment/PackageManager.cpp @@ -39,8 +39,23 @@ using namespace ::AppInstaller::CLI::Execution; namespace winrt::Microsoft::Management::Deployment::implementation { + namespace + { + void LogStartupIfApplicable() + { + static std::once_flag logStartupOnceFlag; + std::call_once(logStartupOnceFlag, + [&]() + { + ::AppInstaller::Logging::Telemetry().SetCaller(GetCallerName()); + ::AppInstaller::Logging::Telemetry().LogStartup(true); + }); + } + } + winrt::Windows::Foundation::Collections::IVectorView PackageManager::GetPackageCatalogs() { + LogStartupIfApplicable(); Windows::Foundation::Collections::IVector catalogs{ winrt::single_threaded_vector() }; std::vector<::AppInstaller::Repository::SourceDetails> sources = ::AppInstaller::Repository::Source::GetCurrentSources(); for (uint32_t i = 0; i < sources.size(); i++) @@ -57,6 +72,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageCatalogReference PackageManager::GetPredefinedPackageCatalog(winrt::Microsoft::Management::Deployment::PredefinedPackageCatalog const& predefinedPackageCatalog) { + LogStartupIfApplicable(); ::AppInstaller::Repository::Source source; switch (predefinedPackageCatalog) { @@ -81,6 +97,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageCatalogReference PackageManager::GetLocalPackageCatalog(winrt::Microsoft::Management::Deployment::LocalPackageCatalog const& localPackageCatalog) { + LogStartupIfApplicable(); ::AppInstaller::Repository::Source source; switch (localPackageCatalog) { @@ -102,6 +119,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageCatalogReference PackageManager::GetPackageCatalogByName(hstring const& catalogName) { + LogStartupIfApplicable(); std::string name = winrt::to_string(catalogName); if (name.empty()) { @@ -152,6 +170,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::PackageCatalogReference PackageManager::CreateCompositePackageCatalog(winrt::Microsoft::Management::Deployment::CreateCompositePackageCatalogOptions const& options) { + LogStartupIfApplicable(); if (!options) { // Can't make a composite source if the options aren't specified. diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl index af33b31b27..ba706b9d42 100644 --- a/src/Microsoft.Management.Deployment/PackageManager.idl +++ b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(7)] + [contractversion(9)] apicontract WindowsPackageManagerContract{}; /// State of the install @@ -749,6 +749,19 @@ namespace Microsoft.Management.Deployment Boolean AcceptSourceAgreements; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 8)] + { + /// Time interval for package catalog to check for an update. Setting to zero will disable the check for update. + Windows.Foundation.TimeSpan PackageCatalogBackgroundUpdateInterval; + } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 9)] + { + // When set to true, the opened catalog will only provide the information regarding packages installed from this catalog. + // In this mode, no external resources should be required. + Boolean InstalledPackageInformationOnly; + } } /// Catalogs with PackageCatalogOrigin Predefined diff --git a/src/Microsoft.Management.Deployment/pch.h b/src/Microsoft.Management.Deployment/pch.h index a12993ff40..15d3018c14 100644 --- a/src/Microsoft.Management.Deployment/pch.h +++ b/src/Microsoft.Management.Deployment/pch.h @@ -5,4 +5,5 @@ #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs index 9e4f1b4b29..ce79ea7c72 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs @@ -19,6 +19,7 @@ public abstract class OpenConfiguration : PSCmdlet /// Gets or sets the configuration file. /// [Parameter( + Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] @@ -28,6 +29,7 @@ public abstract class OpenConfiguration : PSCmdlet /// Gets or sets custom location to install modules. /// [Parameter( + Position = 1, ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] public string ModulePath { get; set; } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs index a0da9a984c..e647279413 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/CompleteWinGetConfigurationCmdlet.cs @@ -7,7 +7,6 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { using System.Management.Automation; - using System.Threading; using Microsoft.WinGet.Configuration.Engine.Commands; using Microsoft.WinGet.Configuration.Engine.PSObjects; @@ -19,10 +18,13 @@ namespace Microsoft.WinGet.Configuration.Cmdlets [Cmdlet(VerbsLifecycle.Complete, "WinGetConfiguration")] public sealed class CompleteWinGetConfigurationCmdlet : PSCmdlet { + private ConfigurationCommand runningCommand = null; + /// /// Gets or sets the configuration task. /// [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -33,8 +35,20 @@ public sealed class CompleteWinGetConfigurationCmdlet : PSCmdlet /// protected override void ProcessRecord() { - var configCommand = new ConfigurationCommand(this); - configCommand.Continue(this.ConfigurationJob); + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Continue(this.ConfigurationJob); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(this.ConfigurationJob); + this.runningCommand.Cancel(); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConfirmWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConfirmWinGetConfigurationCmdlet.cs new file mode 100644 index 0000000000..d7b5fb2147 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConfirmWinGetConfigurationCmdlet.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// + /// Confirm-WinGetConfiguration + /// Validates winget configuration. + /// + [Cmdlet(VerbsLifecycle.Confirm, "WinGetConfiguration")] + public class ConfirmWinGetConfigurationCmdlet : PSCmdlet + { + private ConfigurationCommand runningCommand = null; + + /// + /// Gets or sets the configuration set. + /// + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// + /// Validate configuration. + /// + protected override void ProcessRecord() + { + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Validate(this.Set); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs index 5ad332e5c5..ec4b94fe82 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationDetailsCmdlet.cs @@ -18,10 +18,13 @@ namespace Microsoft.WinGet.Configuration.Cmdlets [Cmdlet(VerbsCommon.Get, "WinGetConfigurationDetails")] public sealed class GetWinGetConfigurationDetailsCmdlet : PSCmdlet { + private ConfigurationCommand runningCommand = null; + /// /// Gets or sets the configuration set. /// [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -32,8 +35,19 @@ public sealed class GetWinGetConfigurationDetailsCmdlet : PSCmdlet /// protected override void ProcessRecord() { - var configCommand = new ConfigurationCommand(this); - configCommand.GetDetails(this.Set); + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.GetDetails(this.Set); + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs index 6231cd6dc6..b22f07eb4b 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/InvokeWinGetConfigurationCmdlet.cs @@ -6,8 +6,8 @@ namespace Microsoft.WinGet.Configuration.Cmdlets { + using System; using System.Management.Automation; - using System.Threading; using Microsoft.WinGet.Configuration.Engine.Commands; using Microsoft.WinGet.Configuration.Engine.PSObjects; @@ -20,11 +20,13 @@ namespace Microsoft.WinGet.Configuration.Cmdlets public sealed class InvokeWinGetConfigurationCmdlet : PSCmdlet { private bool acceptedAgreements = false; + private ConfigurationCommand runningCommand = null; /// /// Gets or sets the configuration set. /// [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -41,7 +43,7 @@ public sealed class InvokeWinGetConfigurationCmdlet : PSCmdlet /// protected override void BeginProcessing() { - this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool()); + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool(), true); } /// @@ -51,8 +53,19 @@ protected override void ProcessRecord() { if (this.acceptedAgreements) { - var configCommand = new ConfigurationCommand(this); - configCommand.Apply(this.Set); + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Apply(this.Set); + } + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs index 29f828f134..2e40575611 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StartWinGetConfigurationCmdlet.cs @@ -24,6 +24,7 @@ public sealed class StartWinGetConfigurationCmdlet : PSCmdlet /// Gets or sets the configuration set. /// [Parameter( + Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] @@ -40,7 +41,7 @@ public sealed class StartWinGetConfigurationCmdlet : PSCmdlet /// protected override void BeginProcessing() { - this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool()); + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool(), true); } /// diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StopWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StopWinGetConfigurationCmdlet.cs new file mode 100644 index 0000000000..5b59cc44f0 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/StopWinGetConfigurationCmdlet.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// + /// Stop-WinGetConfiguration. + /// Cancels a configuration previously started by Start-WinGetConfiguration. + /// + [Cmdlet(VerbsLifecycle.Stop, "WinGetConfiguration")] + public sealed class StopWinGetConfigurationCmdlet : PSCmdlet + { + /// + /// Gets or sets the configuration task. + /// + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationJob ConfigurationJob { get; set; } + + /// + /// Starts to apply the configuration and wait for it to complete. + /// + protected override void ProcessRecord() + { + var configCommand = new ConfigurationCommand(this); + configCommand.Cancel(this.ConfigurationJob); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/TestWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/TestWinGetConfigurationCmdlet.cs new file mode 100644 index 0000000000..8aaab94f15 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/TestWinGetConfigurationCmdlet.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// + /// Test-WinGetConfiguration + /// Tests configuration. + /// + [Cmdlet(VerbsDiagnostic.Test, "WinGetConfiguration")] + public class TestWinGetConfigurationCmdlet : PSCmdlet + { + private bool acceptedAgreements = false; + private ConfigurationCommand runningCommand = null; + + /// + /// Gets or sets the configuration set. + /// + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// + /// Gets or sets a value indicating whether to accept the configuration agreements. + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AcceptConfigurationAgreements { get; set; } + + /// + /// Pre-processing operations. + /// + protected override void BeginProcessing() + { + this.acceptedAgreements = ConfigurationCommand.ConfirmConfigurationProcessing(this, this.AcceptConfigurationAgreements.ToBool(), false); + } + + /// + /// Test configuration. + /// + protected override void ProcessRecord() + { + if (this.acceptedAgreements) + { + this.runningCommand = new ConfigurationCommand(this); + this.runningCommand.Test(this.Set); + } + } + + /// + /// Interrupts currently running code within the command. + /// + protected override void StopProcessing() + { + if (this.runningCommand != null) + { + this.runningCommand.Cancel(); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs index 4b1ad6cb6e..d7171c5e5b 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -32,8 +32,6 @@ public abstract class AsyncCommand private readonly Thread originalThread; private readonly CancellationTokenSource source = new (); - - private CancellationToken cancellationToken; private BlockingCollection queuedStreams = new (); private int progressActivityId = 0; @@ -73,7 +71,6 @@ public AsyncCommand(PSCmdlet psCmdlet) this.PsCmdlet = psCmdlet; this.originalThread = Thread.CurrentThread; - this.cancellationToken = this.source.Token; } /// @@ -122,6 +119,14 @@ public enum StreamType /// protected PSCmdlet PsCmdlet { get; private set; } + /// + /// Request cancellation for this command. + /// + public void Cancel() + { + this.source.Cancel(); + } + /// /// Complete this operation. /// @@ -378,6 +383,15 @@ internal int GetNewProgressActivityId() return Interlocked.Increment(ref this.progressActivityId); } + /// + /// Gets the cancellation token. + /// + /// CancellationToken. + protected CancellationToken GetCancellationToken() + { + return this.source.Token; + } + private void CmdletWrite(StreamType streamType, object data, AsyncCommand writeCommand) { switch (streamType) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs index 3110b634ef..bd59d6e290 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands { using System; using System.IO; + using System.Linq; using System.Management.Automation; using System.Threading.Tasks; using Microsoft.Management.Configuration; @@ -40,15 +41,17 @@ public ConfigurationCommand(PSCmdlet psCmdlet) /// /// PSCmdlet. /// Has already accepted. + /// If prompt is for apply. /// If accepted. - public static bool ConfirmConfigurationProcessing(PSCmdlet psCmdlet, bool hasAccepted) + public static bool ConfirmConfigurationProcessing(PSCmdlet psCmdlet, bool hasAccepted, bool isApply) { bool result = false; if (!hasAccepted) { + var prompt = isApply ? Resources.ConfigurationWarningPromptApply : Resources.ConfigurationWarningPromptTest; bool yesToAll = false; bool noToAll = false; - result = psCmdlet.ShouldContinue(Resources.ConfigurationWarningPrompt, Resources.ConfigurationWarning, true, ref yesToAll, ref noToAll); + result = psCmdlet.ShouldContinue(prompt, Resources.ConfigurationWarning, true, ref yesToAll, ref noToAll); if (yesToAll) { @@ -116,8 +119,7 @@ public void GetDetails(PSConfigurationSet psConfigurationSet) { if (!psConfigurationSet.CanProcess()) { - // TODO: better exception or just write info and return null. - throw new Exception("Someone is using me!!!"); + throw new InvalidOperationException(); } var runningTask = this.RunOnMTA( @@ -125,7 +127,7 @@ public void GetDetails(PSConfigurationSet psConfigurationSet) { try { - psConfigurationSet = await this.GetSetDetailsAsync(psConfigurationSet); + psConfigurationSet = await this.GetSetDetailsAsync(psConfigurationSet, false); } finally { @@ -153,16 +155,16 @@ public void GetDetails(PSConfigurationSet psConfigurationSet) /// PSConfigurationSet. public void StartApply(PSConfigurationSet psConfigurationSet) { - if (psConfigurationSet.Set.State == ConfigurationSetState.Completed) + // if (psConfigurationSet.Set.State == ConfigurationSetState.Completed) + if (psConfigurationSet.ApplyCompleted) { this.Write(StreamType.Warning, "Processing this set is completed"); - return; + throw new InvalidOperationException(); } if (!psConfigurationSet.CanProcess()) { - // TODO: better exception or just write info and return null. - throw new Exception("Someone is using me!!!"); + throw new InvalidOperationException(); } var configurationJob = this.StartApplyInternal(psConfigurationSet); @@ -181,36 +183,108 @@ public void StartApply(PSConfigurationSet psConfigurationSet) /// The configuration job. public void Continue(PSConfigurationJob psConfigurationJob) { - if (psConfigurationJob.ConfigurationTask.IsCompleted) + if (psConfigurationJob.ApplyConfigurationTask.IsCompleted) { // It is safe to print all output. psConfigurationJob.StartCommand.ConsumeAndWriteStreams(this); this.Write(StreamType.Verbose, "The task was completed before waiting"); - if (psConfigurationJob.ConfigurationTask.IsCompletedSuccessfully) + if (psConfigurationJob.ApplyConfigurationTask.IsCompletedSuccessfully) { this.Write(StreamType.Verbose, "Completed successfully"); - this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); + this.Write(StreamType.Object, psConfigurationJob.ApplyConfigurationTask.Result); return; } - else if (psConfigurationJob.ConfigurationTask.IsFaulted) + else if (psConfigurationJob.ApplyConfigurationTask.IsFaulted) { this.Write(StreamType.Verbose, "Completed faulted before waiting"); // Maybe just write error? - throw psConfigurationJob.ConfigurationTask.Exception!; + throw psConfigurationJob.ApplyConfigurationTask.Exception!; } } this.ContinueHelper(psConfigurationJob); } + /// + /// Test configuration. + /// + /// PSConfigurationSet. + public void Test(PSConfigurationSet psConfigurationSet) + { + psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); + + if (!psConfigurationSet.CanProcess()) + { + throw new InvalidOperationException(); + } + + var runningTask = this.RunOnMTA( + async () => + { + try + { + return await this.TestConfigurationAsync(psConfigurationSet); + } + finally + { + this.Complete(); + psConfigurationSet.DoneProcessing(); + } + }); + + this.Wait(runningTask); + this.Write(StreamType.Object, runningTask.Result); + } + + /// + /// Validates configuration. + /// + /// PSConfigurationSet. + public void Validate(PSConfigurationSet psConfigurationSet) + { + psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); + + if (!psConfigurationSet.CanProcess()) + { + throw new InvalidOperationException(); + } + + var runningTask = this.RunOnMTA( + async () => + { + try + { + var setResult = await this.ApplyConfigurationAsync(psConfigurationSet, ApplyConfigurationSetFlags.PerformConsistencyCheckOnly); + return new PSValidateConfigurationSetResult(setResult); + } + finally + { + this.Complete(); + psConfigurationSet.DoneProcessing(); + } + }); + + this.Wait(runningTask); + this.Write(StreamType.Object, runningTask.Result); + } + + /// + /// Cancels a configuration job. + /// + /// PSConfiguration job. + public void Cancel(PSConfigurationJob psConfigurationJob) + { + psConfigurationJob.StartCommand.Cancel(); + } + private void ContinueHelper(PSConfigurationJob psConfigurationJob) { // Signal the command that it can write to streams and wait for task. this.Write(StreamType.Verbose, "Waiting for task to complete"); - psConfigurationJob.StartCommand.Wait(psConfigurationJob.ConfigurationTask, this); - this.Write(StreamType.Object, psConfigurationJob.ConfigurationTask.Result); + psConfigurationJob.StartCommand.Wait(psConfigurationJob.ApplyConfigurationTask, this); + this.Write(StreamType.Object, psConfigurationJob.ApplyConfigurationTask.Result); } private IConfigurationSetProcessorFactory CreateFactory(OpenConfigurationParameters openParams) @@ -259,31 +333,31 @@ private PSConfigurationJob StartApplyInternal(PSConfigurationSet psConfiguration { psConfigurationSet.PsProcessor.UpdateDiagnosticCmdlet(this); - var runningTask = this.RunOnMTA( + var runningTask = this.RunOnMTA( async () => { try { - psConfigurationSet = await this.ApplyConfigurationAsync(psConfigurationSet); + var setResult = await this.ApplyConfigurationAsync(psConfigurationSet, ApplyConfigurationSetFlags.None); + psConfigurationSet.ApplyCompleted = true; + return new PSApplyConfigurationSetResult(setResult); } finally { this.Complete(); psConfigurationSet.DoneProcessing(); } - - return psConfigurationSet; }); return new PSConfigurationJob(runningTask, this); } - private async Task ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet) + private async Task ApplyConfigurationAsync(PSConfigurationSet psConfigurationSet, ApplyConfigurationSetFlags flags) { if (!psConfigurationSet.HasDetails) { this.Write(StreamType.Verbose, "Getting details for configuration set"); - await this.GetSetDetailsAsync(psConfigurationSet); + await this.GetSetDetailsAsync(psConfigurationSet, true); } var processor = psConfigurationSet.PsProcessor.Processor; @@ -297,84 +371,116 @@ private async Task ApplyConfigurationAsync(PSConfigurationSe Resources.OperationCompleted, set.Units.Count); - var applyTask = processor.ApplySetAsync(set, ApplyConfigurationSetFlags.None); + var applyTask = processor.ApplySetAsync(set, flags); applyTask.Progress = applyProgressOutput.Progress; try { - var result = await applyTask; - applyProgressOutput.HandleUnreportedProgress(result); + var result = await applyTask.AsTask(this.GetCancellationToken()); + applyProgressOutput.HandleProgress(result); + return result; } finally { applyProgressOutput.CompleteProgress(); } - - return psConfigurationSet; } - private async Task GetSetDetailsAsync(PSConfigurationSet psConfigurationSet) + private async Task TestConfigurationAsync(PSConfigurationSet psConfigurationSet) { - var processor = psConfigurationSet.PsProcessor.Processor; - var set = psConfigurationSet.Set; - var totalUnitsCount = set.Units.Count; - - if (totalUnitsCount == 0) + if (!psConfigurationSet.HasDetails) { - this.Write(StreamType.Warning, Resources.ConfigurationFileEmpty); - return psConfigurationSet; + this.Write(StreamType.Verbose, "Getting details for configuration set"); + await this.GetSetDetailsAsync(psConfigurationSet, true); } - var detailsProgressOutput = new GetConfigurationSetDetailsProgressOutput( + var processor = psConfigurationSet.PsProcessor.Processor; + var set = psConfigurationSet.Set; + + var testProgressOutput = new TestConfigurationSetProgressOutput( this, this.GetNewProgressActivityId(), - Resources.ConfigurationGettingDetails, + Resources.ConfigurationAssert, Resources.OperationInProgress, Resources.OperationCompleted, - totalUnitsCount); + set.Units.Count); - var detailsTask = processor.GetSetDetailsAsync(set, ConfigurationUnitDetailFlags.ReadOnly); - detailsTask.Progress = detailsProgressOutput.Progress; + var testTask = processor.TestSetAsync(set); + testTask.Progress = testProgressOutput.Progress; try { - var result = await detailsTask; - detailsProgressOutput.HandleUnits(result.UnitResults); - } - catch (Exception e) - { - this.WriteError( - ErrorRecordErrorId.ConfigurationDetailsError, - e); + var result = await testTask.AsTask(this.GetCancellationToken()); + testProgressOutput.HandleProgress(result); + + return new PSTestConfigurationSetResult(result); } finally { - detailsProgressOutput.CompleteProgress(); + testProgressOutput.CompleteProgress(); } + } + + private async Task GetSetDetailsAsync(PSConfigurationSet psConfigurationSet, bool warnOnError) + { + var processor = psConfigurationSet.PsProcessor.Processor; + var set = psConfigurationSet.Set; + var totalUnitsCount = set.Units.Count; - if (detailsProgressOutput.UnitsShown == 0) + if (totalUnitsCount == 0) { - this.Write(StreamType.Warning, Resources.ConfigurationFailedToGetDetails); + this.Write(StreamType.Warning, Resources.ConfigurationFileEmpty); + return psConfigurationSet; } - else + + try { - psConfigurationSet.HasDetails = true; - } + var detailsProgressOutput = new GetConfigurationSetDetailsProgressOutput( + this, + this.GetNewProgressActivityId(), + Resources.ConfigurationGettingDetails, + Resources.OperationInProgress, + Resources.OperationCompleted, + totalUnitsCount); + + var detailsTask = processor.GetSetDetailsAsync(set, ConfigurationUnitDetailFlags.ReadOnly); + detailsTask.Progress = detailsProgressOutput.Progress; + + try + { + var result = await detailsTask.AsTask(this.GetCancellationToken()); + detailsProgressOutput.HandleProgress(result); - return psConfigurationSet; - } + if (result.UnitResults.Where(u => u.ResultInformation.ResultCode != null).Any()) + { + throw new GetDetailsException(result.UnitResults); + } - private void LogFailedGetConfigurationUnitDetails(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) - { - if (resultInformation.ResultCode != null) + if (detailsProgressOutput.UnitsShown == 0) + { + throw new GetDetailsException(); + } + + psConfigurationSet.HasDetails = true; + } + finally + { + detailsProgressOutput.CompleteProgress(); + } + } + catch (Exception e) { - string errorMessage = $"Failed to get unit details for {unit.Type} 0x{resultInformation.ResultCode.HResult:X}" + - $"{Environment.NewLine}Description: '{resultInformation.Description}'{Environment.NewLine}Details: '{resultInformation.Details}'"; - this.WriteError( - ErrorRecordErrorId.ConfigurationDetailsError, - errorMessage, - resultInformation.ResultCode); + if (warnOnError) + { + this.Write(StreamType.Warning, e.Message); + } + else + { + throw; + } } + + return psConfigurationSet; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs new file mode 100644 index 0000000000..3f6dcf4f5c --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ApplyConfigurationException.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// + /// Exception thrown when there's an error when configuration is applied. + /// + public class ApplyConfigurationException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Apply Result. + internal ApplyConfigurationException(ApplyConfigurationSetResult applyResult) + : base(Resources.ConfigurationFailedToApply) + { + this.HResult = applyResult.ResultCode?.HResult ?? ErrorCodes.WingetConfigErrorSetApplyFailed; + + var results = new List(); + foreach (var unitResult in applyResult.UnitResults) + { + results.Add(new PSApplyConfigurationUnitResult(unitResult)); + } + + this.UnitResults = results; + } + + /// + /// Gets the result of the units. + /// + public IReadOnlyList UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs index 48f5fbfd59..14b010c6fe 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorCodes.cs @@ -12,6 +12,9 @@ namespace Microsoft.WinGet.Configuration.Engine.Exceptions internal static class ErrorCodes { #pragma warning disable SA1600 // ElementsMustBeDocumented +#pragma warning disable SA1310 // Field names should not contain underscore + internal const int S_OK = 0; + internal const int WingetConfigErrorInvalidConfigurationFile = unchecked((int)0x8A15C001); internal const int WingetConfigErrorInvalidYaml = unchecked((int)0x8A15C002); internal const int WingetConfigErrorInvalidFieldType = unchecked((int)0x8A15C003); @@ -36,6 +39,7 @@ internal static class ErrorCodes internal const int WinGetConfigUnitModuleConflict = unchecked((int)0x8A15C107); internal const int WinGetConfigUnitImportModule = unchecked((int)0x8A15C108); internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109); +#pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1600 // ElementsMustBeDocumented } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs index 00706918d6..2dfdc124e8 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/ErrorRecordErrorId.cs @@ -15,15 +15,5 @@ internal enum ErrorRecordErrorId /// Error message from diagnostics. /// ConfigurationDiagnosticError, - - /// - /// Error processing details. - /// - ConfigurationDetailsError, - - /// - /// Error applying configuration. - /// - ConfigurationApplyError, } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs new file mode 100644 index 0000000000..6a422a041b --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/GetDetailsException.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Exceptions +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// + /// Exception thrown while getting details. + /// + public class GetDetailsException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Unit results. + internal GetDetailsException(IReadOnlyList? unitResults = null) + : base(Resources.ConfigurationFailedToGetDetails) + { + var results = new List(); + + if (unitResults != null) + { + foreach (var result in unitResults) + { + results.Add(new PSGetConfigurationDetailsResult(result)); + } + } + + this.UnitDetailsResults = results; + } + + /// + /// Gets the unit details result. + /// + public IReadOnlyList UnitDetailsResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs index 3926e00de8..fe998db8bb 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Exceptions/OpenConfigurationSetException.cs @@ -21,7 +21,7 @@ public class OpenConfigurationSetException : Exception /// /// Open Result. /// Configuration file. - public OpenConfigurationSetException(OpenConfigurationSetResult openResult, string configurationFile) + internal OpenConfigurationSetException(OpenConfigurationSetResult openResult, string configurationFile) : base(GetMessage(openResult, configurationFile)) { } @@ -29,32 +29,32 @@ public OpenConfigurationSetException(OpenConfigurationSetResult openResult, stri private static string GetMessage(OpenConfigurationSetResult openResult, string configurationFile) { var sb = new StringBuilder(); - sb.AppendLine($"Failed to open configuration set at {configurationFile} with error 0x{openResult.ResultCode.HResult:X}"); + sb.Append($"Failed to open configuration set at {configurationFile} with error 0x{openResult.ResultCode.HResult:X} "); switch (openResult.ResultCode.HResult) { case ErrorCodes.WingetConfigErrorInvalidFieldType: - sb.AppendLine(string.Format(Resources.ConfigurationFieldInvalidType, openResult.Field)); + sb.Append(string.Format(Resources.ConfigurationFieldInvalidType, openResult.Field)); break; case ErrorCodes.WingetConfigErrorInvalidFieldValue: - sb.AppendLine(string.Format(Resources.ConfigurationFieldInvalidValue, openResult.Field, openResult.Value)); + sb.Append(string.Format(Resources.ConfigurationFieldInvalidValue, openResult.Field, openResult.Value)); break; case ErrorCodes.WingetConfigErrorMissingField: - sb.AppendLine(string.Format(Resources.ConfigurationFieldMissing, openResult.Field)); + sb.Append(string.Format(Resources.ConfigurationFieldMissing, openResult.Field)); break; case ErrorCodes.WingetConfigErrorUnknownConfigurationFileVersion: - sb.AppendLine(string.Format(Resources.ConfigurationFileVersionUnknown, openResult.Value)); + sb.Append(string.Format(Resources.ConfigurationFileVersionUnknown, openResult.Value)); break; case ErrorCodes.WingetConfigErrorInvalidConfigurationFile: case ErrorCodes.WingetConfigErrorInvalidYaml: default: - sb.AppendLine(Resources.ConfigurationFileInvalid); + sb.Append(Resources.ConfigurationFileInvalid); break; } if (openResult.Line != 0) { - sb.AppendLine(string.Format(Resources.SeeLineAndColumn, openResult.Line, openResult.Column)); + sb.Append($" {string.Format(Resources.SeeLineAndColumn, openResult.Line, openResult.Column)}"); } return sb.ToString(); diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs new file mode 100644 index 0000000000..a7ef360957 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Extensions/IAsyncOperationExtensions.cs @@ -0,0 +1,74 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Extensions +{ + using System.Threading; + using System.Threading.Tasks; + using Windows.Foundation; + + /// + /// Extension methods for IAsyncOperation objects. + /// + internal static class IAsyncOperationExtensions + { + /// + /// Wrap IAsyncOperationWithProgress into a task with cancellation support. + /// + /// The result of the operation. + /// The progress data of the operation. + /// The async operation. + /// Optional cancellation token. + /// A task. + public static Task AsTask(this IAsyncOperationWithProgress asyncOperation, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + if (cancellationToken != default) + { + cancellationToken.Register(asyncOperation.Cancel); + } + + asyncOperation.Completed = (asyncInfo, asyncStatus) => + { + switch (asyncStatus) + { + case AsyncStatus.Canceled: + tcs.SetCanceled(); + break; + case AsyncStatus.Completed: + tcs.SetResult(asyncInfo.GetResults()); + break; + case AsyncStatus.Error: + tcs.SetException(asyncInfo.ErrorCode); + break; + case AsyncStatus.Started: + break; + default: + break; + } + }; + + // Make sure to throw operation cancelled exception if needed. + return tcs.Task.ContinueWith( + t => + { + if (t.IsCanceled) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + if (!t.IsFaulted) + { + return t.Result; + } + + // If IsFaulted is true, the task's Status is equal to Faulted, + // and its Exception property will be non-null. + throw t.Exception!; + }); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs index ae3577709e..9b077d832b 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ApplyConfigurationSetProgressOutput.cs @@ -6,30 +6,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { - using System; - using System.Collections.Generic; - using System.Text; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Engine.Exceptions; - using Microsoft.WinGet.Configuration.Engine.Resources; using Windows.Foundation; - using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; /// - /// Helper to handle progress callbacks from ApplyConfigurationSetAsync. + /// Helper to handle progress callbacks from ApplySetAsync. /// - internal class ApplyConfigurationSetProgressOutput + internal class ApplyConfigurationSetProgressOutput : ConfigurationSetProgressOutputBase { - private readonly AsyncCommand cmd; - private readonly int activityId; - private readonly string activity; - private readonly string inProgressMessage; - private readonly string completeMessage; - private readonly int totalUnitsExpected; - - private readonly HashSet unitsCompleted = new (); - private bool isFirstProgress = true; /// @@ -42,66 +27,42 @@ internal class ApplyConfigurationSetProgressOutput /// The activity complete message. /// Total of units expected. public ApplyConfigurationSetProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { - this.cmd = cmd; - this.activityId = activityId; - this.activity = activity; - this.inProgressMessage = inProgressMessage; - this.completeMessage = completeMessage; - this.totalUnitsExpected = totalUnitsExpected; - - // Write initial progress record. - // For some reason, if this is 0 the progress bar is shown full. Start with 1% - this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); } - /// - /// Progress callback. - /// - /// Async operation in progress. - /// Change data. - public void Progress(IAsyncOperationWithProgress operation, ConfigurationSetChangeData data) + /// + public override void Progress(IAsyncOperationWithProgress operation, ConfigurationSetChangeData data) { if (this.isFirstProgress) { - this.HandleUnreportedProgress(operation.GetResults()); + this.HandleProgress(operation.GetResults()); } switch (data.Change) { case ConfigurationSetChangeEventType.UnitStateChanged: - this.HandleUnitProgress(data.Unit, data.UnitState, data.ResultInformation); + this.HandleUnitProgress(data.Unit, data.UnitState); break; } } - /// - /// Handle unreported progress. - /// - /// Set result. - public void HandleUnreportedProgress(ApplyConfigurationSetResult result) + /// + public override void HandleProgress(ApplyConfigurationSetResult result) { if (!this.isFirstProgress) { this.isFirstProgress = false; foreach (var unitResult in result.UnitResults) { - this.HandleUnitProgress(unitResult.Unit, unitResult.State, unitResult.ResultInformation); + this.HandleUnitProgress(unitResult.Unit, unitResult.State); } } } - /// - /// Completes the progress bar. - /// - public void CompleteProgress() + private void HandleUnitProgress(ConfigurationUnit unit, ConfigurationUnitState state) { - this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); - } - - private void HandleUnitProgress(ConfigurationUnit unit, ConfigurationUnitState state, IConfigurationUnitResultInformation resultInformation) - { - if (this.unitsCompleted.Contains(unit.InstanceIdentifier)) + if (this.UnitsCompleted.Contains(unit.InstanceIdentifier)) { return; } @@ -114,108 +75,10 @@ private void HandleUnitProgress(ConfigurationUnit unit, ConfigurationUnitState s case ConfigurationUnitState.InProgress: break; case ConfigurationUnitState.Completed: - if (resultInformation.ResultCode != null) - { - string description = resultInformation.Description.Trim(); - var message = this.GetUnitFailedMessage(unit, resultInformation); - - string errorMessage = $"Configuration unit {unit.Type}[{unit.Identifier}] failed with code 0x{resultInformation.ResultCode.HResult:X}" + - $" and error message:\n{description}\n{resultInformation.Details}\n{message}"; - this.cmd.WriteError( - ErrorRecordErrorId.ConfigurationApplyError, - errorMessage, - resultInformation.ResultCode); - } - - this.CompleteUnit(unit); - break; case ConfigurationUnitState.Skipped: - this.cmd.Write(StreamType.Warning, this.GetUnitSkippedMessage(resultInformation)); this.CompleteUnit(unit); break; } } - - private void CompleteUnit(ConfigurationUnit unit) - { - if (this.unitsCompleted.Add(unit.InstanceIdentifier)) - { - this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.unitsCompleted.Count}/{this.totalUnitsExpected}", this.unitsCompleted.Count, this.totalUnitsExpected); - } - } - - private string GetUnitFailedMessage(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) - { - if (resultInformation.ResultCode == null) - { - return string.Format(Resources.ConfigurationUnitFailed, "null"); - } - - int resultCode = resultInformation.ResultCode.HResult; - switch (resultCode) - { - case ErrorCodes.WingetConfigErrorDuplicateIdentifier: - return string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier); - case ErrorCodes.WingetConfigErrorMissingDependency: - return string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInformation.Details); - case ErrorCodes.WingetConfigErrorAssertionFailed: - return Resources.ConfigurationUnitAssertHadNegativeResult; - case ErrorCodes.WinGetConfigUnitNotFound: - return Resources.ConfigurationUnitNotFoundInModule; - case ErrorCodes.WinGetConfigUnitNotFoundRepository: - return Resources.ConfigurationUnitNotFound; - case ErrorCodes.WinGetConfigUnitMultipleMatches: - return Resources.ConfigurationUnitMultipleMatches; - case ErrorCodes.WinGetConfigUnitInvokeGet: - return Resources.ConfigurationUnitFailedDuringGet; - case ErrorCodes.WinGetConfigUnitInvokeTest: - return Resources.ConfigurationUnitFailedDuringTest; - case ErrorCodes.WinGetConfigUnitInvokeSet: - return Resources.ConfigurationUnitFailedDuringSet; - case ErrorCodes.WinGetConfigUnitModuleConflict: - return Resources.ConfigurationUnitModuleConflict; - case ErrorCodes.WinGetConfigUnitImportModule: - return Resources.ConfigurationUnitModuleImportFailed; - case ErrorCodes.WinGetConfigUnitInvokeInvalidResult: - return Resources.ConfigurationUnitReturnedInvalidResult; - } - - switch (resultInformation.ResultSource) - { - case ConfigurationUnitResultSource.ConfigurationSet: - return string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode); - case ConfigurationUnitResultSource.Internal: - return string.Format(Resources.ConfigurationUnitFailedInternal, resultCode); - case ConfigurationUnitResultSource.Precondition: - return string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode); - case ConfigurationUnitResultSource.SystemState: - return string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode); - case ConfigurationUnitResultSource.UnitProcessing: - return string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode); - } - - return string.Format(Resources.ConfigurationUnitFailed, resultCode); - } - - private string GetUnitSkippedMessage(IConfigurationUnitResultInformation resultInformation) - { - if (resultInformation.ResultCode == null) - { - return string.Format(Resources.ConfigurationUnitSkipped, "null"); - } - - int resultCode = resultInformation.ResultCode.HResult; - switch (resultCode) - { - case ErrorCodes.WingetConfigErrorManuallySkipped: - return Resources.ConfigurationUnitManuallySkipped; - case ErrorCodes.WingetConfigErrorDependencyUnsatisfied: - return Resources.ConfigurationUnitNotRunDueToDependency; - case ErrorCodes.WingetConfigErrorAssertionFailed: - return Resources.ConfigurationUnitNotRunDueToFailedAssert; - } - - return string.Format(Resources.ConfigurationUnitSkipped, resultCode); - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs new file mode 100644 index 0000000000..9d04918a21 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/ConfigurationSetProgressOutputBase.cs @@ -0,0 +1,90 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Windows.Foundation; + + /// + /// Helper to handle progress callbacks. + /// + /// The operation result. + /// Progress data. + internal abstract class ConfigurationSetProgressOutputBase + { + private readonly AsyncCommand cmd; + private readonly int activityId; + private readonly string activity; + private readonly string inProgressMessage; + private readonly string completeMessage; + private readonly int totalUnitsExpected; + + /// + /// Initializes a new instance of the class. + /// + /// Command that outputs the messages. + /// The activity id of the progress bar. + /// The activity. + /// The message in the progress bar. + /// The activity complete message. + /// Total of units expected. + public ConfigurationSetProgressOutputBase(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + { + this.cmd = cmd; + this.activityId = activityId; + this.activity = activity; + this.inProgressMessage = inProgressMessage; + this.completeMessage = completeMessage; + this.totalUnitsExpected = totalUnitsExpected; + + // Write initial progress record. + // For some reason, if this is 0 the progress bar is shown full. Start with 1% + this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); + } + + /// + /// Gets or sets a hash set with the completed units. + /// + protected HashSet UnitsCompleted { get; set; } = new (); + + /// + /// Progress callback. + /// + /// Async operation in progress. + /// Change data. + public abstract void Progress(IAsyncOperationWithProgress operation, TProgressData data); + + /// + /// Handle progress. + /// + /// Set result. + public abstract void HandleProgress(TOperationResult result); + + /// + /// Completes the progress bar. + /// + public void CompleteProgress() + { + this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); + } + + /// + /// Marks a unit as completed and increase progress. + /// + /// Unit. + protected void CompleteUnit(ConfigurationUnit unit) + { + if (this.UnitsCompleted.Add(unit.InstanceIdentifier)) + { + this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.UnitsCompleted.Count}/{this.totalUnitsExpected}", this.UnitsCompleted.Count, this.totalUnitsExpected); + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs index 392b9a035b..ae0bc37910 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/GetConfigurationSetDetailsProgressOutput.cs @@ -6,26 +6,15 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { - using System; - using System.Collections.Generic; using Microsoft.Management.Configuration; using Microsoft.WinGet.Configuration.Engine.Commands; - using Microsoft.WinGet.Configuration.Engine.Exceptions; using Windows.Foundation; - using static Microsoft.WinGet.Configuration.Engine.Commands.AsyncCommand; /// /// Helper to handle progress callback from GetSetDetailsAsync. /// - internal class GetConfigurationSetDetailsProgressOutput + internal class GetConfigurationSetDetailsProgressOutput : ConfigurationSetProgressOutputBase { - private readonly AsyncCommand cmd; - private readonly int activityId; - private readonly string activity; - private readonly string inProgressMessage; - private readonly string completeMessage; - private readonly int totalUnitsExpected; - /// /// Initializes a new instance of the class. /// @@ -36,67 +25,30 @@ internal class GetConfigurationSetDetailsProgressOutput /// The activity complete message. /// Total of units expected. public GetConfigurationSetDetailsProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) { - this.cmd = cmd; - this.activityId = activityId; - this.activity = activity; - this.inProgressMessage = inProgressMessage; - this.completeMessage = completeMessage; - this.totalUnitsExpected = totalUnitsExpected; - - // Write initial progress record. - // For some reason, if this is 0 the progress bar is shown full. Start with 1% - this.cmd.WriteProgressWithPercentage(activityId, activity, $"{this.inProgressMessage} 0/{this.totalUnitsExpected}", 1, 100); - } - - /// - /// Gets the number of units shown. - /// - internal int UnitsShown { get; private set; } = 0; - - /// - /// Progress callback. - /// - /// Async operation in progress. - /// Result. - public void Progress(IAsyncOperationWithProgress operation, GetConfigurationUnitDetailsResult result) - { - this.HandleUnits(operation.GetResults().UnitResults); } /// - /// Handle units. + /// Gets the units shown. /// - /// The unit results. - public void HandleUnits(IReadOnlyList unitResults) + public int UnitsShown { - while (this.UnitsShown < unitResults.Count) - { - GetConfigurationUnitDetailsResult unitResult = unitResults[this.UnitsShown]; - this.LogFailedGetConfigurationUnitDetails(unitResult.Unit, unitResult.ResultInformation); - ++this.UnitsShown; - this.cmd.WriteProgressWithPercentage(this.activityId, this.activity, $"{this.inProgressMessage} {this.UnitsShown}/{this.totalUnitsExpected}", this.UnitsShown, this.totalUnitsExpected); - } + get { return this.UnitsCompleted.Count; } } - /// - /// Complete progress. - /// - public void CompleteProgress() + /// + public override void Progress(IAsyncOperationWithProgress operation, GetConfigurationUnitDetailsResult result) { - this.cmd.CompleteProgress(this.activityId, this.activity, this.completeMessage); + this.HandleProgress(operation.GetResults()); } - private void LogFailedGetConfigurationUnitDetails(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInformation) + /// + public override void HandleProgress(GetConfigurationSetDetailsResult result) { - if (resultInformation.ResultCode != null) + foreach (var unitResult in result.UnitResults) { - string errorMessage = $"Failed to get unit details for {unit.Type} 0x{resultInformation.ResultCode.HResult:X}" + - $"{Environment.NewLine}Description: '{resultInformation.Description}'{Environment.NewLine}Details: '{resultInformation.Details}'"; - this.cmd.WriteError( - ErrorRecordErrorId.ConfigurationDetailsError, - errorMessage, - resultInformation.ResultCode); + this.CompleteUnit(unitResult.Unit); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs index 0590e3e1ab..e7080b0bd8 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs @@ -77,6 +77,10 @@ private string VerifyFile(string filePath, PSCmdlet psCmdlet) psCmdlet.SessionState.Path.CurrentFileSystemLocation.Path, filePath)); } + else + { + filePath = Path.GetFullPath(filePath); + } if (!File.Exists(filePath)) { @@ -129,7 +133,7 @@ private void InitializeModulePath(string modulePath) throw new ArgumentException(Resources.ConfigurationModulePathArgError); } - this.CustomLocation = customLocation; + this.CustomLocation = Path.GetFullPath(customLocation); this.Location = PowerShellConfigurationProcessorLocation.Custom; } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs new file mode 100644 index 0000000000..b00a392d7c --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/TestConfigurationSetProgressOutput.cs @@ -0,0 +1,58 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.Helpers +{ + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Windows.Foundation; + + /// + /// Helper to handle progress callbacks for TestSetAsync. + /// + internal class TestConfigurationSetProgressOutput : ConfigurationSetProgressOutputBase + { + private bool isFirstProgress = true; + + /// + /// Initializes a new instance of the class. + /// + /// Command that outputs the messages. + /// The activity id of the progress bar. + /// The activity. + /// The message in the progress bar. + /// The activity complete message. + /// Total of units expected. + public TestConfigurationSetProgressOutput(AsyncCommand cmd, int activityId, string activity, string inProgressMessage, string completeMessage, int totalUnitsExpected) + : base(cmd, activityId, activity, inProgressMessage, completeMessage, totalUnitsExpected) + { + } + + /// + public override void Progress(IAsyncOperationWithProgress operation, TestConfigurationUnitResult data) + { + if (this.isFirstProgress) + { + this.HandleProgress(operation.GetResults()); + } + + this.CompleteUnit(data.Unit); + } + + /// + public override void HandleProgress(TestConfigurationSetResult result) + { + if (!this.isFirstProgress) + { + this.isFirstProgress = false; + foreach (var unitResult in result.UnitResults) + { + this.CompleteUnit(unitResult.Unit); + } + } + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs index 1121e4b154..5692818bdc 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/Utilities.cs @@ -7,11 +7,12 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers { using System; - using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; using System.Security.Principal; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.PSObjects; /// /// Helper methods. @@ -82,5 +83,41 @@ public static string[] SplitIntoLines(string message, int maxLines) return lines; } + + /// + /// Converts ConfigurationTestResult string value to PSConfigurationTestResult. + /// + /// ConfigurationTestResult value. + /// PSConfigurationTestResult. + public static PSConfigurationTestResult ToPSConfigurationTestResult(ConfigurationTestResult value) + { + return value switch + { + ConfigurationTestResult.Unknown => PSConfigurationTestResult.Unknown, + ConfigurationTestResult.Positive => PSConfigurationTestResult.Positive, + ConfigurationTestResult.Negative => PSConfigurationTestResult.Negative, + ConfigurationTestResult.Failed => PSConfigurationTestResult.Failed, + ConfigurationTestResult.NotRun => PSConfigurationTestResult.NotRun, + _ => throw new InvalidOperationException(), + }; + } + + /// + /// Converts ConfigurationUnitState string value to PSConfigurationUnitState. + /// + /// ConfigurationUnitState value. + /// PSConfigurationUnitState. + public static PSConfigurationUnitState ToPSConfigurationUnitState(ConfigurationUnitState value) + { + return value switch + { + ConfigurationUnitState.Unknown => PSConfigurationUnitState.Unknown, + ConfigurationUnitState.Pending => PSConfigurationUnitState.Pending, + ConfigurationUnitState.InProgress => PSConfigurationUnitState.InProgress, + ConfigurationUnitState.Completed => PSConfigurationUnitState.Completed, + ConfigurationUnitState.Skipped => PSConfigurationUnitState.Skipped, + _ => throw new InvalidOperationException(), + }; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationSetResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationSetResult.cs new file mode 100644 index 0000000000..bc3170d864 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationSetResult.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + + /// + /// Wrapper for ApplyConfigurationSetResult. + /// + public class PSApplyConfigurationSetResult + { + /// + /// Initializes a new instance of the class. + /// + /// Apply set result. + internal PSApplyConfigurationSetResult(ApplyConfigurationSetResult applySetResult) + { + this.ResultCode = applySetResult.ResultCode?.HResult ?? ErrorCodes.S_OK; + + var unitResults = new List(); + foreach (var unitResult in applySetResult.UnitResults) + { + unitResults.Add(new PSApplyConfigurationUnitResult(unitResult)); + } + + this.UnitResults = unitResults; + } + + /// + /// Gets the result code. + /// + public int ResultCode { get; private init; } + + /// + /// Gets the results of the units. + /// + public IReadOnlyList UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationUnitResult.cs new file mode 100644 index 0000000000..3b7f161c27 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSApplyConfigurationUnitResult.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Helpers; + + /// + /// The apply result of a configuration unit. + /// + public class PSApplyConfigurationUnitResult : PSUnitResult + { + /// + /// Initializes a new instance of the class. + /// + /// Apply unit result. + internal PSApplyConfigurationUnitResult(ApplyConfigurationUnitResult unitResult) + : base(unitResult.Unit, unitResult.ResultInformation) + { + this.State = Utilities.ToPSConfigurationUnitState(unitResult.State); + this.PreviouslyInDesiredState = unitResult.PreviouslyInDesiredState; + } + + /// + /// Gets the unit state. + /// + public PSConfigurationUnitState State { get; private init; } + + /// + /// Gets a value indicating whether the unit was in a previous desired state. + /// + public bool PreviouslyInDesiredState { get; private init; } + + /// + /// Gets a value indicating whether a reboot is required after the configuration unit was applied. + /// + public bool RebootRequired { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs index 8b7bbb3bf5..0b8fb21b9d 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationJob.cs @@ -18,33 +18,24 @@ public class PSConfigurationJob /// /// Initializes a new instance of the class. /// - /// The configuration task. + /// The apply configuration task. /// The start command. internal PSConfigurationJob( - Task configTask, + Task applyConfigTask, AsyncCommand startCommand) { - this.ConfigurationTask = configTask; + this.ApplyConfigurationTask = applyConfigTask; this.StartCommand = startCommand; } /// /// Gets the running configuration task. /// - internal Task ConfigurationTask { get; private set; } + internal Task ApplyConfigurationTask { get; private set; } /// /// Gets the command that started async operation. /// internal AsyncCommand StartCommand { get; private set; } - - /// - /// Gets the status of the configuration task. - /// - /// The task status. - public string GetStatus() - { - return this.ConfigurationTask.Status.ToString(); - } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs index c16c7e0825..4cb2ebc884 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs @@ -62,27 +62,34 @@ public string Source } /// - /// Gets the state. + /// Gets the schema version. /// - public string State + public string SchemaVersion { get { - return this.Set.State.ToString(); + return this.Set.SchemaVersion; } } /// - /// Gets the schema version. + /// Gets the state. + /// TODO: enable once implemented. /// - public string SchemaVersion + internal string State { get { - return this.Set.SchemaVersion; + return this.Set.State.ToString(); } } + /// + /// Gets or sets a value indicating whether apply ran. + /// TODO: remove once State is implemented. + /// + internal bool ApplyCompleted { get; set; } + /// /// Gets the PSConfigurationProcessor. /// diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationTestResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationTestResult.cs new file mode 100644 index 0000000000..75bca6afe9 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationTestResult.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + /// + /// Must match ConfigurationTestResult. + /// + public enum PSConfigurationTestResult + { + /// + /// The result is unknown. + /// + Unknown, + + /// + /// The system is in the state described by the configuration. + /// + Positive, + + /// + /// The system is not in the state described by the configuration. + /// + Negative, + + /// + /// Running the test failed. + /// + Failed, + + /// + /// The test was not run because it was not applicable. + /// + NotRun, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationUnitState.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationUnitState.cs new file mode 100644 index 0000000000..b5b60366ec --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationUnitState.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + /// + /// Must match ConfigurationUnitState. + /// + public enum PSConfigurationUnitState + { + /// + /// The state of the configuration unit is unknown. + /// + Unknown, + + /// + /// The configuration unit is in the queue to be applied. + /// + Pending, + + /// + /// The configuration unit is actively being applied. + /// + InProgress, + + /// + /// The configuration unit has completed being applied. + /// + Completed, + + /// + /// The configuration unit was not applied due to external factors. + /// + Skipped, + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSGetConfigurationDetailsResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSGetConfigurationDetailsResult.cs new file mode 100644 index 0000000000..3fe2902c68 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSGetConfigurationDetailsResult.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + + /// + /// Result for getting the details of a unit. + /// + public class PSGetConfigurationDetailsResult + { + /// + /// Initializes a new instance of the class. + /// + /// Get unit details result. + internal PSGetConfigurationDetailsResult(GetConfigurationUnitDetailsResult result) + { + this.Type = result.Unit.Type; + this.ResultCode = result.ResultInformation?.ResultCode?.HResult ?? ErrorCodes.S_OK; + + if (result.ResultInformation?.ResultCode != null) + { + this.ErrorMessage = $"Failed to get unit details for {this.Type} 0x{this.ResultCode:X}" + + $"{Environment.NewLine}Description: '{result.ResultInformation.Description}'{Environment.NewLine}Details: '{result.ResultInformation.Details}'"; + } + } + + /// + /// Gets the unit type. + /// + public string Type { get; private init; } + + /// + /// Gets the result code. + /// + public int ResultCode { get; private init; } + + /// + /// Gets the error message. + /// + public string? ErrorMessage { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationSetResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationSetResult.cs new file mode 100644 index 0000000000..a0e3c47202 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationSetResult.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Helpers; + + /// + /// Wrapper for TestConfigurationSetResult. + /// + public class PSTestConfigurationSetResult + { + /// + /// Initializes a new instance of the class. + /// + /// Test set result. + internal PSTestConfigurationSetResult(TestConfigurationSetResult testSetResult) + { + this.TestResult = Utilities.ToPSConfigurationTestResult(testSetResult.TestResult); + + var unitResults = new List(); + foreach (var unitResult in testSetResult.UnitResults) + { + unitResults.Add(new PSTestConfigurationUnitResult(unitResult)); + } + + this.UnitResults = unitResults; + } + + /// + /// Gets the test result. + /// + public PSConfigurationTestResult TestResult { get; private init; } + + /// + /// Gets the results of the units. + /// + public IReadOnlyList UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationUnitResult.cs new file mode 100644 index 0000000000..0e35a1196a --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSTestConfigurationUnitResult.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Helpers; + + /// + /// Wrapper for TestConfigurationUnitResult. + /// + public class PSTestConfigurationUnitResult : PSUnitResult + { + /// + /// Initializes a new instance of the class. + /// + /// Test unit result. + internal PSTestConfigurationUnitResult(TestConfigurationUnitResult testUnitResult) + : base(testUnitResult.Unit, testUnitResult.ResultInformation) + { + this.TestResult = Utilities.ToPSConfigurationTestResult(testUnitResult.TestResult); + } + + /// + /// Gets the test result. + /// + public PSConfigurationTestResult TestResult { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs new file mode 100644 index 0000000000..ec22ebc304 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSUnitResult.cs @@ -0,0 +1,128 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + using Microsoft.WinGet.Configuration.Engine.Resources; + + /// + /// Unit result. + /// + public abstract class PSUnitResult + { + /// + /// Initializes a new instance of the class. + /// + /// Unit. + /// Result info. + internal PSUnitResult(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInfo) + { + this.Type = unit.Type; + this.ResultCode = resultInfo.ResultCode?.HResult ?? ErrorCodes.S_OK; + + if (this.ResultCode != ErrorCodes.S_OK) + { + this.Message = this.GetUnitMessage(unit, resultInfo); + this.Description = resultInfo.Description.Trim(); + this.Details = resultInfo.Details; + } + } + + /// + /// Gets the unit type. + /// + public string Type { get; private init; } + + /// + /// Gets the result code. + /// + public int ResultCode { get; private init; } + + /// + /// Gets the message. + /// + public string? Message { get; private init; } + + /// + /// Gets the short description. + /// + public string? Description { get; private init; } + + /// + /// Gets detailed information. + /// + public string? Details { get; private init; } + + private string GetUnitMessage(ConfigurationUnit unit, IConfigurationUnitResultInformation resultInfo) + { + if (resultInfo.ResultCode == null) + { + if (unit.State == ConfigurationUnitState.Skipped) + { + return string.Format(Resources.ConfigurationUnitSkipped, "null"); + } + + return string.Format(Resources.ConfigurationUnitFailed, "null"); + } + + int resultCode = resultInfo.ResultCode.HResult; + switch (resultCode) + { + case ErrorCodes.WingetConfigErrorDuplicateIdentifier: + return string.Format(Resources.ConfigurationUnitHasDuplicateIdentifier, unit.Identifier); + case ErrorCodes.WingetConfigErrorMissingDependency: + return string.Format(Resources.ConfigurationUnitHasMissingDependency, resultInfo.Details); + case ErrorCodes.WingetConfigErrorAssertionFailed: + return Resources.ConfigurationUnitAssertHadNegativeResult; + case ErrorCodes.WinGetConfigUnitNotFound: + return Resources.ConfigurationUnitNotFoundInModule; + case ErrorCodes.WinGetConfigUnitNotFoundRepository: + return Resources.ConfigurationUnitNotFound; + case ErrorCodes.WinGetConfigUnitMultipleMatches: + return Resources.ConfigurationUnitMultipleMatches; + case ErrorCodes.WinGetConfigUnitInvokeGet: + return Resources.ConfigurationUnitFailedDuringGet; + case ErrorCodes.WinGetConfigUnitInvokeTest: + return Resources.ConfigurationUnitFailedDuringTest; + case ErrorCodes.WinGetConfigUnitInvokeSet: + return Resources.ConfigurationUnitFailedDuringSet; + case ErrorCodes.WinGetConfigUnitModuleConflict: + return Resources.ConfigurationUnitModuleConflict; + case ErrorCodes.WinGetConfigUnitImportModule: + return Resources.ConfigurationUnitModuleImportFailed; + case ErrorCodes.WinGetConfigUnitInvokeInvalidResult: + return Resources.ConfigurationUnitReturnedInvalidResult; + case ErrorCodes.WingetConfigErrorManuallySkipped: + return Resources.ConfigurationUnitManuallySkipped; + case ErrorCodes.WingetConfigErrorDependencyUnsatisfied: + return Resources.ConfigurationUnitNotRunDueToDependency; + } + + switch (resultInfo.ResultSource) + { + case ConfigurationUnitResultSource.ConfigurationSet: + return string.Format(Resources.ConfigurationUnitFailedConfigSet, resultCode); + case ConfigurationUnitResultSource.Internal: + return string.Format(Resources.ConfigurationUnitFailedInternal, resultCode); + case ConfigurationUnitResultSource.Precondition: + return string.Format(Resources.ConfigurationUnitFailedPrecondition, resultCode); + case ConfigurationUnitResultSource.SystemState: + return string.Format(Resources.ConfigurationUnitFailedSystemState, resultCode); + case ConfigurationUnitResultSource.UnitProcessing: + return string.Format(Resources.ConfigurationUnitFailedUnitProcessing, resultCode); + } + + if (unit.State == ConfigurationUnitState.Skipped) + { + return string.Format(Resources.ConfigurationUnitSkipped, resultCode); + } + + return string.Format(Resources.ConfigurationUnitFailed, resultCode); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationSetResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationSetResult.cs new file mode 100644 index 0000000000..d695f403e6 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationSetResult.cs @@ -0,0 +1,45 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.WinGet.Configuration.Engine.Exceptions; + + /// + /// Wrapper for ApplyConfigurationSetResult for validate. + /// + public class PSValidateConfigurationSetResult + { + /// + /// Initializes a new instance of the class. + /// + /// Apply set result. + internal PSValidateConfigurationSetResult(ApplyConfigurationSetResult applySetResult) + { + this.ResultCode = applySetResult.ResultCode?.HResult ?? ErrorCodes.S_OK; + + var unitResults = new List(); + foreach (var unitResult in applySetResult.UnitResults) + { + unitResults.Add(new PSValidateConfigurationUnitResult(unitResult)); + } + + this.UnitResults = unitResults; + } + + /// + /// Gets the result code. + /// + public int ResultCode { get; private init; } + + /// + /// Gets the results of the units. + /// + public IReadOnlyList UnitResults { get; private init; } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationUnitResult.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationUnitResult.cs new file mode 100644 index 0000000000..47452be064 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSValidateConfigurationUnitResult.cs @@ -0,0 +1,25 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Engine.PSObjects +{ + using Microsoft.Management.Configuration; + + /// + /// The validate result of a configuration unit. + /// + public class PSValidateConfigurationUnitResult : PSUnitResult + { + /// + /// Initializes a new instance of the class. + /// + /// Apply unit result. + internal PSValidateConfigurationUnitResult(ApplyConfigurationUnitResult unitResult) + : base(unitResult.Unit, unitResult.ResultInformation) + { + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs index d251bf6093..804e43e4ec 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs @@ -504,9 +504,18 @@ internal static string ConfigurationWarning { /// /// Looks up a localized string similar to Have you reviewed the configuration and would you like to proceed applying it to the system?. /// - internal static string ConfigurationWarningPrompt { + internal static string ConfigurationWarningPromptApply { get { - return ResourceManager.GetString("ConfigurationWarningPrompt", resourceCulture); + return ResourceManager.GetString("ConfigurationWarningPromptApply", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Have you reviewed the configuration and would you like to proceed verifying it against the system?. + /// + internal static string ConfigurationWarningPromptTest { + get { + return ResourceManager.GetString("ConfigurationWarningPromptTest", resourceCulture); } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx index 2f9c0269af..ba6966f185 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx @@ -182,7 +182,7 @@ You are responsible for understanding the configuration settings you are choosing to execute. Microsoft is not responsible for the configuration file you have authored or imported. This configuration may change settings in Windows, install software, change software settings (including security settings), and accept user agreements to third-party packages and services on your behalf.  By running this configuration file, you acknowledge that you understand and agree to these resources and settings. Any applications installed are licensed to you by their owners. Microsoft is not responsible for, nor does it grant any licenses to, third-party packages or services. Legal approved. Do not change without approval. - + Have you reviewed the configuration and would you like to proceed applying it to the system? PM approved. @@ -306,4 +306,7 @@ `-ModulePath` value must be `CurrentUser`, `AllUsers`, `Default` or an absolute path. {Locked="{-ModulePath}, {CurrentUser}, {AllUsers}, {Default}} + + Have you reviewed the configuration and would you like to proceed verifying it against the system? + \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 index cb9328ec82..f819fc7d70 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 +++ b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 @@ -20,6 +20,9 @@ CmdletsToExport = @( "Get-WinGetConfigurationDetails" "Invoke-WinGetConfiguration" "Start-WinGetConfiguration" + "Test-WinGetConfiguration" + "Confirm-WinGetConfiguration" + "Stop-WinGetConfiguration" ) PrivateData = @{ diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 index 8212d2c60d..5f5e6be27c 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 @@ -5,12 +5,15 @@ .Synopsis Pester tests related to the Microsoft.WinGet.Configuration PowerShell module. 'Invoke-Pester' should be called in an admin PowerShell window. + Requires local test repo to be setup. #> BeforeAll { + $env:POWERSHELL_TELEMETRY_OPTOUT = "true" $deviceGroupPolicyRoot = "HKLM:\Software\Policies\Microsoft\Windows" $wingetPolicyKeyName = "AppInstaller" $wingetGroupPolicyRegistryRoot = $deviceGroupPolicyRoot + "\" + $wingetPolicyKeyName + $e2eTestModule = "xE2ETestResource" Import-Module Microsoft.WinGet.Configuration @@ -43,6 +46,160 @@ BeforeAll { Remove-ItemProperty -Path $wingetGroupPolicyRegistryRoot -Name * } } + + function GetConfigTestDataPath() + { + return Join-Path $PSScriptRoot "..\..\AppInstallerCLIE2ETests\TestData\Configuration\" + } + + function DeleteConfigTxtFiles() + { + Get-ChildItem $(GetConfigTestDataPath) -Filter *.txt -Recurse | ForEach-Object { Remove-Item $_ } + } + + function GetConfigTestDataFile([string] $fileName) + { + $path = Join-Path $(GetConfigTestDataPath) $fileName + + if (-not (Test-Path $path)) + { + throw "$path does not exists" + } + + return $path + } + + enum TestModuleLocation + { + CurrentUser + AllUsers + Custom + DefaultLocation + } + + function GetExpectedModulePath([TestModuleLocation]$testModuleLocation) + { + switch ($testModuleLocation) + { + ([TestModuleLocation]::CurrentUser) + { + $path = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments) + return Join-Path $path "PowerShell\Modules" + } + ([TestModuleLocation]::AllUsers) + { + $path = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles) + return Join-Path $path "PowerShell\Modules" + } + ([TestModuleLocation]::DefaultLocation) + { + $path = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) + return Join-Path $path "Microsoft\WinGet\Configuration\Modules" + } + ([TestModuleLocation]::Custom) + { + return Join-Path $env:TEMP "E2EPesterCustomModules" + } + default + { + throw $testModuleLocation + } + } + } + + function CleanupPsModulePath() + { + $wingetPath = GetExpectedModulePath DefaultLocation + $customPath = GetExpectedModulePath Custom + $modulePath = $env:PsModulePath + $newModulePath = ($modulePath.Split(';') | Where-Object { $_ -ne $wingetPath } | Where-Object { $_ -ne $customPath }) -join ';' + $env:PsModulePath = $newModulePath + } + + function EnsureModuleState([string]$moduleName, [bool]$present, [string]$repository = $null, [TestModuleLocation]$testModuleLocation = [TestModuleLocation]::CurrentUser) + { + CleanupPsModulePath + $wingetPath = GetExpectedModulePath DefaultLocation + $customPath = GetExpectedModulePath Custom + $env:PsModulePath += ";$wingetPath;$customPath" + + $availableModules = Get-Module $moduleName -ListAvailable + $isPresent = $null -ne $availableModules + + if ($isPresent) + { + foreach ($module in $availableModules) + { + try + { + $item = Get-Item $module.Path -ErrorAction Stop + while ($item.Name -ne $moduleName) + { + $item = Get-Item $item.PSParentPath -ErrorAction Stop + } + + if (-not $present) + { + Get-ChildItem $item.FullName -Recurse | Remove-Item -Force -Recurse -ErrorAction Stop + Remove-Item $item -ErrorAction Stop + } + else + { + # Must be in the right location + $expected = GetExpectedModulePath $testModuleLocation + if ($expected -ne $item.Parent.FullName) + { + Get-ChildItem $item.FullName -Recurse | Remove-Item -Force -Recurse -ErrorAction Stop + Remove-Item $item -ErrorAction Stop + $isPresent = $false + } + } + } + catch [System.Management.Automation.ItemNotFoundException] + { + Write-Host "Item not found, ignoring..." $_.Exception.Message + } + } + } + + if ((-not $isPresent) -and $present) + { + $params = @{ + Name = $moduleName + Force = $true + } + + if (-not [string]::IsNullOrEmpty($repository)) + { + $params.Add('Repository', $repository) + } + + if (($testModuleLocation -eq [TestModuleLocation]::CurrentUser) -or + ($testModuleLocation -eq [TestModuleLocation]::AllUsers)) + { + if ($testModuleLocation -eq [TestModuleLocation]::AllUsers) + { + $params.Add('Scope', 'AllUsers') + } + + Install-Module @params + } + else + { + $path = $customPath + if (($testModuleLocation -eq [TestModuleLocation]::WinGetModulePath) -or + ($testModuleLocation -eq [TestModuleLocation]::DefaultLocation)) + { + $path = $wingetPath + } + $params.Add('Path', $path) + + Save-Module @params + } + } + + CleanupPsModulePath + } } Describe 'Test-GroupPolicies' { @@ -103,7 +260,617 @@ Describe 'Test-GroupPolicies' { } } +Describe 'Get configuration' { + + It 'Get configuration and details' { + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $set = Get-WinGetConfigurationDetails -Set $set + $set | Should -Not -BeNullOrEmpty + } + + It 'Get details piped' { + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile | Get-WinGetConfigurationDetails + $set | Should -Not -BeNullOrEmpty + } + + It 'Get configuration and details positional' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $set = Get-WinGetConfigurationDetails $set + $set | Should -Not -BeNullOrEmpty + } + + It 'File doesnt exit' { + $testFile = "c:\dir\fakeFile.txt" + { Get-WinGetConfiguration -File $testFile } | Should -Throw $testFile + } + + It 'Invalid file' { + $testFile = GetConfigTestDataFile "Empty.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C002*" + } + + It 'Missing property' { + $testFile = GetConfigTestDataFile "NotConfig.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00E*properties*missing*" + } + + It 'Missing configurationVersion' { + $testFile = GetConfigTestDataFile "NoVersion.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00E*configurationVersion*missing*" + } + + It 'Unknown version' { + $testFile = GetConfigTestDataFile "UnknownVersion.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C004*Configuration file version*is not known.*" + } + + It 'Resource wrong type' { + $testFile = GetConfigTestDataFile "ResourcesNotASequence.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C003*resources*wrong type*" + } + + It 'Unit wrong type' { + $testFile = GetConfigTestDataFile "UnitNotAMap.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C003*resources*0*wrong type*" + } + + It 'No resource name' { + $testFile = GetConfigTestDataFile "NoResourceName.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00D*resource*invalid value*Module/*" + } + + It 'Module mismatch' { + $testFile = GetConfigTestDataFile "ModuleMismatch.yml" + { Get-WinGetConfiguration -File $testFile } | Should -Throw "*0x8A15C00D*invalid value*DifferentModule*" + } +} + +Describe 'Invoke-WinGetConfiguration' { + + BeforeEach { + DeleteConfigTxtFiles + } + + It 'From Gallery' { + EnsureModuleState "XmlContentDsc" $false + + $testFile = GetConfigTestDataFile "PSGallery_NoModule_NoSettings.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + } + + It 'From TestRepo' { + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + + $expectedModule = Join-Path $(GetExpectedModulePath DefaultLocation) $e2eTestModule + Test-Path $expectedModule | Should -Be $true + } + + It 'From TestRepo Location' -ForEach @( + @{ Location = "CurrentUser"; } + @{ Location = "AllUsers"; } + @{ Location = "DefaultLocation"; } + @{ Location = "Custom"; }) { + $modulePath = "'" + switch ($location) + { + ([TestModuleLocation]::CurrentUser) + { + $modulePath = "currentuser" + break + } + ([TestModuleLocation]::AllUsers) + { + $modulePath = "allusers" + break + } + ([TestModuleLocation]::DefaultLocation) + { + $modulePath = "default" + break + } + ([TestModuleLocation]::Custom) + { + $modulePath = GetExpectedModulePath Custom + break + } + default { + throw $location + } + } + + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo_Location.yml" + $set = Get-WinGetConfiguration -File $testFile -ModulePath $modulePath + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedModule = Join-Path $(GetExpectedModulePath $location) $e2eTestModule + Test-Path $expectedModule | Should -Be $true + } + + It 'Piped' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $result = Get-WinGetConfiguration -File $testFile | Invoke-WinGetConfiguration -AcceptConfigurationAgreements + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Positional' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Independent Resource - One Failure' { + $testFile = GetConfigTestDataFile "IndependentResources_OneFailure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].State | Should -Be "Completed" + $result.UnitResults[1].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "IndependentResources_OneFailure.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Dependent Resource - Failure' { + $testFile = GetConfigTestDataFile "DependentResources_Failure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].State | Should -Be "Skipped" + $result.UnitResults[1].ResultCode | Should -Be -1978286072 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "DependentResources_Failure.txt" + Test-Path $expectedFile | Should -Be $false + } + + It 'ResourceCaseInsensitive' { + $testFile = GetConfigTestDataFile "ResourceCaseInsensitive.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "ResourceCaseInsensitive.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } +} + +Describe 'Start|Complete-WinGetConfiguration' { + + BeforeEach { + DeleteConfigTxtFiles + } + + It 'From Gallery' { + EnsureModuleState "XmlContentDsc" $false + + $testFile = GetConfigTestDataFile "PSGallery_NoModule_NoSettings.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + } + + It 'From TestRepo' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + + # Verify can't be used after. + { Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set } | Should -Throw "Operation is not valid due to the current state of the object." + } + + It 'From TestRepo Location' -ForEach @( + @{ Location = "CurrentUser"; } + @{ Location = "AllUsers"; } + @{ Location = "DefaultLocation"; } + @{ Location = "Custom"; }) { + $modulePath = "'" + switch ($location) + { + ([TestModuleLocation]::CurrentUser) + { + $modulePath = "currentuser" + break + } + ([TestModuleLocation]::AllUsers) + { + $modulePath = "allusers" + break + } + ([TestModuleLocation]::DefaultLocation) + { + $modulePath = "default" + break + } + ([TestModuleLocation]::Custom) + { + $modulePath = GetExpectedModulePath Custom + break + } + default { + throw $location + } + } + + EnsureModuleState $e2eTestModule $false + + $testFile = GetConfigTestDataFile "Configure_TestRepo_Location.yml" + $set = Get-WinGetConfiguration -File $testFile -ModulePath $modulePath + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedModule = Join-Path $(GetExpectedModulePath $location) $e2eTestModule + Test-Path $expectedModule | Should -Be $true + } + + It 'Piped' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $result = Get-WinGetConfiguration -File $testFile | Start-WinGetConfiguration -AcceptConfigurationAgreements | Complete-WinGetConfiguration + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Positional' { + DeleteConfigTxtFiles + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Independent Resource - One Failure' { + $testFile = GetConfigTestDataFile "IndependentResources_OneFailure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + + $result.UnitResults[1].State | Should -Be "Completed" + $result.UnitResults[1].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "IndependentResources_OneFailure.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } + + It 'Dependent Resource - Failure' { + $testFile = GetConfigTestDataFile "DependentResources_Failure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286075 + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].State | Should -Be "Skipped" + $result.UnitResults[1].ResultCode | Should -Be -1978286072 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "DependentResources_Failure.txt" + Test-Path $expectedFile | Should -Be $false + } + + It 'ResourceCaseInsensitive' { + $testFile = GetConfigTestDataFile "ResourceCaseInsensitive.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $job = Start-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $job | Should -Not -BeNullOrEmpty + + $result = Complete-WinGetConfiguration -ConfigurationJob $job + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $expectedFile = Join-Path $(GetConfigTestDataPath) "ResourceCaseInsensitive.txt" + Test-Path $expectedFile | Should -Be $true + Get-Content $expectedFile -Raw | Should -Be "Contents!" + } +} + +Describe 'Test-WinGetConfiguration' { + + BeforeEach { + DeleteConfigTxtFiles + } + + It 'Negative' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Negative" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Negative" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It 'Positive' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $expectedFile = Join-Path $(GetConfigTestDataPath) "Configure_TestRepo.txt" + Set-Content -Path $expectedFile -Value "Contents!" -NoNewline + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Positive" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Positive" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It 'Piped' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $result = Get-WinGetConfiguration -File $testFile | Test-WinGetConfiguration -AcceptConfigurationAgreements + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Negative" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Negative" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It 'Positional' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Negative" + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].TestResult | Should -Be "Negative" + $result.UnitResults[0].ResultCode | Should -Be 0 + } + + It "Failed" { + $testFile = GetConfigTestDataFile "IndependentResources_OneFailure.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Test-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.TestResult | Should -Be "Failed" + $result.UnitResults.Count | Should -Be 2 + $result.UnitResults[0].TestResult | Should -Be "Failed" + $result.UnitResults[0].ResultCode | Should -Be -1978285819 + $result.UnitResults[1].TestResult | Should -Be "Negative" + $result.UnitResults[1].ResultCode | Should -Be 0 + } +} + +Describe 'Confirm-WinGetConfiguration' { + + It 'Duplicate Identifiers' { + $testFile = GetConfigTestDataFile "DuplicateIdentifiers.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286074 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286074 + $result.UnitResults[1].ResultCode | Should -Be -1978286074 + $result.UnitResults[2].ResultCode | Should -Be 0 + } + + It 'Missing dependency' { + $testFile = GetConfigTestDataFile "MissingDependency.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286073 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be 0 + $result.UnitResults[1].ResultCode | Should -Be 0 + $result.UnitResults[2].ResultCode | Should -Be -1978286073 + } + + It 'Dependency cycle' { + $testFile = GetConfigTestDataFile "DependencyCycle.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286068 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286072 + $result.UnitResults[1].ResultCode | Should -Be -1978286072 + $result.UnitResults[2].ResultCode | Should -Be 0 + } + + It 'No issue' { + $testFile = GetConfigTestDataFile "PSGallery_NoSettings.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + } + + It 'Piped' { + $testFile = GetConfigTestDataFile "DuplicateIdentifiers.yml" + $result = Get-WinGetConfiguration -File $testFile | Confirm-WinGetConfiguration + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286074 + $result.UnitResults[1].ResultCode | Should -Be -1978286074 + $result.UnitResults[2].ResultCode | Should -Be 0 + } + + It 'Positional' { + $testFile = GetConfigTestDataFile "DuplicateIdentifiers.yml" + $set = Get-WinGetConfiguration $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Confirm-WinGetConfiguration $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be -1978286074 + $result.UnitResults.Count | Should -Be 3 + $result.UnitResults[0].ResultCode | Should -Be -1978286074 + $result.UnitResults[1].ResultCode | Should -Be -1978286074 + $result.UnitResults[2].ResultCode | Should -Be 0 + } +} + AfterAll { CleanupGroupPolicies CleanupGroupPolicyKeyIfExists + CleanupPsModulePath + DeleteConfigTxtFiles } \ No newline at end of file