winget-cli

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

commit 2f0ef1f20ba2d8524f80f006fcf600c156ececbc
parent 6f6610e8bd9cff40ccc8f198b1d0be218ead9f5f
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Tue,  9 Mar 2021 15:12:15 -0800

Support custom installer success exit codes (#778)


Diffstat:
Mschemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json | 2+-
Mschemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json | 2+-
Msrc/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp | 3++-
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 3+++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Asrc/AppInstallerCLITests/TestData/InstallFlowTest_NonZeroExitCode.yaml | 19+++++++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 24++++++++++++++++++++++++
Msrc/AppInstallerCLITests/YamlManifest.cpp | 2+-
Msrc/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp | 10+++++-----
Msrc/AppInstallerCommonCore/Public/winget/ManifestInstaller.h | 2+-
Msrc/AppInstallerTestExeInstaller/main.cpp | 17++++++++++++++---
11 files changed, 74 insertions(+), 13 deletions(-)

diff --git a/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.installer.1.0.0.json @@ -135,7 +135,7 @@ }, "maxItems": 16, "uniqueItems": true, - "description": "List of non-zero installer success exit codes" + "description": "List of additional non-zero installer success exit codes other than known default values by winget" }, "UpgradeBehavior": { "type": [ "string", "null" ], diff --git a/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json b/schemas/JSON/manifests/v1.0.0/manifest.singleton.1.0.0.json @@ -148,7 +148,7 @@ }, "maxItems": 16, "uniqueItems": true, - "description": "List of non-zero installer success exit codes" + "description": "List of additional non-zero installer success exit codes other than known default values by winget" }, "UpgradeBehavior": { "type": [ "string", "null" ], diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -193,6 +193,7 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; const std::string& installerArgs = context.Get<Execution::Data::InstallerArgs>(); + const auto& additionalSuccessCodes = context.Get<Execution::Data::Installer>()->InstallerSuccessCodes; auto installResult = context.Reporter.ExecuteWithProgress( std::bind(InvokeShellExecute, @@ -205,7 +206,7 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Warn() << "Installation abandoned" << std::endl; AICLI_TERMINATE_CONTEXT(E_ABORT); } - else if (installResult.value() != 0) + else if (installResult.value() != 0 && (std::find(additionalSuccessCodes.begin(), additionalSuccessCodes.end(), installResult.value()) == additionalSuccessCodes.end())) { const auto& manifest = context.Get<Execution::Data::Manifest>(); Logging::Telemetry().LogInstallerFailure(manifest.Id, manifest.Version, manifest.Channel, "ShellExecute", installResult.value()); diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -237,6 +237,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_NonZeroExitCode.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\ImportFile-Bad-Invalid.json"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -279,6 +279,9 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_NonZeroExitCode.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Msix_StreamingFlow.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_NonZeroExitCode.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_NonZeroExitCode.yaml @@ -0,0 +1,19 @@ +PackageIdentifier: AppInstallerCliTest.TestInstaller +PackageVersion: 1.0.0.0 +PackageName: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +InstallerSwitches: + Custom: /ExitCode 0x80070005 + SilentWithProgress: /silentwithprogress + Silent: /silence +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + InstallerSuccessCodes: + - -2147024891 +ManifestType: singleton +ManifestVersion: 1.0.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -445,6 +445,30 @@ TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow][workflow]") REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); } +TEST_CASE("InstallFlowNonZeroExitCode", "[InstallFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_NonZeroExitCode.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is called and parameters are passed in. + REQUIRE(context.GetTerminationHR() == S_OK); + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/ExitCode 0x80070005") != std::string::npos); + REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); +} + TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -366,7 +366,7 @@ void VerifyV1ManifestContent(const Manifest& manifest, bool isSingleton) REQUIRE(defaultSwitches.at(InstallerSwitchType::InstallLocation) == "/dir=<INSTALLPATH>"); REQUIRE(defaultSwitches.at(InstallerSwitchType::Update) == "/upgrade"); - REQUIRE(manifest.DefaultInstallerInfo.InstallerSuccessCodes == std::vector<int>{ 1, static_cast<int>(0x80070005) }); + REQUIRE(manifest.DefaultInstallerInfo.InstallerSuccessCodes == std::vector<DWORD>{ 1, static_cast<DWORD>(0x80070005) }); REQUIRE(manifest.DefaultInstallerInfo.UpdateBehavior == UpdateBehaviorEnum::UninstallPrevious); REQUIRE(manifest.DefaultInstallerInfo.Commands == MultiValue{ "makemsix", "makeappx" }); REQUIRE(manifest.DefaultInstallerInfo.Protocols == MultiValue{ "protocol1", "protocol2" }); diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -60,15 +60,15 @@ namespace AppInstaller::Manifest return result; } - std::vector<int> ProcessIntSequenceNode(const YAML::Node& node) + std::vector<DWORD> ProcessInstallerSuccessCodeSequenceNode(const YAML::Node& node) { THROW_HR_IF(E_INVALIDARG, !node.IsSequence()); - std::vector<int> result; + std::vector<DWORD> result; for (auto const& entry : node.Sequence()) { - result.emplace_back(entry.as<int>()); + result.emplace_back(static_cast<DWORD>(entry.as<int>())); } return result; @@ -214,7 +214,7 @@ namespace AppInstaller::Manifest // Starting v1, we should be only adding new fields for each minor version increase if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) { - // Root level and Localization node level + // Root level and Installer node level std::vector<FieldProcessInfo> v1CommonFields = { { "InstallerLocale", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Locale = value.as<std::string>(); return {}; } }, @@ -223,7 +223,7 @@ namespace AppInstaller::Manifest { "Scope", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Scope = ConvertToScopeEnum(value.as<std::string>()); return {}; } }, { "InstallModes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallModes = ProcessScopeSequenceNode(value); return {}; } }, { "InstallerSwitches", [this](const YAML::Node& value)->ValidationErrors { m_p_switches = &(m_p_installer->Switches); return ValidateAndProcessFields(value, SwitchesFieldInfos); } }, - { "InstallerSuccessCodes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerSuccessCodes = ProcessIntSequenceNode(value); return {}; } }, + { "InstallerSuccessCodes", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->InstallerSuccessCodes = ProcessInstallerSuccessCodeSequenceNode(value); return {}; } }, { "UpgradeBehavior", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->UpdateBehavior = ConvertToUpdateBehaviorEnum(value.as<std::string>()); return {}; } }, { "Commands", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Commands = ProcessStringSequenceNode(value); return {}; } }, { "Protocols", [this](const YAML::Node& value)->ValidationErrors { m_p_installer->Protocols = ProcessStringSequenceNode(value); return {}; } }, diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -49,7 +49,7 @@ namespace AppInstaller::Manifest // If present, has more precedence than root std::map<InstallerSwitchType, string_t> Switches; - std::vector<int> InstallerSuccessCodes; + std::vector<DWORD> InstallerSuccessCodes; UpdateBehaviorEnum UpdateBehavior = UpdateBehaviorEnum::Install; diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -131,6 +131,7 @@ int wmain(int argc, const wchar_t** argv) std::wstringstream outContent; std::wstring productCode; std::wstring version; + int exitCode = 0; // Output to cout by default, but swap to a file if requested std::wostream* out = &std::wcout; @@ -149,7 +150,17 @@ int wmain(int argc, const wchar_t** argv) outContent << argv[i] << ' '; } } - + + // Supports custom exit code + else if (_wcsicmp(argv[i], L"/ExitCode") == 0) + { + if (++i < argc) + { + exitCode = static_cast<int>(std::stoll(argv[i], 0, 0)); + outContent << argv[i] << ' '; + } + } + // Supports custom product code ID else if (_wcsicmp(argv[i], L"/ProductID") == 0) { @@ -195,6 +206,6 @@ int wmain(int argc, const wchar_t** argv) path uninstallerPath = GenerateUninstaller(*out, installDirectory, productCode); WriteToUninstallRegistry(*out, productCode, uninstallerPath, version); - - return 0; + + return exitCode; }