winget-cli

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

commit d170d65e75ec91590d3ff9543811f6174af08ce1
parent 4f7ce38037c0392ef856c3c407f216c9a357f55f
Author: Luis Chacón <lechacon@users.noreply.github.com>
Date:   Wed, 16 Dec 2020 11:19:39 -0800

Implement uninstall flow (#659)

Added an UninstallCommand as an experimental feature, and workflow and E2E tests for it. For uninstalling MSIX/MSStore apps this uses the package name, for MSI it uses the product code, and for everything else the uninstall string. This uses already existing information form manifests/ARP.

Co-authored-by: Josh Soref <jsoref@users.noreply.github.com>
Diffstat:
M.github/actions/spelling/allow.txt | 1+
M.github/actions/spelling/expect.txt | 6++++++
M.github/actions/spelling/patterns.txt | 2++
Mdoc/Settings.md | 10++++++++++
Mdoc/settings.schema.json | 5+++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 4++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 12++++++++++++
Msrc/AppInstallerCLICore/Commands/RootCommand.cpp | 2++
Asrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/UninstallCommand.h | 25+++++++++++++++++++++++++
Msrc/AppInstallerCLICore/ExecutionContext.h | 21+++++++++++++++++++++
Msrc/AppInstallerCLICore/Resources.h | 11+++++++++--
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 6++++--
Msrc/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h | 12++++++++++++
Asrc/AppInstallerCLICore/Workflows/UninstallFlow.cpp | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/UninstallFlow.h | 26++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 4++--
Msrc/AppInstallerCLIE2ETests/BaseCommand.cs | 7+++++--
Msrc/AppInstallerCLIE2ETests/Constants.cs | 8++++++++
Msrc/AppInstallerCLIE2ETests/InstallCommand.cs | 9+++++++--
Msrc/AppInstallerCLIE2ETests/SetUpFixture.cs | 2+-
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml | 1+
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml | 1+
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml | 1+
Msrc/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml | 1+
Msrc/AppInstallerCLIE2ETests/TestIndexSetup.cs | 21+++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/UninstallCommand.cs | 145++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 22++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml | 1+
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml | 1+
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml | 1+
Msrc/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml | 1+
Msrc/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml | 1+
Msrc/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml | 1+
Msrc/AppInstallerCLITests/WorkFlow.cpp | 130++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCommonCore/AppInstallerTelemetry.cpp | 46+++++++++++++++++++++++++++++++++-------------
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 2++
Msrc/AppInstallerCommonCore/Public/AppInstallerTelemetry.h | 3+++
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 3++-
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 6++++++
Msrc/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp | 14++++++++++++--
Msrc/AppInstallerTestExeInstaller/main.cpp | 46+++++++++++++++++++++++++---------------------
45 files changed, 914 insertions(+), 84 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -414,6 +414,7 @@ und undef unicode uninstall +uninstalling Unregister updatemanifest UPLEVEL diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -236,6 +236,7 @@ PWAs PWSTR py pz +qb qword readonly REGSAM @@ -254,6 +255,8 @@ Shlobj sid SIGNATUREHASH Sku +Skype +skypeapp Solaris sortof sourceforge @@ -267,6 +270,7 @@ ssin stackoverflow Standalone startswith +STARTUPINFOW stdarg strchr strcmp @@ -291,6 +295,7 @@ uild uintptr unindent Uninitialize +uninstallation uninstaller uninstallprevious uninstalls @@ -317,6 +322,7 @@ website wesome wiki wikipedia +windir windowsdeveloper winerror wingetdev diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt @@ -15,3 +15,5 @@ data:[a-zA-Z=;,/0-9+-]+ \b[0-9a-f]{40}\b \b([A-Za-z])\1{3,}\b El proyecto .* diferentes +# Package family names +\b[-.A-Za-z0-9]+_[a-z0-9]{13}\b diff --git a/doc/Settings.md b/doc/Settings.md @@ -87,3 +87,13 @@ While work is in progress on upgrade, the command is hidden behind a feature tog "upgrade": true }, ``` + +### uninstall + +While work is in progress on uninstall, the command is hidden behind a feature toggle. One can enable it as below: + +``` + "experimentalFeatures": { + "uninstall": true + }, +``` diff --git a/doc/settings.schema.json b/doc/settings.schema.json @@ -57,6 +57,11 @@ "description": "Enable the upgrade command while it is in development", "type": "boolean", "default": false + }, + "uninstall": { + "description": "Enable the uninstall command while it is in development", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -185,6 +185,7 @@ <ClInclude Include="Commands\InstallCommand.h" /> <ClInclude Include="Commands\RootCommand.h" /> <ClInclude Include="Commands\SourceCommand.h" /> + <ClInclude Include="Commands\UninstallCommand.h" /> <ClInclude Include="Commands\UpgradeCommand.h" /> <ClInclude Include="Commands\ValidateCommand.h" /> <ClInclude Include="Commands\SettingsCommand.h" /> @@ -207,6 +208,7 @@ <ClInclude Include="Workflows\ManifestComparator.h" /> <ClInclude Include="Workflows\ShowFlow.h" /> <ClInclude Include="Workflows\SourceFlow.h" /> + <ClInclude Include="Workflows\UninstallFlow.h" /> <ClInclude Include="Workflows\UpdateFlow.h" /> <ClInclude Include="Workflows\WorkflowBase.h" /> </ItemGroup> @@ -224,6 +226,7 @@ <ClCompile Include="Commands\InstallCommand.cpp" /> <ClCompile Include="Commands\RootCommand.cpp" /> <ClCompile Include="Commands\SourceCommand.cpp" /> + <ClCompile Include="Commands\UninstallCommand.cpp" /> <ClCompile Include="Commands\UpgradeCommand.cpp" /> <ClCompile Include="Commands\ValidateCommand.cpp" /> <ClCompile Include="Commands\SettingsCommand.cpp" /> @@ -244,6 +247,7 @@ <ClCompile Include="Workflows\ManifestComparator.cpp" /> <ClCompile Include="Workflows\ShowFlow.cpp" /> <ClCompile Include="Workflows\SourceFlow.cpp" /> + <ClCompile Include="Workflows\UninstallFlow.cpp" /> <ClCompile Include="Workflows\UpdateFlow.cpp" /> <ClCompile Include="Workflows\WorkflowBase.cpp" /> </ItemGroup> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -135,6 +135,12 @@ <ClInclude Include="Commands\ListCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="Commands\UninstallCommand.h"> + <Filter>Commands</Filter> + </ClInclude> + <ClInclude Include="Workflows\UninstallFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -236,6 +242,12 @@ <ClCompile Include="Commands\ListCommand.cpp"> <Filter>Commands</Filter> </ClCompile> + <ClCompile Include="Commands\UninstallCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> + <ClCompile Include="Workflows\UninstallFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -9,6 +9,7 @@ #include "SearchCommand.h" #include "ListCommand.h" #include "UpgradeCommand.h" +#include "UninstallCommand.h" #include "HashCommand.h" #include "ValidateCommand.h" #include "SettingsCommand.h" @@ -32,6 +33,7 @@ namespace AppInstaller::CLI std::make_unique<SearchCommand>(FullName()), std::make_unique<ListCommand>(FullName()), std::make_unique<UpgradeCommand>(FullName()), + std::make_unique<UninstallCommand>(FullName()), std::make_unique<HashCommand>(FullName()), std::make_unique<ValidateCommand>(FullName()), std::make_unique<SettingsCommand>(FullName()), diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "UninstallCommand.h" +#include "Workflows/UninstallFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/CompletionFlow.h" +#include "Workflows/WorkflowBase.h" +#include "Resources.h" + +using AppInstaller::CLI::Execution::Args; +using AppInstaller::CLI::Workflow::ExecutionStage; + +namespace AppInstaller::CLI +{ + std::vector<Argument> UninstallCommand::GetArguments() const + { + // TODO: determine exact arguments needed + return + { + Argument::ForType(Args::Type::Query), + Argument::ForType(Args::Type::Manifest), + Argument::ForType(Args::Type::Id), + Argument::ForType(Args::Type::Name), + Argument::ForType(Args::Type::Moniker), + Argument::ForType(Args::Type::Version), + Argument::ForType(Args::Type::Channel), + Argument::ForType(Args::Type::Source), + Argument::ForType(Args::Type::Exact), + Argument::ForType(Args::Type::Interactive), + Argument::ForType(Args::Type::Silent), + Argument::ForType(Args::Type::Log), + }; + } + + Resource::LocString UninstallCommand::ShortDescription() const + { + return { Resource::String::UninstallCommandShortDescription }; + } + + Resource::LocString UninstallCommand::LongDescription() const + { + return { Resource::String::UninstallCommandLongDescription }; + } + + void UninstallCommand::Complete(Execution::Context& context, Execution::Args::Type valueType) const + { + if (valueType == Execution::Args::Type::Manifest || + valueType == Execution::Args::Type::Log) + { + // Intentionally output nothing to allow pass through to filesystem. + return; + } + + context << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + switch (valueType) + { + case Execution::Args::Type::Query: + context << + Workflow::RequireCompletionWordNonEmpty << + Workflow::SearchSourceForManyCompletion << + Workflow::CompleteWithMatchedField; + break; + case Execution::Args::Type::Id: + case Execution::Args::Type::Name: + case Execution::Args::Type::Moniker: + case Execution::Args::Type::Version: + case Execution::Args::Type::Channel: + case Execution::Args::Type::Source: + context << + Workflow::CompleteWithSingleSemanticsForValueUsingExistingSource(valueType); + break; + } + } + + std::string UninstallCommand::HelpLink() const + { + // TODO: point to correct location + return "https://aka.ms/winget-command-uninstall"; + } + + void UninstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if (execArgs.Contains(Execution::Args::Type::Manifest) && + (execArgs.Contains(Execution::Args::Type::Query) || + execArgs.Contains(Execution::Args::Type::Id) || + execArgs.Contains(Execution::Args::Type::Name) || + execArgs.Contains(Execution::Args::Type::Moniker) || + execArgs.Contains(Execution::Args::Type::Version) || + execArgs.Contains(Execution::Args::Type::Channel) || + execArgs.Contains(Execution::Args::Type::Source) || + execArgs.Contains(Execution::Args::Type::Exact))) + { + throw CommandException(Resource::String::BothManifestAndSearchQueryProvided, ""); + } + } + + void UninstallCommand::ExecuteInternal(Execution::Context& context) const + { + // open the sources where to search for the package + context << + Workflow::ReportExecutionStage(ExecutionStage::Discovery) << + Workflow::OpenSource << + Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed); + + // find the uninstaller + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + // --manifest case where new manifest is provided + context << + Workflow::GetManifestFromArg << + Workflow::ReportManifestIdentity << + Workflow::SearchSourceUsingManifest << + Workflow::EnsureOneMatchFromSearchResult(true); + } + else + { + // search for a single package to uninstall + context << + Workflow::SearchSourceForSingle << + Workflow::EnsureOneMatchFromSearchResult(true) << + Workflow::ReportPackageIdentity; + } + + context << + Workflow::GetInstalledPackageVersion << + Workflow::GetUninstallInfo << + Workflow::ReportExecutionStage(ExecutionStage::Execution) << + Workflow::ExecuteUninstaller << + Workflow::ReportExecutionStage(ExecutionStage::PostExecution); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.h b/src/AppInstallerCLICore/Commands/UninstallCommand.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct UninstallCommand final : public Command + { + UninstallCommand(std::string_view parent) : Command("uninstall", parent, Settings::ExperimentalFeature::Feature::ExperimentalUninstall) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + + std::string HelpLink() const override; + + protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -57,6 +57,9 @@ namespace AppInstaller::CLI::Execution CompletionData, InstalledPackageVersion, ExecutionStage, + UninstallString, + PackageFamilyNames, + ProductCodes, Max }; @@ -163,6 +166,24 @@ namespace AppInstaller::CLI::Execution using value_t = Workflow::ExecutionStage; }; + template <> + struct DataMapping<Data::UninstallString> + { + using value_t = std::string; + }; + + template <> + struct DataMapping<Data::PackageFamilyNames> + { + using value_t = std::vector<Utility::LocIndString>; + }; + + template <> + struct DataMapping<Data::ProductCodes> + { + using value_t = std::vector<Utility::LocIndString>; + }; + // Used to deduce the DataVariant type; making a variant that includes std::monostate and all DataMapping types. template <size_t... I> inline auto Deduce(std::index_sequence<I...>) { return std::variant<std::monostate, DataMapping<static_cast<Data>(I)>::value_t...>{}; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -107,13 +107,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MsixArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(MsixSignatureHashFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreAppBlocked); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallOrUpdateFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNetworkError); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementNoStoreAccount); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementServerError); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallGetEntitlementSuccess); - WINGET_DEFINE_RESOURCE_STRINGID(MSStoreStoreClientBlocked); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallOrUpdateFailed); WINGET_DEFINE_RESOURCE_STRINGID(MSStoreInstallTryGetEntitlement); + WINGET_DEFINE_RESOURCE_STRINGID(MSStoreStoreClientBlocked); WINGET_DEFINE_RESOURCE_STRINGID(MultipleInstalledPackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); @@ -121,6 +121,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage); WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound); WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound); + WINGET_DEFINE_RESOURCE_STRINGID(NoUninstallInfoFound); WINGET_DEFINE_RESOURCE_STRINGID(NoVTArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatch); WINGET_DEFINE_RESOURCE_STRINGID(OpenSourceFailedNoMatchHelp); @@ -205,6 +206,12 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(TooManyArgError); WINGET_DEFINE_RESOURCE_STRINGID(TooManyBehaviorsError); WINGET_DEFINE_RESOURCE_STRINGID(UnexpectedErrorExecutingCommand); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallAbandoned); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFailedWithCode); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowUninstallSuccess); + WINGET_DEFINE_RESOURCE_STRINGID(UninstallFlowStartingPackageUninstall); WINGET_DEFINE_RESOURCE_STRINGID(UnrecognizedCommand); WINGET_DEFINE_RESOURCE_STRINGID(UpdateAllArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(UpdateNotApplicable); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "InstallFlow.h" +#include "UninstallFlow.h" #include "Resources.h" #include "ShellExecuteInstallerHandler.h" #include "MSStoreInstallerHandler.h" @@ -280,9 +281,10 @@ namespace AppInstaller::CLI::Workflow case ManifestInstaller::InstallerTypeEnum::Wix: if (isUpdate && installer.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious) { - // TODO: hook up with uninstall when uninstall is implemented + context << + GetUninstallInfo << + ExecuteUninstaller; context.ClearFlags(Execution::ContextFlag::InstallerExecutionUseUpdate); - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } context << ShellExecuteInstall; break; diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -7,6 +7,7 @@ using namespace AppInstaller::CLI; using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; namespace AppInstaller::CLI::Workflow { @@ -15,7 +16,7 @@ namespace AppInstaller::CLI::Workflow // ShellExecutes the given path. std::optional<DWORD> InvokeShellExecute(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress) { - AICLI_LOG(CLI, Info, << "Starting installer: '" << filePath.u8string() << "' with arguments '" << args << '\''); + AICLI_LOG(CLI, Info, << "Starting: '" << filePath.u8string() << "' with arguments '" << args << '\''); SHELLEXECUTEINFOW execInfo = { 0 }; execInfo.cbSize = sizeof(execInfo); @@ -26,10 +27,8 @@ namespace AppInstaller::CLI::Workflow // Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete. // Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown. execInfo.nShow = SW_SHOW; - if (!ShellExecuteExW(&execInfo) || !execInfo.hProcess) - { - return GetLastError(); - } + + THROW_LAST_ERROR_IF(!ShellExecuteExW(&execInfo) || !execInfo.hProcess); wil::unique_process_handle process{ execInfo.hProcess }; @@ -166,6 +165,27 @@ namespace AppInstaller::CLI::Workflow // Todo: language token support will be implemented later } + + // Gets the arguments for uninstalling an MSI with MsiExec + std::string GetMsiExecUninstallArgs(Execution::Context& context, const Utility::LocIndString& productCode) + { + std::string args = "/x" + productCode.get(); + + // Set UI level for MsiExec with the /q flag. + // If interactive is requested, use the default instead of Reduced or Full as the installer may not use them. + if (context.Args.Contains(Execution::Args::Type::Silent)) + { + // n = None = silent + args += " /qn"; + } + else if (!context.Args.Contains(Execution::Args::Type::Interactive)) + { + // b = Basic = only progress bar + args += " /qb"; + } + + return args; + } } void ShellExecuteInstallImpl(Execution::Context& context) @@ -247,4 +267,82 @@ namespace AppInstaller::CLI::Workflow installerPath.assign(renamedDownloadedInstaller); AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); } + + void ShellExecuteUninstallImpl(Execution::Context& context) + { + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + std::wstring commandUtf16 = Utility::ConvertToUTF16(context.Get<Execution::Data::UninstallString>()); + + // Parse the command string as application and command line for CreateProcess + wil::unique_cotaskmem_string app = nullptr; + wil::unique_cotaskmem_string args = nullptr; + THROW_IF_FAILED(SHEvaluateSystemCommandTemplate(commandUtf16.c_str(), &app, NULL, &args)); + + auto uninstallResult = context.Reporter.ExecuteWithProgress( + std::bind(InvokeShellExecute, + std::filesystem::path(app.get()), + Utility::ConvertToUTF8(args.get()), + std::placeholders::_1)); + + if (!uninstallResult) + { + context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + else if (uninstallResult.value() != 0) + { + const auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); + Logging::Telemetry().LogUninstallerFailure( + installedPackageVersion->GetProperty(PackageVersionProperty::Id), + installedPackageVersion->GetProperty(PackageVersionProperty::Version), + "UninstallString", + uninstallResult.value()); + + context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult.value() << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); + } + else + { + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } + } + + void ShellExecuteMsiExecUninstall(Execution::Context& context) + { + const auto& productCodes = context.Get<Execution::Data::ProductCodes>(); + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + const std::filesystem::path msiexecPath{ ExpandEnvironmentVariables(L"%windir%\\system32\\msiexec.exe") }; + + for (const auto& productCode : productCodes) + { + AICLI_LOG(CLI, Info, << "Removing: " << productCode); + auto uninstallResult = context.Reporter.ExecuteWithProgress( + std::bind(InvokeShellExecute, + msiexecPath, + GetMsiExecUninstallArgs(context, productCode), + std::placeholders::_1)); + + if (!uninstallResult) + { + context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + else if (uninstallResult.value() != 0) + { + // TODO: Check for other success codes + const auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); + Logging::Telemetry().LogUninstallerFailure( + installedPackageVersion->GetProperty(PackageVersionProperty::Id), + installedPackageVersion->GetProperty(PackageVersionProperty::Version), + "MsiExec", + uninstallResult.value()); + + context.Reporter.Error() << Resource::String::UninstallFailedWithCode << ' ' << uninstallResult.value() << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED); + } + } + + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h @@ -17,6 +17,18 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void ShellExecuteInstallImpl(Execution::Context& context); + // Uninstall is done through invoking ShellExecute on uninstall string. + // Required Args: None + // Inputs: UninstallString + // Outputs: None + void ShellExecuteUninstallImpl(Execution::Context& context); + + // Removes the MSI + // Required Args: None + // Inputs: ProductCodes + // Output: None + void ShellExecuteMsiExecUninstall(Execution::Context& context); + // Gets the installer args from the context. // Required Args: None // Inputs: Manifest?, Installer, InstallerPath diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "UninstallFlow.h" +#include "WorkflowBase.h" +#include "ShellExecuteInstallerHandler.h" +#include "AppInstallerMsixInfo.h" + +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Msix; +using namespace AppInstaller::Repository; + +namespace AppInstaller::CLI::Workflow +{ + void GetUninstallInfo(Execution::Context& context) + { + auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); + const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType]; + switch (ManifestInstaller::ConvertToInstallerTypeEnum(installedTypeString)) + { + case ManifestInstaller::InstallerTypeEnum::Exe: + case ManifestInstaller::InstallerTypeEnum::Burn: + case ManifestInstaller::InstallerTypeEnum::Inno: + case ManifestInstaller::InstallerTypeEnum::Nullsoft: + { + IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata(); + + // Default to silent unless it is not present or interactivity is requested + auto uninstallCommandItr = packageMetadata.find(PackageVersionMetadata::SilentUninstallCommand); + if (uninstallCommandItr == packageMetadata.end() || context.Args.Contains(Execution::Args::Type::Interactive)) + { + auto interactiveItr = packageMetadata.find(PackageVersionMetadata::StandardUninstallCommand); + if (interactiveItr != packageMetadata.end()) + { + uninstallCommandItr = interactiveItr; + } + } + + if (uninstallCommandItr == packageMetadata.end()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + context.Add<Execution::Data::UninstallString>(uninstallCommandItr->second); + break; + } + case ManifestInstaller::InstallerTypeEnum::Msi: + case ManifestInstaller::InstallerTypeEnum::Wix: + { + // Uninstall strings for MSI don't include UI level (/q) needed to avoid interactivity, + // so we handle them differently. + auto productCodes = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); + if (productCodes.empty()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + context.Add<Execution::Data::ProductCodes>(std::move(productCodes)); + break; + } + case ManifestInstaller::InstallerTypeEnum::Msix: + case ManifestInstaller::InstallerTypeEnum::MSStore: + { + auto packageFamilyNames = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); + if (packageFamilyNames.empty()) + { + context.Reporter.Error() << Resource::String::NoUninstallInfoFound << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND); + } + + context.Add<Execution::Data::PackageFamilyNames>(packageFamilyNames); + break; + } + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + void ExecuteUninstaller(Execution::Context& context) + { + const std::string installedTypeString = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[PackageVersionMetadata::InstalledType]; + switch (ManifestInstaller::ConvertToInstallerTypeEnum(installedTypeString)) + { + case ManifestInstaller::InstallerTypeEnum::Exe: + case ManifestInstaller::InstallerTypeEnum::Burn: + case ManifestInstaller::InstallerTypeEnum::Inno: + case ManifestInstaller::InstallerTypeEnum::Nullsoft: + context << Workflow::ShellExecuteUninstallImpl; + break; + case ManifestInstaller::InstallerTypeEnum::Msi: + case ManifestInstaller::InstallerTypeEnum::Wix: + context << Workflow::ShellExecuteMsiExecUninstall; + break; + case ManifestInstaller::InstallerTypeEnum::Msix: + case ManifestInstaller::InstallerTypeEnum::MSStore: + context << Workflow::MsixUninstall; + break; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + void MsixUninstall(Execution::Context& context) + { + const auto& packageFamilyNames = context.Get<Execution::Data::PackageFamilyNames>(); + context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; + + for (const auto& packageFamilyName : packageFamilyNames) + { + auto packageFullName = Msix::GetPackageFullNameFromFamilyName(packageFamilyName); + if (!packageFullName.has_value()) + { + AICLI_LOG(CLI, Warning, << "No package found with family name: " << packageFamilyName); + continue; + } + + AICLI_LOG(CLI, Info, << "Removing MSIX package: " << packageFullName.value()); + context.Reporter.ExecuteWithProgress(std::bind(Deployment::RemovePackage, packageFullName.value(), std::placeholders::_1)); + } + + context.Reporter.Info() << Resource::String::UninstallFlowUninstallSuccess << std::endl; + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.h b/src/AppInstallerCLICore/Workflows/UninstallFlow.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Gets the command string or package family names used to uninstall the package. + // Required Args: None + // Inputs: InstalledPackageVersion + // Output: UninstallString?, PackageFamilyNames? + void GetUninstallInfo(Execution::Context& context); + + // Uninstalls the package according to its type. + // Required Args: None + // Inputs: InstalledPackageVersion, UninstallString?, PackageFamilyNames? + // Output: None + void ExecuteUninstaller(Execution::Context& context); + + // Removes the MSIX. + // Required Args: None + // Inputs: PackageFamilyNames + // Outputs: None + void MsixUninstall(Execution::Context& context); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -155,7 +155,7 @@ namespace AppInstaller::CLI::Workflow void ReportMultiplePackageFoundResult(Execution::Context& context); // Ensures that there is at least one result in the search. - // Required Args: bool indicating id the search result is from installed source + // Required Args: bool indicating if the search result is from installed source // Inputs: SearchResult // Outputs: None struct EnsureMatchesFromSearchResult : public WorkflowTask @@ -170,7 +170,7 @@ namespace AppInstaller::CLI::Workflow }; // Ensures that there is only one result in the search. - // Required Args: bool indicating id the search result is from installed source + // Required Args: bool indicating if the search result is from installed source // Inputs: SearchResult // Outputs: None struct EnsureOneMatchFromSearchResult : public WorkflowTask diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -12,7 +12,9 @@ namespace AppInstallerCLIE2ETests public class BaseCommand { - public readonly string SettingsJsonFilePath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json"; + public string SettingsJsonFilePath => TestCommon.PackagedContext ? + @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json" : + @"Microsoft\WinGet\Settings\settings.json"; public readonly string LocalAppData = "LocalAppData"; [OneTimeSetUp] @@ -57,7 +59,8 @@ namespace AppInstallerCLIE2ETests experimentalCmd = status, experimentalMSStore = status, list = status, - upgrade = status + upgrade = status, + uninstall = status, } }; diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -45,6 +45,14 @@ namespace AppInstallerCLIE2ETests public const string WinGetUtil = "WinGetUtil"; public const string E2ETestLogsPath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir"; + // Test installers' package IDs + public const string ExeInstallerPackageId = "AppInstallerTest.TestExeInstaller"; + public const string MsiInstallerPackageId = "AppInstallerTest.TestMsiInstaller"; + public const string MsixInstallerPackageId = "AppInstallerTest.TestMsixInstaller"; + + public const string MsiInstallerProductCode = "{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; + public const string MsixInstallerPackageFamilyName = "6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e_8wekyb3d8bbwe"; + public class ErrorCode { public const int S_OK = 0; diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -9,7 +9,7 @@ namespace AppInstallerCLIE2ETests public class InstallCommand : BaseCommand { private const string InstallTestExeInstalledFile = @"TestExeInstalled.txt"; - private const string InstallTestMsiInstalledFile = @"AppInstallerTestMsiInstaller.msi"; + private const string InstallTestMsiInstalledFile = @"AppInstallerTestExeInstaller.exe"; private const string InstallTestMsiProductId = @"{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; private const string InstallTestMsixName = @"6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; @@ -92,9 +92,14 @@ namespace AppInstallerCLIE2ETests Assert.True(VerifyTestExeInstalled(installDir, "/S")); } - //[Test] + [Test] public void InstallMSI() { + if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + Assert.Ignore("MSI installer not available"); + } + var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"TestMsiInstaller --silent -l {installDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -64,7 +64,7 @@ namespace AppInstallerCLIE2ETests ShouldDisableDevModeOnExit = EnableDevMode(true); - ShouldRevertDefaultFileTypeRiskOnExit = DecreaseFileTypeRisk(".exe", false); + ShouldRevertDefaultFileTypeRiskOnExit = DecreaseFileTypeRisk(".exe;.msi", false); Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"TRUSTEDPEOPLE\" " + TestCommon.GetTestDataFile(Constants.AppInstallerTestCert)), "Add AppInstallerTestCert"); Assert.True(TestCommon.RunCommand("certutil.exe", "-addstore -f \"ROOT\" " + TestCommon.GetTestDataFile(Constants.IndexPackageRootCert)), "Add IndexPackageRootCert"); diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml @@ -8,6 +8,7 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: <EXEHASH> InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' Switches: Custom: /execustom SilentWithProgress: /exeswp diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.yaml @@ -8,6 +8,7 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe Sha256: <EXEHASH> InstallerType: exe + ProductCode: '{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}' Switches: Custom: /execustom SilentWithProgress: /exeswp diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsiInstaller.yaml @@ -8,4 +8,5 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi Sha256: <MSIHASH> InstallerType: msi + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestMsixInstaller.yaml @@ -8,4 +8,5 @@ Installers: Url: https://localhost:5001/TestKit/AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.msix Sha256: <MSIXHASH> InstallerType: msix + PackageFamilyName: 6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -31,6 +31,11 @@ namespace AppInstallerCLIE2ETests CopyExeInstallerToTestDirectory(); } + if (!string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + CopyMsiInstallerToTestDirectory(); + } + if (!string.IsNullOrEmpty(TestCommon.MsixInstallerPath)) { CopyMsixInstallerToTestDirectory(); @@ -95,6 +100,22 @@ namespace AppInstallerCLIE2ETests SignFile(TestCommon.ExeInstallerPath); } + private static void CopyMsiInstallerToTestDirectory() + { + // Set MSI Test Installer Path + string msiInstallerDestPath = Path.Combine(TestCommon.StaticFileRootPath, Constants.MsiInstaller); + DirectoryInfo msiInstallerDestDir = Directory.CreateDirectory(msiInstallerDestPath); + + // Copy MSI Test Installer to Destination Path + string msiInstallerFullName = Path.Combine(msiInstallerDestDir.FullName, "AppInstallerTestMsiInstaller.msi"); + + File.Copy(TestCommon.MsiInstallerPath, msiInstallerFullName, true); + TestCommon.MsiInstallerPath = msiInstallerFullName; + + // Sign MSI Installer File + SignFile(TestCommon.MsiInstallerPath); + } + private static void CopyMsixInstallerToTestDirectory() { // Set Msix Test Installer Path diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -1,30 +1,115 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace AppInstallerCLIE2ETests -{ - using NUnit.Framework; - using System.IO; - - public class UninstallCommand : BaseCommand - { - private const string UninstallTestExeInstalledFile = @"TestExeUninstalled.txt"; - - //[Test] - public void UninstallTestExe() - { - // Example Uninstall Command Test - var installDir = TestCommon.GetRandomTestDir(); - TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); - var result = TestCommon.RunAICLICommand("uninstall", $"AppInstallerTest.TextExeInstaller"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Successfully uninstalled")); - Assert.True(VerifyTestExeUninstalled(installDir)); - } - - private bool VerifyTestExeUninstalled(string uninstallDir) - { - return File.Exists(Path.Combine(uninstallDir, UninstallTestExeInstalledFile)); - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + using System.IO; + + public class UninstallCommand : BaseCommand + { + // Custom product code for overriding the default in the test exe + private const string CustomProductCode = "{f08fc03c-0b7e-4fca-9b3c-3a384d18a9f3}"; + + // File written when uninstalling the test exe + private const string UninstallTestExeUninstalledFile = "TestExeUninstalled.txt"; + + // Name of a file installed by the MSI that will be removed during uninstall + private const string UninstallTestMsiInstalledFile = "AppInstallerTestExeInstaller.exe"; + + // Package name of the test MSIX package + private const string UninstallTestMsixName = "6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + InitializeAllFeatures(false); + ConfigureFeature("uninstall", true); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + InitializeAllFeatures(false); + } + + [Test] + public void UninstallTestExe() + { + // Uninstall an Exe + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"{Constants.ExeInstallerPackageId} --silent -l {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", Constants.ExeInstallerPackageId); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestExeUninstalled(installDir)); + } + + [Test] + public void UninstallTestMsi() + { + if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath)) + { + Assert.Ignore("MSI installer not available"); + } + + // Uninstall an MSI + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"{Constants.MsiInstallerPackageId} -l {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", Constants.MsiInstallerPackageId); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestMsiUninstalled(installDir)); + } + + [Test] + public void UninstallTestMsix() + { + // Uninstall an MSIX + TestCommon.RunAICLICommand("install", Constants.MsixInstallerPackageId); + var result = TestCommon.RunAICLICommand("uninstall", Constants.MsixInstallerPackageId); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestMsixUninstalled()); + } + + [Test] + public void UninstallNotIndexed() + { + // Uninstalls a package found with ARP not matching any known manifest. + // Install the test EXE providing a custom Product Code so that it cannot be mapped + // back to its manifest, then uninstall it using its Product Code + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"{Constants.ExeInstallerPackageId} --override \"/ProductID {CustomProductCode} /InstallDir {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", CustomProductCode); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestExeUninstalled(installDir)); + } + + [Test] + public void UninstallAppNotInstalled() + { + // Verify failure when trying to uninstall an app that is not installed. + var result = TestCommon.RunAICLICommand("uninstall", $"TestMsixInstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("No installed package found matching input criteria.")); + } + + private bool VerifyTestExeUninstalled(string installDir) + { + return File.Exists(Path.Combine(installDir, UninstallTestExeUninstalledFile)); + } + + private bool VerifyTestMsiUninstalled(string installDir) + { + return !File.Exists(Path.Combine(installDir, UninstallTestMsiInstalledFile)); + } + + private bool VerifyTestMsixUninstalled() + { + var result = TestCommon.RunCommandWithResult("powershell", $"Get-AppxPackage {UninstallTestMsixName}"); + return string.IsNullOrWhiteSpace(result.StdOut); + } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -734,4 +734,26 @@ They can be configured through the settings file 'winget settings'.</value> <data name="SourceOpenWithFailedUpdate" xml:space="preserve"> <value>Failed in attempting to update the source:</value> </data> + <data name="UninstallCommandLongDescription" xml:space="preserve"> + <value>Uninstalls the selected package, either found by searching the installed packages list or directly from a manifest. By default, the query must case-insensitively match the id, name, or moniker of the package. Other fields can be used by passing their appropriate option.</value> + </data> + <data name="UninstallCommandShortDescription" xml:space="preserve"> + <value>Uninstalls the given package</value> + </data> + <data name="UninstallFlowStartingPackageUninstall" xml:space="preserve"> + <value>Starting package uninstall...</value> + </data> + <data name="UninstallFlowUninstallSuccess" xml:space="preserve"> + <value>Successfully uninstalled</value> + </data> + <data name="NoUninstallInfoFound" xml:space="preserve"> + <value>winget cannot locate the uninstall command for this package. Please reach out to the package publisher for support.</value> + <comment>{Locked="winget"}</comment> + </data> + <data name="UninstallAbandoned" xml:space="preserve"> + <value>Uninstallation abandoned</value> + </data> + <data name="UninstallFailedWithCode" xml:space="preserve"> + <value>Uninstall failed with exit code:</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_MSStore.yaml @@ -9,4 +9,5 @@ Installers: Url: https://ThisIsNotUsed InstallerType: MSStore ProductId: 9WZDNCRFJ364 + PackageFamilyName: Microsoft.SkypeApp_kzf8qxf38zg5c ManifestVersion: 0.2.0-msstore diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_DownloadFlow.yaml @@ -9,4 +9,5 @@ Installers: Url: https://github.com/microsoft/msix-packaging/blob/master/src/test/testData/unpack/TestAppxPackage_x64.appx?raw=true InstallerType: msix Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Msix_StreamingFlow.yaml @@ -10,4 +10,5 @@ Installers: InstallerType: msix Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07 + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml b/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_NoSwitches.yaml @@ -9,4 +9,5 @@ Installers: Url: https://ThisIsNotUsed InstallerType: msi Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml b/src/AppInstallerCLITests/TestData/InstallerArgTest_Msi_WithSwitches.yaml @@ -9,6 +9,7 @@ Installers: Url: https://ThisIsNotUsed InstallerType: msi Sha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + ProductCode: '{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}' Switches: Custom: /mycustom SilentWithProgress: /mysilentwithprogress diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Msix.yaml @@ -11,4 +11,5 @@ Installers: InstallerType: msix Sha256: 6a2d3683fa19bf00e58e07d1313d20a5f5735ebbd6a999d33381d28740ee07ea SignatureSha256: 138781c3e6f635240353f3d14d1d57bdcb89413e49be63b375e6a5d7b93b0d07 + PackageFamilyName: 20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -8,6 +8,7 @@ #include <AppInstallerDownloader.h> #include <AppInstallerStrings.h> #include <Workflows/InstallFlow.h> +#include <Workflows/UninstallFlow.h> #include <Workflows/UpdateFlow.h> #include <Workflows/MSStoreInstallerHandler.h> #include <Workflows/ShowFlow.h> @@ -17,6 +18,7 @@ #include <Public/AppInstallerRepositorySearch.h> #include <Commands/InstallCommand.h> #include <Commands/ShowCommand.h> +#include <Commands/UninstallCommand.h> #include <Commands/UpgradeCommand.h> #include <winget/LocIndependent.h> #include <winget/ManifestYamlParser.h> @@ -111,7 +113,12 @@ namespace ResultMatch( TestPackage::Make( manifest, - TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, + TestPackage::MetadataMap + { + { PackageVersionMetadata::InstalledType, "Exe" }, + { PackageVersionMetadata::StandardUninstallCommand, "C:\\uninstall.exe" }, + { PackageVersionMetadata::SilentUninstallCommand, "C:\\uninstall.exe /silence" }, + }, std::vector<Manifest>{ manifest2, manifest } ), PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestExeInstaller"))); @@ -309,6 +316,19 @@ void OverrideForShellExecute(TestContext& context) OverrideForUpdateInstallerMotw(context); } +void OverrideForExeUninstall(TestContext& context) +{ + context.Override({ ShellExecuteUninstallImpl, [](TestContext& context) + { + // Write out the uninstall command + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestExeUninstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + file << context.Get<Execution::Data::UninstallString>(); + file.close(); + } }); +} + void OverrideForMSIX(TestContext& context) { context.Override({ MsixInstall, [](TestContext& context) @@ -330,6 +350,23 @@ void OverrideForMSIX(TestContext& context) } }); } +void OverrideForMSIXUninstall(TestContext& context) +{ + context.Override({ MsixUninstall, [](TestContext& context) + { + // Write out the package full name + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestMsixUninstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + for (const auto& packageFamilyName : context.Get<Execution::Data::PackageFamilyNames>()) + { + file << packageFamilyName << std::endl; + } + + file.close(); + } }); +} + void OverrideForMSStore(TestContext& context, bool isUpdate) { if (isUpdate) @@ -940,6 +977,97 @@ TEST_CASE("UpdateFlow_UpdateAllApplicable", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updateMSStoreResultPath.GetPath())); } +TEST_CASE("UninstallFlow_UninstallExe", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForExeUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestExeInstaller"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is called and parameters are passed in. + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); + std::ifstream uninstallResultFile(uninstallResultPath.GetPath()); + REQUIRE(uninstallResultFile.is_open()); + std::string uninstallResultStr; + std::getline(uninstallResultFile, uninstallResultStr); + REQUIRE(uninstallResultStr.find("uninstall.exe") != std::string::npos); + REQUIRE(uninstallResultStr.find("/silence") != std::string::npos); +} + +TEST_CASE("UninstallFlow_UninstallMsix", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestMsixUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSIXUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMsixInstaller"sv); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is called with the package full name. + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); + std::ifstream uninstallResultFile(uninstallResultPath.GetPath()); + REQUIRE(uninstallResultFile.is_open()); + std::string uninstallResultStr; + std::getline(uninstallResultFile, uninstallResultStr); + REQUIRE(uninstallResultStr.find("20477fca-282d-49fb-b03e-371dca074f0f_8wekyb3d8bbwe") != std::string::npos); +} + +TEST_CASE("UninstallFlow_UninstallMSStore", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestMsixUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + OverrideForMSIXUninstall(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestMSStoreInstaller"sv); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is called with the package full name + REQUIRE(std::filesystem::exists(uninstallResultPath.GetPath())); + std::ifstream uninstallResultFile(uninstallResultPath.GetPath()); + REQUIRE(uninstallResultFile.is_open()); + std::string uninstallResultStr; + std::getline(uninstallResultFile, uninstallResultStr); + REQUIRE(uninstallResultStr.find("microsoft.skypeapp_kzf8qxf38zg5c") != std::string::npos); +} + +TEST_CASE("UninstallFlow_UninstallExeNotFound", "[UninstallFlow][workflow]") +{ + TestCommon::TempFile uninstallResultPath("TestExeUninstalled.txt"); + + std::ostringstream uninstallOutput; + TestContext context{ uninstallOutput, std::cin }; + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.MissingApp"sv); + context.Args.AddArg(Execution::Args::Type::Silent); + + UninstallCommand uninstall({}); + uninstall.Execute(context); + INFO(uninstallOutput.str()); + + // Verify Uninstaller is not called. + REQUIRE(!std::filesystem::exists(uninstallResultPath.GetPath())); + REQUIRE(uninstallOutput.str().find(Resource::LocString(Resource::String::NoInstalledPackageFound).get()) != std::string::npos); + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); +} + void VerifyMotw(const std::filesystem::path& testFile, DWORD zone) { std::filesystem::path motwFile(testFile); diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -421,8 +421,28 @@ namespace AppInstaller::Logging } AICLI_LOG(CLI, Error, << type << " installer failed: " << errorCode); - } - + } + + void TelemetryTraceLogger::LogUninstallerFailure(std::string_view id, std::string_view version, std::string_view type, uint32_t errorCode) + { + if (IsTelemetryEnabled()) + { + TraceLoggingWriteActivity(g_hTelemetryProvider, + "UninstallerFailure", + GetActivityId(), + nullptr, + TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), + AICLI_TraceLoggingStringView(id, "Id"), + AICLI_TraceLoggingStringView(version, "Version"), + AICLI_TraceLoggingStringView(type, "Type"), + TraceLoggingUInt32(errorCode, "ErrorCode"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, << type << " uninstaller failed: " << errorCode); + } + void TelemetryTraceLogger::LogDuplicateARPEntry(HRESULT hr, std::string_view scope, std::string_view architecture, std::string_view productCode, std::string_view name) { if (IsTelemetryEnabled()) @@ -469,15 +489,15 @@ namespace AppInstaller::Logging std::atomic_uint32_t SubExecutionTelemetryScope::m_sessionId{ s_RootExecutionId }; - SubExecutionTelemetryScope::SubExecutionTelemetryScope() - { - auto expected = s_RootExecutionId; - THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !s_subExecutionId.compare_exchange_strong(expected, ++m_sessionId), - "Cannot create a sub execution telemetry session when a previous session exists."); - } - - SubExecutionTelemetryScope::~SubExecutionTelemetryScope() - { - s_subExecutionId = s_RootExecutionId; - } + SubExecutionTelemetryScope::SubExecutionTelemetryScope() + { + auto expected = s_RootExecutionId; + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !s_subExecutionId.compare_exchange_strong(expected, ++m_sessionId), + "Cannot create a sub execution telemetry session when a previous session exists."); + } + + SubExecutionTelemetryScope::~SubExecutionTelemetryScope() + { + s_subExecutionId = s_RootExecutionId; + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -25,6 +25,8 @@ namespace AppInstaller::Settings return User().Get<Setting::EFList>(); case Feature::ExperimentalUpgrade: return User().Get<Setting::EFExperimentalUpgrade>(); + case Feature::ExperimentalUninstall: + return User().Get<Setting::EFUninstall>(); default: THROW_HR(E_UNEXPECTED); } @@ -44,6 +46,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "List Command", "list", "https://aka.ms/winget-settings", Feature::ExperimentalList }; case Feature::ExperimentalUpgrade: return ExperimentalFeature{ "Upgrade Command", "upgrade", "https://aka.ms/winget-settings", Feature::ExperimentalUpgrade }; + case Feature::ExperimentalUninstall: + return ExperimentalFeature{ "Uninstall Command", "uninstall", "https://aka.ms/winget-settings", Feature::ExperimentalUninstall }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -59,6 +59,8 @@ #define APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE ((HRESULT)0x8A15002C) #define APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED ((HRESULT)0x8A15002D) #define APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH ((HRESULT)0x8A15002E) +#define APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND ((HRESULT)0x8a15002F) +#define APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED ((HRESULT)0x8a150030) namespace AppInstaller { diff --git a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h @@ -89,6 +89,9 @@ namespace AppInstaller::Logging // Logs a failed installation attempt. void LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode); + // Logs a failed uninstallation attempt. + void LogUninstallerFailure(std::string_view id, std::string_view version, std::string_view type, uint32_t errorCode); + // Logs a failure to insert a value into the in-memory cache of installed system packages. // The most likely reason is due to the same key name being used under multiple ARP scope/architecture locations. void LogDuplicateARPEntry(HRESULT hr, std::string_view scope, std::string_view architecture, std::string_view productCode, std::string_view name); diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -24,7 +24,8 @@ namespace AppInstaller::Settings ExperimentalMSStore = 0x4, ExperimentalList = 0x8, ExperimentalUpgrade = 0x10, - Max = 0x11, // This MUST always be last + ExperimentalUninstall = 0x20, + Max, // This MUST always be last }; using Feature_t = std::underlying_type_t<ExperimentalFeature::Feature>; diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -51,6 +51,7 @@ namespace AppInstaller::Settings EFExperimentalMSStore, EFList, EFExperimentalUpgrade, + EFUninstall, Max }; @@ -85,6 +86,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalMSStore, bool, bool, false, ".experimentalFeatures.experimentalMSStore"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFList, bool, bool, false, ".experimentalFeatures.list"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalUpgrade, bool, bool, false, ".experimentalFeatures.upgrade"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFUninstall, bool, bool, false, "experimentalFeatures.uninstall"sv); // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template <size_t... I> diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -210,6 +210,12 @@ namespace AppInstaller::Settings { return value; } + + std::optional<SettingMapping<Setting::EFUninstall>::value_t> + SettingMapping<Setting::EFUninstall>::Validate(const SettingMapping<Setting::EFUninstall>::json_t& value) + { + return value; + } } UserSettings::UserSettings() : m_type(UserSettingsType::Default) diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -152,9 +152,19 @@ namespace AppInstaller::Repository::Microsoft void ARPHelper::AddMetadataIfPresent(const Registry::Key& key, const std::wstring& name, SQLiteIndex& index, SQLiteIndex::IdType manifestId, PackageVersionMetadata metadata) { auto value = key[name]; - if (value && value->GetType() == Registry::Value::Type::String) + if (value) { - auto valueString = value->GetValue<Registry::Value::Type::String>(); + std::string valueString; + + if (value->GetType() == Registry::Value::Type::String) + { + valueString = value->GetValue<Registry::Value::Type::String>(); + } + else if (value->GetType() == Registry::Value::Type::ExpandString) + { + valueString = value->GetValue<Registry::Value::Type::ExpandString>(); + } + if (!valueString.empty()) { index.SetMetadataByManifestId(manifestId, metadata, valueString); diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -14,7 +14,8 @@ using namespace std::filesystem; std::string_view registrySubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; std::string_view defaultProductID = "{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; -std::wstring GenerateUninstaller(std::ostream& out, const path& installDirectory) { +path GenerateUninstaller(std::ostream& out, const path& installDirectory, const std::string& productID) +{ path uninstallerPath = installDirectory; uninstallerPath /= "UninstallTestExe.bat"; @@ -23,17 +24,28 @@ std::wstring GenerateUninstaller(std::ostream& out, const path& installDirectory path uninstallerOutputTextFilePath = installDirectory; uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; - // TODO: Needs to re-invoke the installer and remove the Uninstall key that it added + std::string registryKey{ "HKEY_CURRENT_USER\\" }; + registryKey += registrySubkey; + if (!productID.empty()) + { + registryKey += productID; + } + else + { + registryKey += defaultProductID; + } + std::ofstream uninstallerScript(uninstallerPath); uninstallerScript << "@echo off\n"; uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; + uninstallerScript << "REG DELETE " << registryKey << " /f\n"; uninstallerScript.close(); - return uninstallerPath.wstring(); + return uninstallerPath; } -void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, const std::wstring& uninstallerPath) +void WriteToUninstallRegistry(std::ostream& out, const std::string& productID, const path& uninstallerPath) { HKEY hkey; LONG lReg; @@ -45,20 +57,20 @@ void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, const wchar_t* uninstallString = uninstallerPath.c_str(); DWORD version = 1; - path registryKey{ registrySubkey }; + std::string registryKey{ registrySubkey }; if (!productID.empty()) { - registryKey /= productID; + registryKey += productID; out << "Product Code overridden to: " << registryKey << "\n"; } else { - registryKey /= defaultProductID; + registryKey += defaultProductID; out << "Default Product Code used: " << registryKey << "\n"; } - lReg = RegCreateKeyEx( + lReg = RegCreateKeyExA( HKEY_CURRENT_USER, registryKey.c_str(), 0, @@ -69,8 +81,8 @@ void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, &hkey, NULL); - if (lReg == ERROR_SUCCESS) { - + if (lReg == ERROR_SUCCESS) + { out << "Successfully opened registry key \n"; // Set Display Name Property Value @@ -116,9 +128,8 @@ void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, int main(int argc, const char** argv) { path installDirectory = temp_directory_path(); - std::wstringstream productCodeStream; std::stringstream outContent; - std::wstring productCode; + std::string productCode; // Output to cout by default, but swap to a file if requested std::ostream* out = &std::cout; @@ -138,7 +149,7 @@ int main(int argc, const char** argv) // Supports custom product code ID if (_stricmp(argv[i], "/ProductID") == 0 && ++i < argc) { - productCodeStream << argv[i]; + productCode = argv[i]; } // Supports log file @@ -157,16 +168,9 @@ int main(int argc, const char** argv) file.close(); - if (!productCodeStream.str().empty()) - { - productCode = productCodeStream.str(); - } - - std::wstring uninstallerPath = GenerateUninstaller(*out, installDirectory); + path uninstallerPath = GenerateUninstaller(*out, installDirectory, productCode); WriteToUninstallRegistry(*out, productCode, uninstallerPath); return 0; } - -