winget-cli

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

commit 2dfe512566e3b93772eca768a79f537310c2355b
parent a864c8242a0418a31ef363ba8c6e12a8f9b309bf
Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com>
Date:   Tue, 16 Aug 2022 13:57:00 -0700

Add support for installing portables without developer mode and running as admin (#2401)


Diffstat:
Msrc/AppInstallerCLICore/ExecutionContextData.h | 7++++---
Msrc/AppInstallerCLICore/Workflows/PortableFlow.cpp | 411+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/AppInstallerCLICore/Workflows/PortableFlow.h | 2++
Msrc/AppInstallerCLICore/Workflows/UninstallFlow.cpp | 17++++++++++-------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Asrc/AppInstallerCLITests/PortableEntry.cpp | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml | 2+-
Msrc/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml | 1+
Msrc/AppInstallerCLITests/TestHooks.h | 6++++++
Msrc/AppInstallerCLITests/TestSource.cpp | 13+++++++------
Msrc/AppInstallerCLITests/TestSource.h | 2+-
Msrc/AppInstallerCLITests/WorkFlow.cpp | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 4++++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 12++++++++++++
Msrc/AppInstallerCommonCore/Filesystem.cpp | 36++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/PathVariable.cpp | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/PortableARPEntry.cpp | 2++
Asrc/AppInstallerCommonCore/PortableEntry.cpp | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/Filesystem.h | 3+++
Asrc/AppInstallerCommonCore/Public/winget/PathVariable.h | 32++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/PortableARPEntry.h | 1+
Asrc/AppInstallerCommonCore/Public/winget/PortableEntry.h | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
23 files changed, 801 insertions(+), 312 deletions(-)

diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -5,6 +5,7 @@ #include <winget/Manifest.h> #include <winget/ARPCorrelation.h> #include <winget/PortableARPEntry.h> +#include <winget/PortableEntry.h> #include "CompletionData.h" #include "PackageCollection.h" #include "Workflows/WorkflowBase.h" @@ -53,7 +54,7 @@ namespace AppInstaller::CLI::Execution Dependencies, DependencySource, AllowedArchitectures, - PortableARPEntry, + PortableEntry, AllowUnknownScope, Max }; @@ -219,9 +220,9 @@ namespace AppInstaller::CLI::Execution }; template <> - struct DataMapping<Data::PortableARPEntry> + struct DataMapping<Data::PortableEntry> { - using value_t = Registry::Portable::PortableARPEntry; + using value_t = Portable::PortableEntry; }; template <> diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.cpp b/src/AppInstallerCLICore/Workflows/PortableFlow.cpp @@ -5,12 +5,16 @@ #include "WorkflowBase.h" #include "winget/Filesystem.h" #include "winget/PortableARPEntry.h" +#include "winget/PortableEntry.h" +#include "winget/PathVariable.h" #include "AppInstallerStrings.h" using namespace AppInstaller::Manifest; +using namespace AppInstaller::Portable; using namespace AppInstaller::Utility; using namespace AppInstaller::Registry; using namespace AppInstaller::Registry::Portable; +using namespace AppInstaller::Registry::Environment; using namespace AppInstaller::Repository; using namespace std::filesystem; @@ -18,9 +22,6 @@ namespace AppInstaller::CLI::Workflow { namespace { - constexpr std::wstring_view s_PathName = L"Path"; - constexpr std::wstring_view s_PathSubkey_User = L"Environment"; - constexpr std::wstring_view s_PathSubkey_Machine = L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; constexpr std::string_view s_DefaultSource = "*DefaultSource"sv; void AppendExeExtension(std::filesystem::path& value) @@ -174,76 +175,6 @@ namespace AppInstaller::CLI::Workflow return appsAndFeaturesEntry; } - bool AddToPathRegistry(Manifest::ScopeEnum scope) - { - const std::filesystem::path& linksDirectory = GetPortableLinksLocation(scope); - - Key key; - if (scope == Manifest::ScopeEnum::Machine) - { - key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); - } - else - { - key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); - } - - std::wstring pathName = std::wstring{ s_PathName }; - std::string portableLinksDir = Normalize(linksDirectory.u8string()); - std::string pathValue = Normalize(key[pathName]->GetValue<Value::Type::String>()); - - if (pathValue.find(portableLinksDir) == std::string::npos) - { - if (pathValue.back() != ';') - { - pathValue += ";"; - } - - pathValue += portableLinksDir + ";"; - AICLI_LOG(CLI, Info, << "Adding to Path environment variable: " << portableLinksDir); - key.SetValue(pathName, ConvertToUTF16(pathValue), REG_EXPAND_SZ); - return true; - } - else - { - AICLI_LOG(CLI, Verbose, << "Path already existed in environment variable. Skipping..."); - return false; - } - } - - bool RemoveFromPathRegistry(Manifest::ScopeEnum scope) - { - const std::filesystem::path& linksDirectory = GetPortableLinksLocation(scope); - - Key key; - if (scope == Manifest::ScopeEnum::Machine) - { - key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); - } - else - { - key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); - } - - std::wstring pathName = std::wstring{ s_PathName }; - std::string portableLinksDir = Normalize(linksDirectory.u8string()); - std::string pathValue = Normalize(key[pathName]->GetValue<Value::Type::String>()); - - if (pathValue.find(portableLinksDir) != std::string::npos) - { - FindAndReplace(pathValue, portableLinksDir, ""); - FindAndReplace(pathValue, ";;", ";"); - AICLI_LOG(CLI, Info, << "Removing from Path environment variable: " << portableLinksDir); - key.SetValue(pathName, ConvertToUTF16(pathValue), REG_EXPAND_SZ); - return true; - } - else - { - AICLI_LOG(CLI, Verbose, << "Path does not exist in environment variable."); - return false; - } - } - void InitializePortableARPEntry(Execution::Context& context) { const std::string& packageIdentifier = context.Get<Execution::Data::Manifest>().Id; @@ -258,12 +189,12 @@ namespace AppInstaller::CLI::Workflow sourceIdentifier = s_DefaultSource; } - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); - - if (uninstallEntry.Exists()) - { - if (!uninstallEntry.IsSamePortablePackageEntry(packageIdentifier, sourceIdentifier)) - { + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + + if (portableEntry.Exists()) + { + if (packageIdentifier != portableEntry.WinGetPackageIdentifier || sourceIdentifier != portableEntry.WinGetSourceIdentifier) + { // TODO: Replace HashOverride with --Force when argument behavior gets updated. if (!context.Args.Contains(Execution::Args::Type::HashOverride)) { @@ -274,241 +205,200 @@ namespace AppInstaller::CLI::Workflow { AICLI_LOG(CLI, Info, << "Overriding registry match check..."); context.Reporter.Warn() << Resource::String::PortableRegistryCollisionOverridden << std::endl; - } - } - } - - uninstallEntry.SetValue(PortableValueName::WinGetPackageIdentifier, packageIdentifier); - uninstallEntry.SetValue(PortableValueName::WinGetSourceIdentifier, sourceIdentifier); - uninstallEntry.SetValue(PortableValueName::UninstallString, L"winget uninstall --product-code " + ConvertToUTF16(GetPortableProductCode(context))); - uninstallEntry.SetValue(PortableValueName::WinGetInstallerType, ConvertToUTF16(InstallerTypeToString(InstallerTypeEnum::Portable))); + } + } + } + + portableEntry.Commit(PortableValueName::WinGetPackageIdentifier, portableEntry.WinGetPackageIdentifier = packageIdentifier); + portableEntry.Commit(PortableValueName::WinGetSourceIdentifier, portableEntry.WinGetSourceIdentifier = sourceIdentifier); + portableEntry.Commit(PortableValueName::UninstallString, portableEntry.UninstallString = "winget uninstall --product-code " + GetPortableProductCode(context)); + portableEntry.Commit(PortableValueName::WinGetInstallerType, portableEntry.WinGetInstallerType = InstallerTypeToString(InstallerTypeEnum::Portable)); } void MovePortableExe(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); const std::filesystem::path& installerPath = context.Get<Execution::Data::InstallerPath>(); - const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); - const std::filesystem::path& targetDirectory = GetPortableTargetDirectory(context); - - bool isDirectoryCreated = false; - if (std::filesystem::create_directories(targetDirectory)) - { - AICLI_LOG(CLI, Info, << "Created target install directory: " << targetDirectory); - isDirectoryCreated = true; - } - - if (std::filesystem::exists(targetFullPath)) - { - std::filesystem::remove(targetFullPath); - AICLI_LOG(CLI, Info, << "Removing existing portable exe at: " << targetFullPath); - } - - Filesystem::RenameFile(installerPath, targetFullPath); - AICLI_LOG(CLI, Info, << "Portable exe moved to: " << targetFullPath); - - if (!uninstallEntry[PortableValueName::InstallDirectoryCreated].has_value()) - { - uninstallEntry.SetValue(PortableValueName::InstallDirectoryCreated, isDirectoryCreated); - } - - uninstallEntry.SetValue(PortableValueName::PortableTargetFullPath, targetFullPath.wstring()); - uninstallEntry.SetValue(PortableValueName::InstallLocation, GetPortableTargetDirectory(context).wstring()); - uninstallEntry.SetValue(PortableValueName::SHA256, Utility::SHA256::ConvertToWideString(context.Get<Execution::Data::HashPair>().second)); + portableEntry.Commit(PortableValueName::PortableTargetFullPath, portableEntry.PortableTargetFullPath = GetPortableTargetFullPath(context)); + portableEntry.Commit(PortableValueName::InstallLocation, portableEntry.InstallLocation = GetPortableTargetDirectory(context)); + portableEntry.Commit(PortableValueName::SHA256, portableEntry.SHA256 = SHA256::ConvertToString(context.Get<Execution::Data::HashPair>().second)); + portableEntry.MovePortableExe(installerPath); } void RemovePortableExe(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); - const auto& targetPath = uninstallEntry[PortableValueName::PortableTargetFullPath]; - - if (targetPath.has_value()) - { - const std::filesystem::path& targetPathValue = targetPath.value().GetValue<Value::Type::UTF16String>(); - const auto& expectedHash = uninstallEntry[PortableValueName::SHA256]; - std::string expectedHashValue = expectedHash.has_value() ? uninstallEntry[PortableValueName::SHA256].value().GetValue<Value::Type::String>() : ""; + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + const auto& targetPath = portableEntry.PortableTargetFullPath; - if (std::filesystem::exists(targetPathValue)) + if (std::filesystem::exists(targetPath)) + { + if (!portableEntry.VerifyPortableExeHash()) { - std::ifstream inStream{ targetPathValue, std::ifstream::binary }; - const Utility::SHA256::HashBuffer& targetFileHash = SHA256::ComputeHash(inStream); - inStream.close(); - bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); - - if (!SHA256::AreEqual(SHA256::ConvertToBytes(expectedHashValue), targetFileHash)) + if (overrideHashMismatch) { - if (overrideHashMismatch) - { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; - } - else - { - context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); - } + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverridden << std::endl; + } + else + { + context.Reporter.Warn() << Resource::String::PortableHashMismatchOverrideRequired << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_UNINSTALL_FAILED); } - - std::filesystem::remove(targetPathValue); - AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPathValue); } - else - { - AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPathValue); - } - } - else - { - AICLI_LOG(CLI, Info, << "The registry value for [TargetFullPath] does not exist"); + + std::filesystem::remove(targetPath); + AICLI_LOG(CLI, Info, << "Successfully deleted portable exe:" << targetPath); + } + else + { + AICLI_LOG(CLI, Info, << "Portable exe not found; Unable to delete portable exe: " << targetPath); } } void RemoveInstallDirectory(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); - const auto& installDirectory = uninstallEntry[PortableValueName::InstallLocation]; + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + const auto& installDirectory = portableEntry.InstallLocation; - if (installDirectory.has_value()) + if (std::filesystem::exists(installDirectory)) { - const std::filesystem::path& installDirectoryValue = installDirectory.value().GetValue<Value::Type::UTF16String>(); - if (std::filesystem::exists(installDirectoryValue)) - { - const auto& isDirectoryCreated = uninstallEntry[PortableValueName::InstallDirectoryCreated]; - const auto& isDirectoryCreatedValue = isDirectoryCreated.has_value() ? isDirectoryCreated.value().GetValue<Value::Type::DWord>() : FALSE; - - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + const auto& isCreated = portableEntry.InstallDirectoryCreated; + bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); - if (context.Args.Contains(Execution::Args::Type::Purge) || - (!isUpdate && Settings::User().Get<Settings::Setting::UninstallPurgePortablePackage>() && !context.Args.Contains(Execution::Args::Type::Preserve))) + if (context.Args.Contains(Execution::Args::Type::Purge) || + (!isUpdate && Settings::User().Get<Settings::Setting::UninstallPurgePortablePackage>() && !context.Args.Contains(Execution::Args::Type::Preserve))) + { + if (isCreated) { - if (isDirectoryCreatedValue) - { - context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl; - const auto& removedFilesCount = std::filesystem::remove_all(installDirectoryValue); - AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); - } - else - { - context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl; - } - + context.Reporter.Warn() << Resource::String::PurgeInstallDirectory << std::endl; + const auto& removedFilesCount = std::filesystem::remove_all(installDirectory); + AICLI_LOG(CLI, Info, << "Purged install location directory. Deleted " << removedFilesCount << " files or directories"); } - else if (std::filesystem::is_empty(installDirectoryValue)) - { - if (isDirectoryCreatedValue) - { - std::filesystem::remove(installDirectoryValue); - AICLI_LOG(CLI, Info, << "Install directory deleted: " << installDirectoryValue); - } - } - else - { - context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectoryValue << std::endl; + else + { + context.Reporter.Warn() << Resource::String::UnableToPurgeInstallDirectory << std::endl; } + } - else - { - AICLI_LOG(CLI, Info, << "Install directory does not exist: " << installDirectoryValue); + else if (std::filesystem::is_empty(installDirectory)) + { + if (isCreated) + { + std::filesystem::remove(installDirectory); + AICLI_LOG(CLI, Info, << "Install directory deleted: " << installDirectory); + } + } + else + { + context.Reporter.Warn() << Resource::String::FilesRemainInInstallDirectory << installDirectory << std::endl; } } else { - AICLI_LOG(CLI, Info, << "The registry value for [InstallLocation] does not exist"); + AICLI_LOG(CLI, Info, << "Install directory does not exist: " << installDirectory); } } void CreatePortableSymlink(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); - const std::filesystem::path& targetFullPath = GetPortableTargetFullPath(context); + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + if (portableEntry.InstallDirectoryAddedToPath) + { + AICLI_LOG(CLI, Info, << "Package directory was previously added to PATH. Skipping symlink creation."); + return; + } + const std::filesystem::path& symlinkFullPath = GetPortableSymlinkFullPath(context); - + portableEntry.Commit(PortableValueName::PortableSymlinkFullPath, portableEntry.PortableSymlinkFullPath = symlinkFullPath); + std::filesystem::file_status status = std::filesystem::status(symlinkFullPath); if (std::filesystem::is_directory(status)) { AICLI_LOG(CLI, Info, << "Unable to create symlink. '" << symlinkFullPath << "points to an existing directory."); AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PORTABLE_SYMLINK_PATH_IS_DIRECTORY); } - else if (std::filesystem::remove(symlinkFullPath)) + + if (std::filesystem::remove(symlinkFullPath)) { AICLI_LOG(CLI, Info, << "Removed existing file at " << symlinkFullPath); context.Reporter.Warn() << Resource::String::OverwritingExistingFileAtMessage << ' ' << symlinkFullPath.u8string() << std::endl; } - std::filesystem::create_symlink(targetFullPath, symlinkFullPath); - AICLI_LOG(CLI, Info, << "Symlink created at: " << symlinkFullPath); - uninstallEntry.SetValue(PortableValueName::PortableSymlinkFullPath, symlinkFullPath.wstring()); + portableEntry.CreatePortableSymlink(); + } - Manifest::ScopeEnum scope = ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - if (AddToPathRegistry(scope)) + void AddToPathVariable(Execution::Context& context) + { + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + const std::filesystem::path& pathValue = portableEntry.GetPathValue(); + if (portableEntry.AddToPathVariable()) { + AICLI_LOG(CLI, Info, << "Appended target directory to PATH registry: " << pathValue); context.Reporter.Warn() << Resource::String::ModifiedPathRequiresShellRestart << std::endl; + } + else + { + AICLI_LOG(CLI, Info, << "Target directory already exists in PATH registry: " << pathValue); } } + void RemoveFromPathVariable(Execution::Context& context) + { + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + const std::filesystem::path& pathValue = portableEntry.GetPathValue(); + if (portableEntry.RemoveFromPathVariable()) + { + AICLI_LOG(CLI, Info, << "Removed target directory from PATH registry: " << pathValue); + } + else + { + AICLI_LOG(CLI, Info, << "Target directory not removed from PATH registry: " << pathValue); + } + } + void RemovePortableSymlink(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); - const auto& symlinkPath = uninstallEntry[PortableValueName::PortableSymlinkFullPath]; - if (symlinkPath.has_value()) - { - const std::filesystem::path& symlinkPathValue = symlinkPath->GetValue<Value::Type::UTF16String>(); - if (std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPathValue))) + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + const auto& symlinkPath = portableEntry.PortableSymlinkFullPath; + + if (!std::filesystem::is_symlink(std::filesystem::symlink_status(symlinkPath))) + { + AICLI_LOG(Core, Info, << "The registry value for [PortableSymlinkFullPath] does not point to a valid symlink file."); + return; + } + + if (portableEntry.VerifySymlinkTarget()) + { + if (!std::filesystem::remove(symlinkPath)) { - const auto& targetPath = uninstallEntry[PortableValueName::PortableTargetFullPath]; - if (targetPath.has_value()) - { - const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(symlinkPathValue); - const std::filesystem::path& targetPathValue = targetPath->GetValue<Value::Type::UTF16String>(); - if (symlinkTargetPath != targetPathValue) - { - AICLI_LOG(CLI, Warning, << "Portable symlink not deleted; Symlink points to a different target exe: " << symlinkTargetPath << - "; Expected target exe: " << targetPathValue); - context.Reporter.Warn() << Resource::String::SymlinkModified << std::endl; - } - else if (!std::filesystem::remove(symlinkPathValue)) - { - AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPathValue); - } - } - else - { - AICLI_LOG(CLI, Info, << "The registry value for [TargetFullPath] does not exist"); - } + AICLI_LOG(CLI, Info, << "Portable symlink not found; Unable to delete portable symlink: " << symlinkPath); } - } - else - { - AICLI_LOG(CLI, Info, << "The registry value for [SymlinkFullPath] does not exist"); - } - - Manifest::ScopeEnum scope = uninstallEntry.GetScope(); - - if (std::filesystem::is_empty(GetPortableLinksLocation(scope))) + } + else { - RemoveFromPathRegistry(scope); + context.Reporter.Warn() << Resource::String::SymlinkModified << std::endl; } } void RemovePortableARPEntry(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); - uninstallEntry.Delete(); + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); + portableEntry.RemoveARPEntry(); AICLI_LOG(CLI, Info, << "PortableARPEntry deleted."); } void CommitPortableMetadataToRegistry(Execution::Context& context) { - PortableARPEntry& uninstallEntry = context.Get<Execution::Data::PortableARPEntry>(); + Portable::PortableEntry& portableEntry = context.Get<Execution::Data::PortableEntry>(); const AppInstaller::Manifest::Manifest& manifest = context.Get<Execution::Data::Manifest>(); const Manifest::AppsAndFeaturesEntry& entry = GetAppsAndFeaturesEntryForPortableInstall(context.Get<Execution::Data::Installer>()->AppsAndFeaturesEntries, manifest); - uninstallEntry.SetValue(PortableValueName::DisplayName, entry.DisplayName); - uninstallEntry.SetValue(PortableValueName::DisplayVersion, entry.DisplayVersion); - uninstallEntry.SetValue(PortableValueName::Publisher, entry.Publisher); - uninstallEntry.SetValue(PortableValueName::InstallDate, Utility::GetCurrentDateForARP()); - uninstallEntry.SetValue(PortableValueName::URLInfoAbout, manifest.CurrentLocalization.Get<Manifest::Localization::PackageUrl>()); - uninstallEntry.SetValue(PortableValueName::HelpLink, manifest.CurrentLocalization.Get<Manifest::Localization::PublisherSupportUrl>()); + portableEntry.Commit(PortableValueName::DisplayName, portableEntry.DisplayName = entry.DisplayName); + portableEntry.Commit(PortableValueName::DisplayVersion, portableEntry.DisplayVersion = entry.DisplayVersion); + portableEntry.Commit(PortableValueName::Publisher, portableEntry.Publisher = entry.Publisher); + portableEntry.Commit(PortableValueName::InstallDate, portableEntry.InstallDate = Utility::GetCurrentDateForARP()); + portableEntry.Commit(PortableValueName::URLInfoAbout, portableEntry.URLInfoAbout = manifest.CurrentLocalization.Get<Manifest::Localization::PackageUrl>()); + portableEntry.Commit(PortableValueName::HelpLink, portableEntry.HelpLink = manifest.CurrentLocalization.Get < Manifest::Localization::PublisherSupportUrl>()); } void EnsureValidArgsForPortableInstall(Execution::Context& context) @@ -542,21 +432,6 @@ namespace AppInstaller::CLI::Workflow } } - // TODO: Remove entire task once issue regarding symlink creation privilege has been resolved. - void EnsureSymlinkCreationPrivilege(Execution::Context& context) - { - if (!Runtime::IsDevModeEnabled()) - { - AICLI_LOG(CLI, Info, << "Developer mode not enabled."); - context << Workflow::EnsureRunningAsAdmin; - - if (context.IsTerminated()) - { - context.Reporter.Error() << std::endl << "https://github.com/microsoft/winget-cli/issues/2368" << std::endl; - } - } - } - void EnsureRunningAsAdminForMachineScopeInstall(Execution::Context& context) { // Admin is required for machine scope install or else creating a symlink in the %PROGRAMFILES% link location will fail. @@ -591,7 +466,8 @@ namespace AppInstaller::CLI::Workflow context.Get<Execution::Data::Installer>()->Arch, GetPortableProductCode(context)); - context.Add<Execution::Data::PortableARPEntry>(std::move(uninstallEntry)); + PortableEntry portableEntry = PortableEntry(uninstallEntry); + context.Add<Execution::Data::PortableEntry>(std::move(portableEntry)); try { @@ -601,6 +477,7 @@ namespace AppInstaller::CLI::Workflow InitializePortableARPEntry << MovePortableExe << CreatePortableSymlink << + AddToPathVariable << CommitPortableMetadataToRegistry; context.Add<Execution::Data::OperationReturnCode>(context.GetTerminationHR()); @@ -623,7 +500,7 @@ namespace AppInstaller::CLI::Workflow Execution::Context& uninstallPortableContext = *uninstallPortableContextPtr; auto previousThreadGlobals = uninstallPortableContext.SetForCurrentThread(); - uninstallPortableContext.Add<Execution::Data::PortableARPEntry>(context.Get<Execution::Data::PortableARPEntry>()); + uninstallPortableContext.Add<Execution::Data::PortableEntry>(context.Get<Execution::Data::PortableEntry>()); uninstallPortableContext << PortableUninstallImpl; } } @@ -638,6 +515,7 @@ namespace AppInstaller::CLI::Workflow RemovePortableExe << RemoveInstallDirectory << RemovePortableSymlink << + RemoveFromPathVariable << RemovePortableARPEntry; context.Add<Execution::Data::OperationReturnCode>(context.GetTerminationHR()); @@ -658,13 +536,26 @@ namespace AppInstaller::CLI::Workflow if (installerType == InstallerTypeEnum::Portable) { context << - EnsureSymlinkCreationPrivilege << EnsureRunningAsAdminForMachineScopeInstall << EnsureValidArgsForPortableInstall << EnsureVolumeSupportsReparsePoints; } } + void EnsureSupportForPortableUninstall(Execution::Context& context) + { + auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); + const std::string installedTypeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledType]; + if (ConvertToInstallerTypeEnum(installedTypeString) == InstallerTypeEnum::Portable) + { + const std::string installedScope = installedPackageVersion->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; + if (ConvertToScopeEnum(installedScope) == Manifest::ScopeEnum::Machine) + { + context << EnsureRunningAsAdmin; + } + } + } + // TODO: remove this check once support for portable in archive has been implemented void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context) { diff --git a/src/AppInstallerCLICore/Workflows/PortableFlow.h b/src/AppInstallerCLICore/Workflows/PortableFlow.h @@ -19,5 +19,7 @@ namespace AppInstaller::CLI::Workflow void EnsureSupportForPortableInstall(Execution::Context& context); + void EnsureSupportForPortableUninstall(Execution::Context& context); + void EnsureNonPortableTypeForArchiveInstall(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp b/src/AppInstallerCLICore/Workflows/UninstallFlow.cpp @@ -59,6 +59,7 @@ namespace AppInstaller::CLI::Workflow { context << Workflow::GetInstalledPackageVersion << + Workflow::EnsureSupportForPortableUninstall << Workflow::GetUninstallInfo << Workflow::GetDependenciesInfoForUninstall << Workflow::ReportDependencies(Resource::String::UninstallCommandReportDependencies) << @@ -131,20 +132,22 @@ namespace AppInstaller::CLI::Workflow } case InstallerTypeEnum::Portable: { - 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); + 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); } const std::string installedScope = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[Repository::PackageVersionMetadata::InstalledScope]; const std::string installedArch = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[Repository::PackageVersionMetadata::InstalledArchitecture]; - Portable::PortableARPEntry uninstallEntry = Portable::PortableARPEntry( + Registry::Portable::PortableARPEntry uninstallEntry = Registry::Portable::PortableARPEntry( ConvertToScopeEnum(installedScope), Utility::ConvertToArchitectureEnum(installedArch), productCodes[0]); - context.Add<Execution::Data::PortableARPEntry>(uninstallEntry); + Portable::PortableEntry portableEntry = Portable::PortableEntry(uninstallEntry); + + context.Add<Execution::Data::PortableEntry>(portableEntry); break; } default: diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -212,6 +212,7 @@ <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="PackageCollection.cpp" /> <ClCompile Include="PackageTrackingCatalog.cpp" /> + <ClCompile Include="PortableEntry.cpp" /> <ClCompile Include="PredefinedInstalledSource.cpp" /> <ClCompile Include="PreIndexedPackageSource.cpp" /> <ClCompile Include="Regex.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -224,6 +224,9 @@ <ClCompile Include="FolderFileWatcher.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="PortableEntry.cpp"> + <Filter>Source Files\Common</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/PortableEntry.cpp b/src/AppInstallerCLITests/PortableEntry.cpp @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <winget/PathVariable.h> +#include <winget/PortableEntry.h> +#include <winget/PortableARPEntry.h> +#include <Public/AppInstallerArchitecture.h> + +using namespace AppInstaller::Portable; +using namespace AppInstaller::Utility; +using namespace TestCommon; + +TEST_CASE("VerifyPortableMove", "[PortableEntry]") +{ + PortableARPEntry testARPEntry = PortableARPEntry( + AppInstaller::Manifest::ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + PortableEntry testEntry = PortableEntry(testARPEntry); + TestCommon::TempDirectory tempDirectory("TempDirectory", false); + testEntry.InstallLocation = tempDirectory.GetPath(); + testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "output.txt"; + + TestCommon::TempFile testFile("input.txt"); + std::ofstream file(testFile.GetPath(), std::ofstream::out); + file.close(); + + testEntry.MovePortableExe(testFile.GetPath()); + REQUIRE(std::filesystem::exists(testEntry.PortableTargetFullPath)); + REQUIRE(testEntry.InstallDirectoryCreated); + + // Create a second PortableEntry instance to emulate installing for a second time. (ARP entry should already exist) + PortableARPEntry testARPEntry2 = PortableARPEntry( + AppInstaller::Manifest::ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + PortableEntry testEntry2 = PortableEntry(testARPEntry2); + REQUIRE(testEntry2.InstallDirectoryCreated); // InstallDirectoryCreated should already be initialized as true. + + testEntry2.InstallLocation = tempDirectory.GetPath(); + testEntry2.PortableTargetFullPath = tempDirectory.GetPath() / "output2.txt"; + + TestCommon::TempFile testFile2("input2.txt"); + std::ofstream file2(testFile2, std::ofstream::out); + file2.close(); + + testEntry2.MovePortableExe(testFile2.GetPath()); + REQUIRE(std::filesystem::exists(testEntry2.PortableTargetFullPath)); + // InstallDirectoryCreated value should be preserved even though the directory was not created; + REQUIRE(testEntry2.InstallDirectoryCreated); + testEntry2.RemoveARPEntry(); +} + +TEST_CASE("VerifySymlinkCheck", "[PortableEntry]") +{ + PortableARPEntry testARPEntry = PortableARPEntry( + AppInstaller::Manifest::ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + PortableEntry testEntry = PortableEntry(testARPEntry); + + TestCommon::TempFile testFile("target.txt"); + std::ofstream file(testFile.GetPath(), std::ofstream::out); + file.close(); + + TestCommon::TempDirectory tempDirectory("TempDirectory", true); + testEntry.PortableTargetFullPath = testFile.GetPath(); + testEntry.PortableSymlinkFullPath = tempDirectory.GetPath() / "symlink.exe"; + + testEntry.CreatePortableSymlink(); + + REQUIRE(testEntry.VerifySymlinkTarget()); + + // Modify with incorrect target full path. + testEntry.PortableTargetFullPath = tempDirectory.GetPath() / "invalidTarget.txt"; + REQUIRE_FALSE(testEntry.VerifySymlinkTarget()); + testEntry.RemoveARPEntry(); +} + +TEST_CASE("VerifyPathVariableModified", "[PortableEntry]") +{ + PortableARPEntry testARPEntry = PortableARPEntry( + AppInstaller::Manifest::ScopeEnum::User, + Architecture::X64, + "testProductCode"); + + PortableEntry testEntry = PortableEntry(testARPEntry); + testEntry.InstallDirectoryAddedToPath = true; + TestCommon::TempDirectory tempDirectory("TempDirectory", false); + const std::filesystem::path& pathValue = tempDirectory.GetPath(); + testEntry.InstallLocation = pathValue; + testEntry.AddToPathVariable(); + + AppInstaller::Registry::Environment::PathVariable pathVariable(AppInstaller::Manifest::ScopeEnum::User); + REQUIRE(pathVariable.Contains(pathValue)); + + testEntry.RemoveFromPathVariable(); + REQUIRE_FALSE(pathVariable.Contains(pathValue)); + testEntry.RemoveARPEntry(); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_Portable.yaml @@ -5,7 +5,7 @@ PackageName: AppInstaller Test Portable Exe Publisher: Microsoft Corporation AppMoniker: AICLITestPortable License: Test -ProductCode: AppInstallerCliTest.TestPortable__DefaultSource +ProductCode: AppInstallerCliTest.TestPortableInstaller__TestSource Installers: - Architecture: x64 InstallerUrl: https://ThisIsNotUsed diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_Portable.yaml @@ -5,6 +5,7 @@ PackageLocale: en-US PackageName: AppInstaller Test Portable Exe Publisher: Microsoft Corporation AppMoniker: AICLITestPortable +ProductCode: AppInstallerCliTest.TestPortableInstaller__TestSource License: Test Installers: - Architecture: x64 diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -10,6 +10,7 @@ #include <AppInstallerTelemetry.h> #include <AppInstallerRuntime.h> #include <winget/UserSettings.h> +#include <winget/Filesystem.h> #ifdef AICLI_DISABLE_TEST_HOOKS static_assert(false, "Test hooks have been disabled"); @@ -39,4 +40,9 @@ namespace AppInstaller { void SetUserSettingsOverride(UserSettings* value); } + + namespace Filesystem + { + void TestHook_SetCreateSymlinkResult_Override(bool* status); + } } diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp @@ -70,13 +70,14 @@ namespace TestCommon case PackageVersionMultiProperty::PackageFamilyName: for (const auto& installer : VersionManifest.Installers) { - AddFoldedIfHasValueAndNotPresent(installer.PackageFamilyName, result); + AddIfHasValueAndNotPresent(installer.PackageFamilyName, result, true); } break; case PackageVersionMultiProperty::ProductCode: for (const auto& installer : VersionManifest.Installers) { - AddFoldedIfHasValueAndNotPresent(installer.ProductCode, result); + bool shouldFoldCaseForNonPortable = installer.EffectiveInstallerType() != AppInstaller::Manifest::InstallerTypeEnum::Portable; + AddIfHasValueAndNotPresent(installer.ProductCode, result, shouldFoldCaseForNonPortable); } break; case PackageVersionMultiProperty::Name: @@ -112,15 +113,15 @@ namespace TestCommon return Metadata; } - void TestPackageVersion::AddFoldedIfHasValueAndNotPresent(const Utility::NormalizedString& value, std::vector<LocIndString>& target) + void TestPackageVersion::AddIfHasValueAndNotPresent(const Utility::NormalizedString& value, std::vector<LocIndString>& target, bool folded) { if (!value.empty()) { - std::string folded = FoldCase(value); - auto itr = std::find(target.begin(), target.end(), folded); + std::string valueString = folded ? FoldCase(value) : value; + auto itr = std::find(target.begin(), target.end(), valueString); if (itr == target.end()) { - target.emplace_back(std::move(folded)); + target.emplace_back(std::move(valueString)); } } } diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h @@ -38,7 +38,7 @@ namespace TestCommon std::weak_ptr<const ISource> Source; protected: - static void AddFoldedIfHasValueAndNotPresent(const AppInstaller::Utility::NormalizedString& value, std::vector<LocIndString>& target); + static void AddIfHasValueAndNotPresent(const AppInstaller::Utility::NormalizedString& value, std::vector<LocIndString>& target, bool folded = false); }; // IPackage for TestSource diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -37,6 +37,7 @@ #include <Commands/SourceCommand.h> #include <winget/LocIndependent.h> #include <winget/ManifestYamlParser.h> +#include <winget/PathVariable.h> #include <Resources.h> #include <AppInstallerFileLogger.h> #include <Commands/ValidateCommand.h> @@ -641,6 +642,28 @@ void OverrideForPortableInstallFlow(TestContext& context) OverrideForPortableInstall(context); } +void OverridePortableInstaller(TestContext& context) +{ + context.Override({ DownloadInstallerFile, [](TestContext& context) + { + std::filesystem::path tempDirectory = std::filesystem::temp_directory_path(); + const auto& installerPath = TestDataFile("AppInstallerTestExeInstaller.exe").GetPath(); + const auto& tempInstallerPath = tempDirectory / "AppInstallerTestExeInstaller.exe"; + std::filesystem::copy(installerPath, tempInstallerPath, std::filesystem::copy_options::overwrite_existing); + context.Add<Data::InstallerPath>(tempInstallerPath); + + std::ifstream inStream{ tempInstallerPath, std::ifstream::binary }; + SHA256::HashBuffer fileHash = SHA256::ComputeHash(inStream); + context.Add<Data::HashPair>({ fileHash, fileHash }); + } }); + + context.Override({ RenameDownloadedInstaller, [](TestContext&) + { + } }); + + OverrideForUpdateInstallerMotw(context); +} + void OverrideForPortableUninstall(TestContext& context) { context.Override({ PortableUninstallImpl, [](TestContext& context) @@ -1252,7 +1275,7 @@ TEST_CASE("MsiInstallFlow_DirectMsi", "[InstallFlow][workflow]") REQUIRE(installResultStr.find("/quiet") != std::string::npos); } -TEST_CASE("PortableInstallFlow", "[InstallFlow][workflow]") +TEST_CASE("InstallFlow_Portable", "[InstallFlow][workflow]") { TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); TestCommon::TempFile portableInstallResultPath("TestPortableInstalled.txt"); @@ -1271,6 +1294,28 @@ TEST_CASE("PortableInstallFlow", "[InstallFlow][workflow]") REQUIRE(std::filesystem::exists(portableInstallResultPath.GetPath())); } +TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]") +{ + std::ostringstream installOutput; + TestContext installContext{ installOutput, std::cin }; + auto PreviousThreadGlobals = installContext.SetForCurrentThread(); + OverridePortableInstaller(installContext); + bool overrideCreateSymlinkStatus = false; + AppInstaller::Filesystem::TestHook_SetCreateSymlinkResult_Override(&overrideCreateSymlinkStatus); + installContext.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Portable.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(installContext); + INFO(installOutput.str()); + + // 'DefaultSource' is expected because we are installing from a local manifest. + const auto& portableUserRoot = AppInstaller::Runtime::GetPathTo(AppInstaller::Runtime::PathName::PortablePackageUserRoot); + const auto& portableTargetDirectory = portableUserRoot / "AppInstallerCliTest.TestPortableInstaller__DefaultSource"; + const auto& portableTargetPath = portableTargetDirectory / "AppInstallerTestExeInstaller.exe"; + REQUIRE(std::filesystem::exists(portableTargetPath)); + REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableTargetDirectory)); +} + TEST_CASE("PortableInstallFlow_UserScope", "[InstallFlow][workflow]") { TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); @@ -1316,39 +1361,6 @@ TEST_CASE("PortableInstallFlow_MachineScope", "[InstallFlow][workflow]") REQUIRE(std::filesystem::exists(portableInstallResultPath.GetPath())); } -TEST_CASE("PortableInstallFlow_DevModeDisabled", "[InstallFlow][workflow]") -{ - if (!AppInstaller::Runtime::IsRunningAsAdmin()) - { - WARN("Test requires admin privilege. Skipped."); - return; - } - - TestCommon::TempDirectory tempDirectory("TestPortableInstallRoot", false); - TestCommon::TempFile portableInstallResultPath("TestPortableInstalled.txt"); - - std::ostringstream installOutput; - TestContext context{ installOutput, std::cin }; - auto previousThreadGlobals = context.SetForCurrentThread(); - TestCommon::EnableDevMode(false); - - // Override admin check to report as false. - context.Override({ EnsureRunningAsAdmin, [](TestContext& testContext) - { - testContext.SetTerminationHR(APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN); - } }); - - context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Portable.yaml").GetPath().u8string()); - InstallCommand install({}); - install.Execute(context); - TestCommon::EnableDevMode(true); - INFO(installOutput.str()); - - // Verify proper message is printed for installing portable in non-developer mode and not running as admin. - REQUIRE_FALSE(std::filesystem::exists(portableInstallResultPath.GetPath())); - REQUIRE(installOutput.str().find("https://github.com/microsoft/winget-cli/issues/2368") != std::string::npos); -} - TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow][workflow]") { { @@ -1964,6 +1976,41 @@ TEST_CASE("UpdateFlow_UpdatePortable", "[UpdateFlow][workflow]") REQUIRE(std::filesystem::exists(updateResultPath.GetPath())); } +TEST_CASE("UpdateFlow_Portable_SymlinkCreationFail", "[UpdateFlow][workflow]") +{ + // Update portable with symlink creation failure verify that it succeeds. + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto PreviousThreadGlobals = context.SetForCurrentThread(); + bool overrideCreateSymlinkStatus = false; + AppInstaller::Filesystem::TestHook_SetCreateSymlinkResult_Override(&overrideCreateSymlinkStatus); + OverridePortableInstaller(context); + OverrideForCompositeInstalledSource(context); + context.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestPortableInstaller"sv); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + const auto& portableTargetDirectory = AppInstaller::Runtime::GetPathTo(AppInstaller::Runtime::PathName::PortablePackageUserRoot) / "AppInstallerCliTest.TestPortableInstaller__TestSource"; + const auto& portableTargetPath = portableTargetDirectory / "AppInstallerTestExeInstaller.exe"; + REQUIRE(std::filesystem::exists(portableTargetPath)); + REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableTargetDirectory)); + + // Perform uninstall + std::ostringstream uninstallOutput; + TestContext uninstallContext{ uninstallOutput, std::cin }; + auto previousThreadGlobals = uninstallContext.SetForCurrentThread(); + OverrideForCompositeInstalledSource(uninstallContext); + uninstallContext.Args.AddArg(Execution::Args::Type::Query, "AppInstallerCliTest.TestPortableInstaller"sv); + + UninstallCommand uninstall({}); + uninstall.Execute(uninstallContext); + INFO(uninstallOutput.str()); + + REQUIRE_FALSE(std::filesystem::exists(portableTargetPath)); + REQUIRE_FALSE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(portableTargetDirectory)); +} + TEST_CASE("UpdateFlow_UpdateExeWithUnsupportedArgs", "[UpdateFlow][workflow]") { TestCommon::TempFile updateResultPath("TestExeInstalled.txt"); diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -327,7 +327,9 @@ <ClInclude Include="Public\winget\Filesystem.h" /> <ClInclude Include="Public\winget\Regex.h" /> <ClInclude Include="Public\winget\Registry.h" /> + <ClInclude Include="Public\winget\PathVariable.h" /> <ClInclude Include="Public\winget\PortableARPEntry.h" /> + <ClInclude Include="Public\winget\PortableEntry.h" /> <ClInclude Include="Public\winget\ManifestSchemaValidation.h" /> <ClInclude Include="Public\winget\Resources.h" /> <ClInclude Include="Public\winget\Settings.h" /> @@ -401,7 +403,9 @@ <ClCompile Include="Archive.cpp" /> <ClCompile Include="Settings.cpp" /> <ClCompile Include="SHA256.cpp" /> + <ClCompile Include="PathVariable.cpp" /> <ClCompile Include="PortableARPEntry.cpp" /> + <ClCompile Include="PortableEntry.cpp" /> <ClCompile Include="Synchronization.cpp" /> <ClCompile Include="Telemetry\TraceLogging.cpp" /> <ClCompile Include="Architecture.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -192,6 +192,9 @@ <ClInclude Include="Public\winget\PortableARPEntry.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\PortableEntry.h"> + <Filter>Public\winget</Filter> + </ClInclude> <ClInclude Include="Public\winget\JsonUtil.h"> <Filter>Public\winget</Filter> </ClInclude> @@ -213,6 +216,9 @@ <ClInclude Include="Public\winget\MsixManifestValidation.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Public\winget\PathVariable.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -356,6 +362,9 @@ <ClCompile Include="PortableARPEntry.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="PortableEntry.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="ManagedFile.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -377,6 +386,9 @@ <ClCompile Include="Manifest\MsixManifestValidation.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClInclude Include="PathVariable.cpp"> + <Filter>Source Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/Filesystem.cpp b/src/AppInstallerCommonCore/Filesystem.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/AppInstallerStrings.h" +#include "winget/Filesystem.h" namespace AppInstaller::Filesystem { @@ -131,4 +132,39 @@ namespace AppInstaller::Filesystem // but it is better to succeed the operation and leave a file around than to fail. std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing); } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static bool* s_CreateSymlinkResult_TestHook_Override = nullptr; + + void TestHook_SetCreateSymlinkResult_Override(bool* status) + { + s_CreateSymlinkResult_TestHook_Override = status; + } +#endif + + bool CreateSymlink(const std::filesystem::path& to, const std::filesystem::path& target) + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_CreateSymlinkResult_TestHook_Override) + { + return *s_CreateSymlinkResult_TestHook_Override; + } +#endif + try + { + std::filesystem::create_symlink(to, target); + return true; + } + catch (std::filesystem::filesystem_error& error) + { + if (error.code().value() == ERROR_PRIVILEGE_NOT_HELD) + { + return false; + } + else + { + throw; + } + } + } } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/PathVariable.cpp b/src/AppInstallerCommonCore/PathVariable.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/PathVariable.h" + +using namespace AppInstaller::Utility; + +namespace AppInstaller::Registry::Environment +{ + namespace + { + constexpr std::wstring_view s_PathName = L"Path"; + constexpr std::wstring_view s_PathSubkey_User = L"Environment"; + constexpr std::wstring_view s_PathSubkey_Machine = L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; + } + + PathVariable::PathVariable(Manifest::ScopeEnum scope) + { + if (scope == Manifest::ScopeEnum::Machine) + { + m_key = Registry::Key::Create(HKEY_LOCAL_MACHINE, std::wstring{ s_PathSubkey_Machine }); + } + else + { + m_key = Registry::Key::Create(HKEY_CURRENT_USER, std::wstring{ s_PathSubkey_User }); + } + } + + std::string PathVariable::GetPathValue() + { + std::wstring pathName = std::wstring{ s_PathName }; + return Normalize(m_key[pathName]->GetValue<Value::Type::String>()); + } + + bool PathVariable::Contains(const std::filesystem::path& target) + { + std::string targetString = Normalize(target.u8string()); + return (GetPathValue().find(targetString) != std::string::npos); + } + + bool PathVariable::Remove(const std::filesystem::path& target) + { + if (Contains(target)) + { + std::string targetString = Normalize(target.u8string()); + std::string pathValue = GetPathValue(); + FindAndReplace(pathValue, targetString, ""); + FindAndReplace(pathValue, ";;", ";"); + SetPathValue(pathValue); + return true; + } + else + { + return false; + } + } + + bool PathVariable::Append(const std::filesystem::path& target) + { + if (!Contains(target)) + { + std::string targetString = Normalize(target.u8string()); + std::string pathValue = GetPathValue(); + if (pathValue.back() != ';') + { + pathValue += ";"; + } + + pathValue += targetString + ";"; + SetPathValue(pathValue); + return true; + } + else + { + return false; + } + } + + void PathVariable::SetPathValue(const std::string& value) + { + std::wstring pathName = std::wstring{ s_PathName }; + m_key.SetValue(pathName, ConvertToUTF16(value), REG_EXPAND_SZ); + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/PortableARPEntry.cpp b/src/AppInstallerCommonCore/PortableARPEntry.cpp @@ -29,6 +29,7 @@ namespace AppInstaller::Registry::Portable constexpr std::wstring_view s_WinGetPackageIdentifier = L"WinGetPackageIdentifier"; constexpr std::wstring_view s_WinGetSourceIdentifier = L"WinGetSourceIdentifier"; constexpr std::wstring_view s_InstallDirectoryCreated = L"InstallDirectoryCreated"; + constexpr std::wstring_view s_InstallDirectoryAddedToPath = L"InstallDirectoryAddedToPath"; } PortableARPEntry::PortableARPEntry(Manifest::ScopeEnum scope, Utility::Architecture arch, const std::string& productCode) @@ -90,6 +91,7 @@ namespace AppInstaller::Registry::Portable VALUENAMECASE(WinGetPackageIdentifier); VALUENAMECASE(WinGetSourceIdentifier); VALUENAMECASE(InstallDirectoryCreated); + VALUENAMECASE(InstallDirectoryAddedToPath); default: return {}; } } diff --git a/src/AppInstallerCommonCore/PortableEntry.cpp b/src/AppInstallerCommonCore/PortableEntry.cpp @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/PortableEntry.h" +#include "winget/PortableARPEntry.h" +#include "winget/Manifest.h" +#include "winget/Filesystem.h" +#include "winget/PathVariable.h" +#include "Public/AppInstallerLogging.h" + +using namespace AppInstaller::Registry; +using namespace AppInstaller::Registry::Portable; +using namespace AppInstaller::Registry::Environment; + +namespace AppInstaller::Portable +{ + PortableEntry::PortableEntry(PortableARPEntry& portableARPEntry) : + m_portableARPEntry(portableARPEntry) + { + // Initialize all values if present + if (Exists()) + { + DisplayName = GetStringValue(PortableValueName::DisplayName); + DisplayVersion = GetStringValue(PortableValueName::DisplayVersion); + HelpLink = GetStringValue(PortableValueName::HelpLink); + InstallDate = GetStringValue(PortableValueName::InstallDate); + Publisher = GetStringValue(PortableValueName::Publisher); + SHA256 = GetStringValue(PortableValueName::SHA256); + URLInfoAbout = GetStringValue(PortableValueName::URLInfoAbout); + UninstallString = GetStringValue(PortableValueName::UninstallString); + WinGetInstallerType = GetStringValue(PortableValueName::WinGetInstallerType); + WinGetPackageIdentifier = GetStringValue(PortableValueName::WinGetPackageIdentifier); + WinGetSourceIdentifier = GetStringValue(PortableValueName::WinGetSourceIdentifier); + + InstallLocation = GetPathValue(PortableValueName::InstallLocation); + PortableSymlinkFullPath = GetPathValue(PortableValueName::PortableSymlinkFullPath); + PortableTargetFullPath = GetPathValue(PortableValueName::PortableTargetFullPath); + InstallLocation = GetPathValue(PortableValueName::InstallLocation); + + InstallDirectoryCreated = GetBoolValue(PortableValueName::InstallDirectoryCreated); + InstallDirectoryAddedToPath = GetBoolValue(PortableValueName::InstallDirectoryAddedToPath); + } + } + + void PortableEntry::MovePortableExe(const std::filesystem::path& installerPath) + { + bool isDirectoryCreated = false; + if (std::filesystem::create_directories(InstallLocation)) + { + AICLI_LOG(Core, Info, << "Created target install directory: " << InstallLocation); + isDirectoryCreated = true; + } + + if (std::filesystem::exists(PortableTargetFullPath)) + { + std::filesystem::remove(PortableTargetFullPath); + AICLI_LOG(Core, Info, << "Removing existing portable exe at: " << PortableTargetFullPath); + } + + Filesystem::RenameFile(installerPath, PortableTargetFullPath); + AICLI_LOG(Core, Info, << "Portable exe moved to: " << PortableTargetFullPath); + + // Only assign this value if this is a new portable install or the install directory was actually created. + // Otherwise, we want to preserve the existing value from the prior install. + if (!Exists() || isDirectoryCreated) + { + Commit(PortableValueName::InstallDirectoryCreated, InstallDirectoryCreated = isDirectoryCreated); + } + } + + bool PortableEntry::VerifyPortableExeHash() + { + std::ifstream inStream{ PortableTargetFullPath, std::ifstream::binary }; + const Utility::SHA256::HashBuffer& targetFileHash = Utility::SHA256::ComputeHash(inStream); + inStream.close(); + + return Utility::SHA256::AreEqual(Utility::SHA256::ConvertToBytes(SHA256), targetFileHash); + } + + void PortableEntry::CreatePortableSymlink() + { + if (Filesystem::CreateSymlink(PortableTargetFullPath, PortableSymlinkFullPath)) + { + AICLI_LOG(Core, Info, << "Symlink created at: " << PortableSymlinkFullPath); + } + else + { + // Symlink creation should only fail if the user executes in user mode and non-admin. + // Resort to adding install directory to PATH directly. + AICLI_LOG(Core, Info, << "Portable install executed in user mode. Adding package directory to PATH."); + Commit(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true); + } + } + + bool PortableEntry::VerifySymlinkTarget() + { + AICLI_LOG(Core, Info, << "Expected portable target path: " << PortableTargetFullPath); + const std::filesystem::path& symlinkTargetPath = std::filesystem::read_symlink(PortableSymlinkFullPath); + + if (symlinkTargetPath == PortableTargetFullPath) + { + AICLI_LOG(Core, Info, << "Portable symlink target matches portable target path: " << symlinkTargetPath); + return true; + } + else + { + AICLI_LOG(Core, Info, << "Portable symlink does not match portable target path: " << symlinkTargetPath); + return false; + } + } + + bool PortableEntry::AddToPathVariable() + { + return PathVariable(GetScope()).Append(GetPathValue()); + } + + bool PortableEntry::RemoveFromPathVariable() + { + bool removeFromPath = true; + std::filesystem::path pathValue = GetPathValue(); + if (!InstallDirectoryAddedToPath) + { + // Default links directory must be empty before removing from PATH. + if (!std::filesystem::is_empty(pathValue)) + { + AICLI_LOG(Core, Info, << "Install directory is not empty: " << pathValue); + removeFromPath = false; + } + } + + if (removeFromPath) + { + return PathVariable(GetScope()).Remove(pathValue); + } + else + { + return false; + } + } + + void PortableEntry::RemoveARPEntry() + { + m_portableARPEntry.Delete(); + } + + std::string PortableEntry::GetStringValue(PortableValueName valueName) + { + if (m_portableARPEntry[valueName].has_value()) + { + return m_portableARPEntry[valueName]->GetValue<Value::Type::String>(); + } + else + { + return {}; + } + } + + std::filesystem::path PortableEntry::GetPathValue(PortableValueName valueName) + { + if (m_portableARPEntry[valueName].has_value()) + { + return m_portableARPEntry[valueName]->GetValue<Value::Type::UTF16String>(); + } + { + return {}; + } + } + + bool PortableEntry::GetBoolValue(PortableValueName valueName) + { + if (m_portableARPEntry[valueName].has_value()) + { + return m_portableARPEntry[valueName]->GetValue<Value::Type::DWord>(); + } + else + { + return false; + } + } +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Filesystem.h b/src/AppInstallerCommonCore/Public/winget/Filesystem.h @@ -19,4 +19,7 @@ namespace AppInstaller::Filesystem // Renames the file to a new path. void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to); + + // Creates a symlink that points to the target path. + bool CreateSymlink(const std::filesystem::path& path, const std::filesystem::path& target); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PathVariable.h b/src/AppInstallerCommonCore/Public/winget/PathVariable.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "winget/Registry.h" +#include "winget/Manifest.h" + +namespace AppInstaller::Registry::Environment +{ + struct PathVariable + { + PathVariable(Manifest::ScopeEnum scope); + + // Returns the PATH variable as a string. + std::string GetPathValue(); + + // Checks if the PATH variable contains the target path. + bool Contains(const std::filesystem::path& target); + + // Returns a value indicating whether the target path was removed from the PATH variable. + bool Remove(const std::filesystem::path& target); + + // Returns a value indicating whether the target path was appended to the PATH variable. + bool Append(const std::filesystem::path& target); + + private: + void SetPathValue(const std::string& value); + Registry::Key m_key; + HKEY m_root; + Manifest::ScopeEnum m_scope; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h @@ -23,6 +23,7 @@ namespace AppInstaller::Registry::Portable WinGetInstallerType, WinGetPackageIdentifier, WinGetSourceIdentifier, + InstallDirectoryAddedToPath, }; std::wstring_view ToString(PortableValueName valueName); diff --git a/src/AppInstallerCommonCore/Public/winget/PortableEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableEntry.h @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerStrings.h> +#include <AppInstallerSHA256.h> +#include "winget/PortableARPEntry.h" +#include <filesystem> +#include <optional> + +using namespace AppInstaller::Registry; +using namespace AppInstaller::Registry::Portable; + +namespace AppInstaller::Portable +{ + struct PortableEntry + { + std::string DisplayName; + std::string DisplayVersion; + std::string HelpLink; + std::string InstallDate; + bool InstallDirectoryCreated = false; + std::filesystem::path InstallLocation; + std::filesystem::path PortableSymlinkFullPath; + std::filesystem::path PortableTargetFullPath; + std::string Publisher; + std::string SHA256; + std::string URLInfoAbout; + std::string UninstallString; + std::string WinGetInstallerType; + std::string WinGetPackageIdentifier; + std::string WinGetSourceIdentifier; + bool InstallDirectoryAddedToPath = false; + + template<typename T> + void Commit(PortableValueName valueName, T value) + { + m_portableARPEntry.SetValue(valueName, value); + } + + Manifest::ScopeEnum GetScope() { return m_portableARPEntry.GetScope(); }; + + bool Exists() { return m_portableARPEntry.Exists(); }; + + PortableEntry(PortableARPEntry& portableARPEntry); + + std::filesystem::path GetPathValue() const + { + return InstallDirectoryAddedToPath ? InstallLocation : PortableSymlinkFullPath.parent_path(); + } + + bool VerifyPortableExeHash(); + + bool VerifySymlinkTarget(); + + void MovePortableExe(const std::filesystem::path& installerPath); + + void CreatePortableSymlink(); + + bool AddToPathVariable(); + + bool RemoveFromPathVariable(); + + void RemoveARPEntry(); + + private: + PortableARPEntry m_portableARPEntry; + std::string GetStringValue(PortableValueName valueName); + std::filesystem::path GetPathValue(PortableValueName valueName); + bool GetBoolValue(PortableValueName valueName); + }; +}+ \ No newline at end of file