diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 17be096521..429fd27891 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -61,6 +61,8 @@ namespace AppInstaller::CLI return Argument{ "ignore-security-hash"_liv, NoAlias, Args::Type::HashOverride, Resource::String::HashOverrideArgumentDescription, ArgumentType::Flag, Settings::TogglePolicy::Policy::HashOverride }; case Args::Type::AcceptPackageAgreements: return Argument{ "accept-package-agreements"_liv, NoAlias, Args::Type::AcceptPackageAgreements, Resource::String::AcceptPackageAgreementsArgumentDescription, ArgumentType::Flag }; + case Args::Type::NoUpgrade: + return Argument{ "no-upgrade"_liv, NoAlias, Args::Type::NoUpgrade, Resource::String::NoUpgradeArgumentDescription, ArgumentType::Flag }; case Args::Type::HashFile: return Argument{ "file"_liv, 'f', Args::Type::HashFile, Resource::String::FileArgumentDescription, ArgumentType::Positional, true }; case Args::Type::Msix: diff --git a/src/AppInstallerCLICore/Commands/ImportCommand.cpp b/src/AppInstallerCLICore/Commands/ImportCommand.cpp index 6bc90abe4b..924554ecb7 100644 --- a/src/AppInstallerCLICore/Commands/ImportCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ImportCommand.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::CLI Argument{ "import-file", 'i', Execution::Args::Type::ImportFile, Resource::String::ImportFileArgumentDescription, ArgumentType::Positional, true }, Argument{ "ignore-unavailable", Argument::NoAlias, Execution::Args::Type::IgnoreUnavailable, Resource::String::ImportIgnoreUnavailableArgumentDescription, ArgumentType::Flag }, Argument{ "ignore-versions", Argument::NoAlias, Execution::Args::Type::IgnoreVersions, Resource::String::ImportIgnorePackageVersionsArgumentDescription, ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::NoUpgrade), Argument::ForType(Execution::Args::Type::AcceptPackageAgreements), Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp index 2c448bc64a..728b18ad8e 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -43,6 +43,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::DependencySource), Argument::ForType(Args::Type::AcceptPackageAgreements), + Argument::ForType(Args::Type::NoUpgrade), Argument::ForType(Args::Type::CustomHeader), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::Rename), diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 3f8498aa29..fd3d196b72 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -35,13 +35,14 @@ namespace AppInstaller::CLI::Execution Silent, Locale, Log, - Override, //Override args are (and the only args) directly passed to installer + Override, // Override args are (and the only args) directly passed to installer InstallLocation, InstallScope, InstallArchitecture, HashOverride, // Ignore hash mismatches AcceptPackageAgreements, // Accept all license agreements for packages Rename, // Renames the file of the executable. Only applies to the portable installerType + NoUpgrade, // Install flow should not try to convert to upgrade flow upon finding existing installed version // Uninstall behavior Purge, // Removes all files and directories related to a package during an uninstall. Only applies to the portable installerType. diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index aa3d830e95..cc4ca9ff1e 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -219,6 +219,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(NoPackagesFoundInImportFile); WINGET_DEFINE_RESOURCE_STRINGID(Notes); WINGET_DEFINE_RESOURCE_STRINGID(NoUninstallInfoFound); + WINGET_DEFINE_RESOURCE_STRINGID(NoUpgradeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OpenLogsArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatch); @@ -231,6 +232,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Package); WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsNotAgreedTo); WINGET_DEFINE_RESOURCE_STRINGID(PackageAgreementsPrompt); + WINGET_DEFINE_RESOURCE_STRINGID(PackageAlreadyInstalled); WINGET_DEFINE_RESOURCE_STRINGID(PackageDependencies); WINGET_DEFINE_RESOURCE_STRINGID(PendingWorkError); WINGET_DEFINE_RESOURCE_STRINGID(PoliciesDisabled); diff --git a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp index 80eab7e38d..9a001fc610 100644 --- a/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ImportExportFlow.cpp @@ -309,20 +309,25 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::Cancelled << std::endl; return; } - else if (searchContext.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE) - { - AICLI_LOG(CLI, Info, << "Package is already installed: [" << packageRequest.Id << "]"); - context.Reporter.Info() << Resource::String::ImportPackageAlreadyInstalled << ' ' << packageRequest.Id << std::endl; - continue; - } else { - AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << packageRequest.VersionAndChannel.ToString()); - context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; - - // Keep searching for the remaining packages and only fail at the end. - foundAll = false; - continue; + auto searchTerminationHR = searchContext.GetTerminationHR(); + if (searchTerminationHR == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE || + searchTerminationHR == APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED) + { + AICLI_LOG(CLI, Info, << "Package is already installed: [" << packageRequest.Id << "]"); + context.Reporter.Info() << Resource::String::ImportPackageAlreadyInstalled << ' ' << packageRequest.Id << std::endl; + continue; + } + else + { + AICLI_LOG(CLI, Info, << "Package not found for import: [" << packageRequest.Id << "], Version " << packageRequest.VersionAndChannel.ToString()); + context.Reporter.Info() << Resource::String::ImportSearchFailed << ' ' << packageRequest.Id << std::endl; + + // Keep searching for the remaining packages and only fail at the end. + foundAll = false; + continue; + } } } diff --git a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp index b48a427640..76a4353963 100644 --- a/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/UpdateFlow.cpp @@ -246,10 +246,19 @@ namespace AppInstaller::CLI::Workflow if (!m_isUpgrade && context.Contains(Execution::Data::InstalledPackageVersion) && context.Get() != nullptr) { - AICLI_LOG(CLI, Info, << "Found installed package, converting to upgrade flow"); - context.Reporter.Info() << Execution::ConvertToUpgradeFlowEmphasis << Resource::String::ConvertInstallFlowToUpgrade << std::endl; - context.SetFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); - m_isUpgrade = true; + if (context.Args.Contains(Execution::Args::Type::NoUpgrade)) + { + AICLI_LOG(CLI, Warning, << "Found installed package, exiting installation."); + context.Reporter.Warn() << Resource::String::PackageAlreadyInstalled << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED); + } + else + { + AICLI_LOG(CLI, Info, << "Found installed package, converting to upgrade flow"); + context.Reporter.Info() << Execution::ConvertToUpgradeFlowEmphasis << Resource::String::ConvertInstallFlowToUpgrade << std::endl; + context.SetFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); + m_isUpgrade = true; + } } if (context.Args.Contains(Execution::Args::Type::Version)) diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index bcd821863c..68abf6193a 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -198,6 +198,8 @@ public class ErrorCode public const int ERROR_NESTEDINSTALLER_INVALID_PATH = unchecked((int)0x8A15005D); public const int ERROR_PINNED_CERTIFICATE_MISMATCH = unchecked((int)0x8A15005E); public const int ERROR_INSTALL_LOCATION_REQUIRED = unchecked((int)0x8A15005F); + public const int ERROR_ARCHIVE_SCAN_FAILED = unchecked((int)0x8A150060); + public const int ERROR_PACKAGE_ALREADY_INSTALLED = unchecked((int)0x8A150061); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); @@ -215,6 +217,9 @@ public class ErrorCode public const int ERROR_INSTALL_DOWNGRADE = unchecked((int)0x8A15010E); public const int ERROR_INSTALL_BLOCKED_BY_POLICY = unchecked((int)0x8A15010F); public const int ERROR_INSTALL_DEPENDENCIES = unchecked((int)0x8A150110); + public const int ERROR_INSTALL_PACKAGE_IN_USE_BY_APPLICATION = unchecked((int)0x8A150111); + public const int ERROR_INSTALL_INVALID_PARAMETER = unchecked((int)0x8A150112); + public const int ERROR_INSTALL_SYSTEM_NOT_SUPPORTED = unchecked((int)0x8A150113); public const int INSTALLED_STATUS_ARP_ENTRY_NOT_FOUND = unchecked((int)0x8A150201); public const int INSTALLED_STATUS_INSTALL_LOCATION_NOT_APPLICABLE = unchecked((int)0x0A150202); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 9e6ed9cb8b..a86aaeade1 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1349,7 +1349,6 @@ Please specify one of them using the `--source` option to proceed. Command line alias added: - Portable install failed; Cleaning up... @@ -1467,4 +1466,10 @@ Please specify one of them using the `--source` option to proceed. Archive scan detected malware; proceeding due to --force {Locked="--force"} + + Skips upgrade if an installed version already exists + + + A package version is already installed. Installation cancelled. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 9fa4e44e16..9536d2165f 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -3685,6 +3685,27 @@ TEST_CASE("InstallFlow_FoundInstalledAndUpgradeAvailable", "[UpdateFlow][workflo REQUIRE(installResultStr.find("/ver3.0.0.0") != std::string::npos); } +TEST_CASE("InstallFlow_FoundInstalledAndUpgradeAvailable_WithNoUpgrade", "[UpdateFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller"sv); + context.Args.AddArg(Execution::Args::Type::NoUpgrade); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify Installer is not called. + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageAlreadyInstalled).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED); +} + TEST_CASE("InstallFlow_FoundInstalledAndUpgradeNotAvailable", "[UpdateFlow][workflow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp index 008ff9966d..0dce355f0e 100644 --- a/src/AppInstallerCommonCore/Errors.cpp +++ b/src/AppInstallerCommonCore/Errors.cpp @@ -180,6 +180,8 @@ namespace AppInstaller return "Failed to install portable package"; case APPINSTALLER_CLI_ERROR_PORTABLE_REPARSE_POINT_NOT_SUPPORTED: return "Volume does not support reparse points."; + case APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS: + return "Portable package from a different source already exists."; case APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY: return "Unable to create symlink, path points to a directory."; case APPINSTALLER_CLI_ERROR_INSTALLER_PROHIBITS_ELEVATION: @@ -188,12 +190,30 @@ namespace AppInstaller return "Failed to uninstall portable package"; case APPINSTALLER_CLI_ERROR_ARP_VERSION_VALIDATION_FAILED: return "Failed to validate DisplayVersion values against index."; + case APPINSTALLER_CLI_ERROR_UNSUPPORTED_ARGUMENT: + return "One or more arguments are not supported."; + case APPINSTALLER_CLI_ERROR_BIND_WITH_EMBEDDED_NULL: + return "Embedded null characters are disallowed for SQLite"; + case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND: + return "Failed to find the nested installer in the archive."; + case APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED: + return "Failed to extract archive."; + case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH: + return "Invalid relative file path to nested installer provided."; + case APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH: + return "The server certificate did not match any of the expected values."; case APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED: - return "Install location required but not provided"; + return "Install location must be provided."; + case APPINSTALLER_CLI_ERROR_ARCHIVE_SCAN_FAILED: + return "Archive malware scan failed."; + case APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED: + return "Found at least one version of the package installed."; + + // Install errors case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: - return "Application is currently running.Exit the application then try again."; + return "Application is currently running. Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS: - return "Another installation is already in progress.Try again later."; + return "Another installation is already in progress. Try again later."; case APPINSTALLER_CLI_ERROR_INSTALL_FILE_IN_USE: return "One or more file is being used. Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_MISSING_DEPENDENCY: @@ -222,14 +242,12 @@ namespace AppInstaller return "Organization policies are preventing installation. Contact your admin."; case APPINSTALLER_CLI_ERROR_INSTALL_DEPENDENCIES: return "Failed to install package dependencies."; - case APPINSTALLER_CLI_ERROR_BIND_WITH_EMBEDDED_NULL: - return "Embedded null characters are disallowed for SQLite"; - case APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH: - return "The server certificate did not match any of the expected values."; - case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND: - return "Failed to find the nested installer in the archive."; - case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH: - return "Invalid relative file path to nested installer provided."; + case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE_BY_APPLICATION: + return "Application is currently in use by another application."; + case APPINSTALLER_CLI_ERROR_INSTALL_INVALID_PARAMETER: + return "Invalid parameter."; + case APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED: + return "Package not supported by the system."; default: return "Unknown Error Code"; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index fdde7df584..130a556b0b 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -109,6 +109,7 @@ #define APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH ((HRESULT)0x8A15005E) #define APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED ((HRESULT)0x8A15005F) #define APPINSTALLER_CLI_ERROR_ARCHIVE_SCAN_FAILED ((HRESULT)0x8A150060) +#define APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED ((HRESULT)0x8A150061) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101)