diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 79ba88574a..6163cccbff 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -385,6 +385,7 @@ regex regexp removemanifest repolibtest +requeue rescap resheader resmimetype diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index 6156f71782..8fb56be603 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -275,6 +275,7 @@ + @@ -324,6 +325,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 23bf4b6e8a..35e3bed028 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -170,6 +170,9 @@ Workflows + + Workflows + @@ -307,6 +310,9 @@ Workflows + + Workflows + diff --git a/src/AppInstallerCLICore/Commands/COMInstallCommand.cpp b/src/AppInstallerCLICore/Commands/COMInstallCommand.cpp index 9235cbcb32..2ebc86d3d4 100644 --- a/src/AppInstallerCLICore/Commands/COMInstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/COMInstallCommand.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "COMInstallCommand.h" +#include "Workflows/DownloadFlow.h" #include "Workflows/InstallFlow.h" #include "Workflows/WorkflowBase.h" @@ -13,12 +14,21 @@ using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI { // IMPORTANT: To use this command, the caller should have already retrieved the package manifest (GetManifest()) and added it to the Context Data - void COMInstallCommand::ExecuteInternal(Context& context) const + void COMDownloadCommand::ExecuteInternal(Context& context) const { context << Workflow::ReportExecutionStage(ExecutionStage::Discovery) << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << - Workflow::InstallSinglePackage; + Workflow::DownloadSinglePackage; + } + + // IMPORTANT: To use this command, the caller should have already executed the COMDownloadCommand + void COMInstallCommand::ExecuteInternal(Context& context) const + { + context << + Workflow::GetInstallerHash << + Workflow::VerifyInstallerHash << + Workflow::InstallPackageInstaller; } } diff --git a/src/AppInstallerCLICore/Commands/COMInstallCommand.h b/src/AppInstallerCLICore/Commands/COMInstallCommand.h index 46ad3498a2..f454f4e5b1 100644 --- a/src/AppInstallerCLICore/Commands/COMInstallCommand.h +++ b/src/AppInstallerCLICore/Commands/COMInstallCommand.h @@ -5,6 +5,15 @@ namespace AppInstaller::CLI { + // IMPORTANT: To use this command, the caller should have already retrieved the package manifest (GetManifest()) and added it to the Context Data + struct COMDownloadCommand final : public Command + { + COMDownloadCommand(std::string_view parent) : Command("download", parent) {} + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; + // IMPORTANT: To use this command, the caller should have already retrieved the package manifest (GetManifest()) and added it to the Context Data struct COMInstallCommand final : public Command { diff --git a/src/AppInstallerCLICore/Commands/RootCommand.h b/src/AppInstallerCLICore/Commands/RootCommand.h index 2f7a38e1e9..87d051eee6 100644 --- a/src/AppInstallerCLICore/Commands/RootCommand.h +++ b/src/AppInstallerCLICore/Commands/RootCommand.h @@ -7,7 +7,9 @@ namespace AppInstaller::CLI { struct RootCommand final : public Command { - RootCommand() : Command("root", {}) {} + constexpr static std::string_view CommandName = "root"sv; + + RootCommand() : Command(CommandName, {}) {} std::vector> GetCommands() const override; std::vector GetArguments() const override; diff --git a/src/AppInstallerCLICore/ContextOrchestrator.cpp b/src/AppInstallerCLICore/ContextOrchestrator.cpp index f9876d4bcb..b8f0c01216 100644 --- a/src/AppInstallerCLICore/ContextOrchestrator.cpp +++ b/src/AppInstallerCLICore/ContextOrchestrator.cpp @@ -59,6 +59,13 @@ namespace AppInstaller::CLI::Execution } } + void ContextOrchestrator::RequeueItem(OrchestratorQueueItem& item) + { + std::lock_guard lockQueue{ m_queueLock }; + + item.SetState(OrchestratorQueueItemState::Queued); + } + void ContextOrchestrator::EnqueueAndRunItem(std::shared_ptr item) { EnqueueItem(item); @@ -101,11 +108,10 @@ namespace AppInstaller::CLI::Execution HRESULT terminationHR = S_OK; try { - ::AppInstaller::CLI::RootCommand rootCommand; + std::unique_ptr command = item->PopNextCommand(); std::unique_ptr setThreadGlobalsToPreviousState = item->GetContext().GetThreadGlobals().SetForCurrentThread(); - std::unique_ptr<::AppInstaller::CLI::Command> command = std::make_unique<::AppInstaller::CLI::COMInstallCommand>(rootCommand.Name()); item->GetContext().GetThreadGlobals().GetTelemetryLogger().LogCommand(command->FullName()); command->ValidateArguments(item->GetContext().Args); @@ -123,7 +129,16 @@ namespace AppInstaller::CLI::Execution item->GetContext().SetTerminationHR(terminationHR); } - RemoveItemInState(*item, OrchestratorQueueItemState::Running); + item->GetContext().EnableCtrlHandler(false); + + if (FAILED(terminationHR) || item->IsComplete()) + { + RemoveItemInState(*item, OrchestratorQueueItemState::Running); + } + else + { + RequeueItem(*item); + } item = GetNextItem(); } @@ -180,7 +195,10 @@ namespace AppInstaller::CLI::Execution std::unique_ptr OrchestratorQueueItemFactory::CreateItemForInstall(std::wstring packageId, std::wstring sourceId, std::unique_ptr context) { - return std::make_unique(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context)); + std::unique_ptr item = std::make_unique(OrchestratorQueueItemId(std::move(packageId), std::move(sourceId)), std::move(context)); + item->AddCommand(std::make_unique<::AppInstaller::CLI::COMDownloadCommand>(RootCommand::CommandName)); + item->AddCommand(std::make_unique<::AppInstaller::CLI::COMInstallCommand>(RootCommand::CommandName)); + return item; } } diff --git a/src/AppInstallerCLICore/ContextOrchestrator.h b/src/AppInstallerCLICore/ContextOrchestrator.h index e45d8a2946..1123e10831 100644 --- a/src/AppInstallerCLICore/ContextOrchestrator.h +++ b/src/AppInstallerCLICore/ContextOrchestrator.h @@ -6,6 +6,7 @@ #include "ExecutionArgs.h" #include "ExecutionContextData.h" #include "CompletionData.h" +#include "Command.h" #include "COMContext.h" #include @@ -40,11 +41,20 @@ namespace AppInstaller::CLI::Execution COMContext& GetContext() const { return *m_context; } const wil::unique_event& GetCompletedEvent() const { return m_completedEvent; } const OrchestratorQueueItemId& GetId() const { return m_id; } + void AddCommand(std::unique_ptr command) { m_commands.push_back(std::move(command)); } + std::unique_ptr PopNextCommand() + { + std::unique_ptr command = std::move(m_commands.front()); + m_commands.pop_front(); + return command; + } + bool IsComplete() const { return m_commands.empty(); } private: OrchestratorQueueItemState m_state = OrchestratorQueueItemState::NotQueued; std::unique_ptr m_context; wil::unique_event m_completedEvent{ wil::EventOptions::ManualReset }; OrchestratorQueueItemId m_id; + std::deque> m_commands; }; struct OrchestratorQueueItemFactory @@ -67,6 +77,7 @@ namespace AppInstaller::CLI::Execution void RunItems(); std::shared_ptr GetNextItem(); void EnqueueItem(std::shared_ptr item); + void RequeueItem(OrchestratorQueueItem& item); void RemoveItemInState(const OrchestratorQueueItem& item, OrchestratorQueueItemState state); _Requires_lock_held_(m_queueLock) diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp new file mode 100644 index 0000000000..031e810cc9 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -0,0 +1,496 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DownloadFlow.h" + +#include + +namespace AppInstaller::CLI::Workflow +{ + using namespace AppInstaller::Manifest; + using namespace AppInstaller::Repository; + using namespace AppInstaller::Utility; + using namespace std::string_view_literals; + + namespace + { + // Get the base download path for the installer path. + // This path does not include the file extension, which will be added + // after verifying the file hash to prevent it from being ShellExecute-d + std::filesystem::path GetInstallerBaseDownloadPath(Execution::Context& context) + { + const auto& manifest = context.Get(); + std::filesystem::path tempInstallerPath = Runtime::GetPathTo(Runtime::PathName::Temp); + tempInstallerPath /= Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); + return tempInstallerPath; + } + + // Get the file extension to be used for the installer file. + std::wstring_view GetInstallerFileExtension(Execution::Context& context) + { + const auto& installer = context.Get(); + switch (installer->InstallerType) + { + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Nullsoft: + return L".exe"sv; + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Wix: + return L".msi"sv; + case InstallerTypeEnum::Msix: + // Note: We may need to distinguish between .msix and .msixbundle in the future. + return L".msix"sv; + case InstallerTypeEnum::Zip: + return L".zip"sv; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + // Try to remove the installer file, ignoring any errors. + void RemoveInstallerFile(const std::filesystem::path& path) + { + try + { + std::filesystem::remove(path); + } + catch (const std::exception& e) + { + AICLI_LOG(CLI, Warning, << "Failed to remove installer file. Reason: " << e.what()); + } + catch (...) + { + AICLI_LOG(CLI, Warning, << "Failed to remove installer file. Reason unknown."); + } + + } + + // Checks the file hash for an existing installer file. + // Returns true if the file exists and its hash matches, false otherwise. + // If the hash does not match, deletes the file. + bool ExistingInstallerFileHasHashMatch(const SHA256::HashBuffer& expectedHash, const std::filesystem::path& filePath, SHA256::HashBuffer& fileHash) + { + if (std::filesystem::exists(filePath)) + { + AICLI_LOG(CLI, Info, << "Found existing installer file at '" << filePath << "'. Verifying file hash."); + std::ifstream inStream{ filePath, std::ifstream::binary }; + fileHash = SHA256::ComputeHash(inStream); + + if (SHA256::AreEqual(expectedHash, fileHash)) + { + return true; + } + + AICLI_LOG(CLI, Info, << "Hash does not match. Removing existing installer file " << filePath); + RemoveInstallerFile(filePath); + } + + return false; + } + + // Complicated rename algorithm due to somewhat arbitrary failures. + // 1. First, try to rename. + // 2. Then, create an empty file for the target, and attempt to rename. + // 3. Then, try repeatedly for 500ms in case it is a timing thing. + // 4. Attempt to use a hard link if available. + // 5. Copy the file if nothing else has worked so far. + void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to) + { + // 1. First, try to rename. + try + { + // std::filesystem::rename() handles motw correctly if applicable. + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + + // 2. Then, create an empty file for the target, and attempt to rename. + // This seems to fix things in certain cases, so we do it. + try + { + { + std::ofstream targetFile{ to }; + } + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + + // 3. Then, try repeatedly for 500ms in case it is a timing thing. + for (int i = 0; i < 5; ++i) + { + try + { + std::this_thread::sleep_for(100ms); + std::filesystem::rename(from, to); + return; + } + CATCH_LOG(); + } + + // 4. Attempt to use a hard link if available. + if (Runtime::SupportsHardLinks(from)) + { + try + { + // Create a hard link to the file; the installer will be left in the temp directory afterward + // but it is better to succeed the operation and leave a file around than to fail. + // First we have to remove the target file as the function will not overwrite. + std::filesystem::remove(to); + std::filesystem::create_hard_link(from, to); + return; + } + CATCH_LOG(); + } + + // 5. Copy the file if nothing else has worked so far. + // Create a copy of the file; the installer will be left in the temp directory afterward + // 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); + } + } + + void DownloadInstaller(Execution::Context& context) + { + // Check if file was already downloaded. + // This may happen after a failed installation or if the download was done + // separately before, e.g. on COM scenarios. + context << + ReportExecutionStage(ExecutionStage::Download) << + CheckForExistingInstaller; + if (context.IsTerminated()) + { + return; + } + + // CheckForExistingInstaller will set the InstallerPath if found + if (!context.Contains(Execution::Data::InstallerPath)) + { + const auto& installer = context.Get().value(); + switch (installer.InstallerType) + { + case InstallerTypeEnum::Exe: + case InstallerTypeEnum::Burn: + case InstallerTypeEnum::Inno: + case InstallerTypeEnum::Msi: + case InstallerTypeEnum::Nullsoft: + case InstallerTypeEnum::Wix: + context << DownloadInstallerFile; + break; + case InstallerTypeEnum::Msix: + if (installer.SignatureSha256.empty()) + { + context << DownloadInstallerFile; + } + else + { + // Signature hash provided. No download needed. Just verify signature hash. + context << GetMsixSignatureHash; + } + break; + case InstallerTypeEnum::MSStore: + // Nothing to do here + return; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + } + + context << + VerifyInstallerHash << + UpdateInstallerFileMotwIfApplicable << + RenameDownloadedInstaller; + } + + void CheckForExistingInstaller(Execution::Context& context) + { + const auto& installer = context.Get().value(); + if (installer.InstallerType == InstallerTypeEnum::MSStore) + { + // No installer is downloaded in this case + return; + } + + // Try looking for the file with and without extension. + auto installerPath = GetInstallerBaseDownloadPath(context); + SHA256::HashBuffer fileHash; + if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath, fileHash)) + { + installerPath += GetInstallerFileExtension(context); + if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath, fileHash)) + { + // No match + return; + } + } + + AICLI_LOG(CLI, Info, << "Existing installer file hash matches. Will use existing installer."); + context.Add(installerPath); + context.Add(std::make_pair(installer.Sha256, fileHash)); + } + + void GetInstallerDownloadPath(Execution::Context& context) + { + if (!context.Contains(Execution::Data::InstallerPath)) + { + auto tempInstallerPath = GetInstallerBaseDownloadPath(context); + AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); + context.Add(std::move(tempInstallerPath)); + } + } + + void DownloadInstallerFile(Execution::Context& context) + { + context << GetInstallerDownloadPath; + if (context.IsTerminated()) + { + return; + } + + const auto& installer = context.Get().value(); + const auto& installerPath = context.Get(); + + Utility::DownloadInfo downloadInfo{}; + downloadInfo.DisplayName = Resource::GetFixedString(Resource::FixedString::ProductName); + // Use the SHA256 hash of the installer as the identifier for the download + downloadInfo.ContentId = SHA256::ConvertToString(installer.Sha256); + + context.Reporter.Info() << "Downloading " << Execution::UrlEmphasis << installer.Url << std::endl; + + std::optional> hash; + + const int MaxRetryCount = 2; + for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) + { + bool success = false; + try + { + hash = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, + installer.Url, + installerPath, + Utility::DownloadType::Installer, + std::placeholders::_1, + true, + downloadInfo)); + + success = true; + } + catch (...) + { + if (retryCount < MaxRetryCount - 1) + { + AICLI_LOG(CLI, Info, << "Failed to download, waiting a bit and retry. Url: " << installer.Url); + Sleep(500); + } + else + { + throw; + } + } + + if (success) + { + break; + } + } + + if (!hash) + { + context.Reporter.Info() << "Package download canceled." << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + + context.Add(std::make_pair(installer.Sha256, hash.value())); + } + + void GetMsixSignatureHash(Execution::Context& context) + { + // We use this when the server won't support streaming install to swap to download. + bool downloadInstead = false; + + try + { + const auto& installer = context.Get().value(); + + Msix::MsixInfo msixInfo(installer.Url); + auto signature = msixInfo.GetSignature(); + + auto signatureHash = SHA256::ComputeHash(signature.data(), static_cast(signature.size())); + + context.Add(std::make_pair(installer.SignatureSha256, signatureHash)); + } + catch (const winrt::hresult_error& e) + { + if (static_cast(e.code()) == HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED) || + HRESULT_FACILITY(e.code()) == FACILITY_HTTP) + { + // Failed to get signature hash through HttpStream, use download + downloadInstead = true; + } + else + { + throw; + } + } + + if (downloadInstead) + { + context << DownloadInstallerFile; + } + } + + void VerifyInstallerHash(Execution::Context& context) + { + const auto& hashPair = context.Get(); + + if (!std::equal( + hashPair.first.begin(), + hashPair.first.end(), + hashPair.second.begin())) + { + bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); + + const auto& manifest = context.Get(); + Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, hashPair.first, hashPair.second, overrideHashMismatch); + + // If running as admin, do not allow the user to override the hash failure. + if (Runtime::IsRunningAsAdmin()) + { + context.Reporter.Error() << Resource::String::InstallerHashMismatchAdminBlock << std::endl; + } + else if (overrideHashMismatch) + { + context.Reporter.Warn() << Resource::String::InstallerHashMismatchOverridden << std::endl; + return; + } + else if (Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::HashOverride)) + { + context.Reporter.Error() << Resource::String::InstallerHashMismatchOverrideRequired << std::endl; + } + else + { + context.Reporter.Error() << Resource::String::InstallerHashMismatchError << std::endl; + } + + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH); + } + else + { + AICLI_LOG(CLI, Info, << "Installer hash verified"); + context.Reporter.Info() << Resource::String::InstallerHashVerified << std::endl; + + context.SetFlags(Execution::ContextFlag::InstallerHashMatched); + + if (context.Contains(Execution::Data::PackageVersion) && + context.Get()->GetSource() != nullptr && + WI_IsFlagSet(context.Get()->GetSource()->GetDetails().TrustLevel, SourceTrustLevel::Trusted)) + { + context.SetFlags(Execution::ContextFlag::InstallerTrusted); + } + } + } + + void UpdateInstallerFileMotwIfApplicable(Execution::Context& context) + { + if (context.Contains(Execution::Data::InstallerPath)) + { + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerTrusted)) + { + Utility::ApplyMotwIfApplicable(context.Get(), URLZONE_TRUSTED); + } + else if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerHashMatched)) + { + const auto& installer = context.Get(); + HRESULT hr = Utility::ApplyMotwUsingIAttachmentExecuteIfApplicable(context.Get(), installer.value().Url, URLZONE_INTERNET); + + // Not using SUCCEEDED(hr) to check since there are cases file is missing after a successful scan + if (hr != S_OK) + { + switch (hr) + { + case INET_E_SECURITY_PROBLEM: + context.Reporter.Error() << Resource::String::InstallerBlockedByPolicy << std::endl; + break; + case E_FAIL: + context.Reporter.Error() << Resource::String::InstallerFailedVirusScan << std::endl; + break; + default: + context.Reporter.Error() << Resource::String::InstallerFailedSecurityCheck << std::endl; + } + + AICLI_LOG(Fail, Error, << "Installer failed security check. Url: " << installer.value().Url << " Result: " << WINGET_OSTREAM_FORMAT_HRESULT(hr)); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED); + } + } + } + } + + void GetInstallerHash(Execution::Context& context) + { + const auto& installer = context.Get().value(); + + if (context.Contains(Execution::Data::InstallerPath)) + { + // Get the hash from the installer file + const auto& installerPath = context.Get(); + std::ifstream inStream{ installerPath, std::ifstream::binary }; + auto existingFileHash = SHA256::ComputeHash(inStream); + context.Add(std::make_pair(installer.Sha256, existingFileHash)); + } + else if (installer.InstallerType == InstallerTypeEnum::MSStore) + { + // No installer file in this case + return; + } + else if (installer.InstallerType == InstallerTypeEnum::Msix && !installer.SignatureSha256.empty()) + { + // We didn't download the installer file before. Just verify the signature hash again. + context << GetMsixSignatureHash; + } + else + { + // No installer downloaded + AICLI_LOG(CLI, Error, << "Installer file not found."); + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + } + } + + void RenameDownloadedInstaller(Execution::Context& context) + { + if (!context.Contains(Execution::Data::InstallerPath)) + { + // No installer downloaded, no need to rename anything. + return; + } + + auto& installerPath = context.Get(); + auto installerExtension = GetInstallerFileExtension(context); + if (installerPath.extension() == installerExtension) + { + // Installer file already has expected extension. + return; + } + + std::filesystem::path renamedDownloadedInstaller(installerPath); + renamedDownloadedInstaller += installerExtension; + + RenameFile(installerPath, renamedDownloadedInstaller); + + installerPath.assign(renamedDownloadedInstaller); + AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); + } + + void RemoveInstaller(Execution::Context& context) + { + // Path may not be present if installed from a URL for MSIX + if (context.Contains(Execution::Data::InstallerPath)) + { + const auto& path = context.Get(); + AICLI_LOG(CLI, Info, << "Removing installer: " << path); + RemoveInstallerFile(path); + } + } +} diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.h b/src/AppInstallerCLICore/Workflows/DownloadFlow.h new file mode 100644 index 0000000000..744de69467 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/DownloadFlow.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Composite flow that chooses what to do based on the installer type. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: None + void DownloadInstaller(Execution::Context& context); + + // Check if the desired installer has already been downloaded. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: HashPair, InstallerPath (only if found) + void CheckForExistingInstaller(Execution::Context& context); + + // Computes the download path for the installer file. Does nothing if already determined + // Required Args: None + // Inputs: Installer, Manifest + // Outputs: InstallerPath + void GetInstallerDownloadPath(Execution::Context& context); + + // Downloads the file referenced by the Installer. + // Required Args: None + // Inputs: Installer, Manifest + // Outputs: HashPair, InstallerPath + void DownloadInstallerFile(Execution::Context& context); + + // Computes the hash of the MSIX signature file. + // Required Args: None + // Inputs: Installer + // Outputs: HashPair + void GetMsixSignatureHash(Execution::Context& context); + + // Gets the hash of the downloaded installer. + // Downloading already computes the hash, so this is only needed to re-verify the installer hash. + // Required Args: None + // Inputs: InstallerPath, Installer + // Outputs: HashPair + void GetInstallerHash(Execution::Context& context); + + // Verifies that the downloaded installer hash matches the hash in the manifest. + // Required Args: None + // Inputs: HashPair + // Outputs: None + void VerifyInstallerHash(Execution::Context& context); + + // Update Motw of the downloaded installer if applicable + // Required Args: None + // Inputs: HashPair, InstallerPath?, SourceId? + // Outputs: None + void UpdateInstallerFileMotwIfApplicable(Execution::Context& context); + + // This method appends appropriate extension to the downloaded installer. + // ShellExecute uses file extension to launch the installer appropriately. + // Required Args: None + // Inputs: Installer, InstallerPath + // Modifies: InstallerPath + // Outputs: None + void RenameDownloadedInstaller(Execution::Context& context); + + // Deletes the installer file. + // Required Args: None + // Inputs: InstallerPath + // Outputs: None + void RemoveInstaller(Execution::Context& context); +} diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp index b82da0a59f..40a2bee916 100644 --- 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 "DownloadFlow.h" #include "UninstallFlow.h" #include "ShowFlow.h" #include "Resources.h" @@ -12,7 +13,6 @@ #include "Workflows/DependenciesFlow.h" #include -#include namespace AppInstaller::CLI::Workflow { @@ -20,7 +20,6 @@ namespace AppInstaller::CLI::Workflow using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Foundation::Collections; using namespace winrt::Windows::Management::Deployment; - using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Settings; @@ -214,225 +213,6 @@ namespace AppInstaller::CLI::Workflow } } - void DownloadInstaller(Execution::Context& context) - { - const auto& installer = context.Get().value(); - - switch (installer.InstallerType) - { - case InstallerTypeEnum::Exe: - case InstallerTypeEnum::Burn: - case InstallerTypeEnum::Inno: - case InstallerTypeEnum::Msi: - case InstallerTypeEnum::Nullsoft: - case InstallerTypeEnum::Wix: - context << DownloadInstallerFile << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; - break; - case InstallerTypeEnum::Msix: - if (installer.SignatureSha256.empty()) - { - context << DownloadInstallerFile << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; - } - else - { - // Signature hash provided. No download needed. Just verify signature hash. - context << GetMsixSignatureHash << VerifyInstallerHash << UpdateInstallerFileMotwIfApplicable; - } - break; - case InstallerTypeEnum::MSStore: - // Nothing to do here - break; - default: - THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); - } - } - - void DownloadInstallerFile(Execution::Context& context) - { - const auto& manifest = context.Get(); - const auto& installer = context.Get().value(); - - std::filesystem::path tempInstallerPath = Runtime::GetPathTo(Runtime::PathName::Temp); - tempInstallerPath /= Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); - - Utility::DownloadInfo downloadInfo{}; - downloadInfo.DisplayName = Resource::GetFixedString(Resource::FixedString::ProductName); - // Use the SHA256 hash of the installer as the identifier for the download - downloadInfo.ContentId = SHA256::ConvertToString(installer.Sha256); - - AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); - - context.Reporter.Info() << "Downloading " << Execution::UrlEmphasis << installer.Url << std::endl; - - std::optional> hash; - - const int MaxRetryCount = 2; - for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) - { - bool success = false; - try - { - hash = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, - installer.Url, - tempInstallerPath, - Utility::DownloadType::Installer, - std::placeholders::_1, - true, - downloadInfo)); - - success = true; - } - catch (...) - { - if (retryCount < MaxRetryCount - 1) - { - AICLI_LOG(CLI, Info, << "Failed to download, waiting a bit and retry. Url: " << installer.Url); - Sleep(500); - } - else - { - throw; - } - } - - if (success) - { - break; - } - } - - if (!hash) - { - context.Reporter.Info() << "Package download canceled." << std::endl; - AICLI_TERMINATE_CONTEXT(E_ABORT); - } - - context.Add(std::make_pair(installer.Sha256, hash.value())); - context.Add(std::move(tempInstallerPath)); - } - - void GetMsixSignatureHash(Execution::Context& context) - { - // We use this when the server won't support streaming install to swap to download. - bool downloadInstead = false; - - try - { - const auto& installer = context.Get().value(); - - Msix::MsixInfo msixInfo(installer.Url); - auto signature = msixInfo.GetSignature(); - - auto signatureHash = SHA256::ComputeHash(signature.data(), static_cast(signature.size())); - - context.Add(std::make_pair(installer.SignatureSha256, signatureHash)); - } - catch (const winrt::hresult_error& e) - { - if (static_cast(e.code()) == HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED) || - HRESULT_FACILITY(e.code()) == FACILITY_HTTP) - { - // Failed to get signature hash through HttpStream, use download - downloadInstead = true; - } - else - { - throw; - } - } - - if (downloadInstead) - { - context << DownloadInstallerFile; - } - } - - void VerifyInstallerHash(Execution::Context& context) - { - const auto& hashPair = context.Get(); - - if (!std::equal( - hashPair.first.begin(), - hashPair.first.end(), - hashPair.second.begin())) - { - bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); - - const auto& manifest = context.Get(); - Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, hashPair.first, hashPair.second, overrideHashMismatch); - - // If running as admin, do not allow the user to override the hash failure. - if (Runtime::IsRunningAsAdmin()) - { - context.Reporter.Error() << Resource::String::InstallerHashMismatchAdminBlock << std::endl; - } - else if (overrideHashMismatch) - { - context.Reporter.Warn() << Resource::String::InstallerHashMismatchOverridden << std::endl; - return; - } - else if (Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::HashOverride)) - { - context.Reporter.Error() << Resource::String::InstallerHashMismatchOverrideRequired << std::endl; - } - else - { - context.Reporter.Error() << Resource::String::InstallerHashMismatchError << std::endl; - } - - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH); - } - else - { - AICLI_LOG(CLI, Info, << "Installer hash verified"); - context.Reporter.Info() << Resource::String::InstallerHashVerified << std::endl; - - context.SetFlags(Execution::ContextFlag::InstallerHashMatched); - - if (context.Contains(Execution::Data::PackageVersion) && - context.Get()->GetSource() != nullptr && - WI_IsFlagSet(context.Get()->GetSource()->GetDetails().TrustLevel, SourceTrustLevel::Trusted)) - { - context.SetFlags(Execution::ContextFlag::InstallerTrusted); - } - } - } - - void UpdateInstallerFileMotwIfApplicable(Execution::Context& context) - { - if (context.Contains(Execution::Data::InstallerPath)) - { - if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerTrusted)) - { - Utility::ApplyMotwIfApplicable(context.Get(), URLZONE_TRUSTED); - } - else if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerHashMatched)) - { - const auto& installer = context.Get(); - HRESULT hr = Utility::ApplyMotwUsingIAttachmentExecuteIfApplicable(context.Get(), installer.value().Url, URLZONE_INTERNET); - - // Not using SUCCEEDED(hr) to check since there are cases file is missing after a successful scan - if (hr != S_OK) - { - switch (hr) - { - case INET_E_SECURITY_PROBLEM: - context.Reporter.Error() << Resource::String::InstallerBlockedByPolicy << std::endl; - break; - case E_FAIL: - context.Reporter.Error() << Resource::String::InstallerFailedVirusScan << std::endl; - break; - default: - context.Reporter.Error() << Resource::String::InstallerFailedSecurityCheck << std::endl; - } - - AICLI_LOG(Fail, Error, << "Installer failed security check. Url: " << installer.value().Url << " Result: " << WINGET_OSTREAM_FORMAT_HRESULT(hr)); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED); - } - } - } - } - void ExecuteInstaller(Execution::Context& context) { const auto& installer = context.Get().value(); @@ -480,7 +260,6 @@ namespace AppInstaller::CLI::Workflow { context << GetInstallerArgs << - RenameDownloadedInstaller << ShellExecuteInstallImpl << ReportInstallerResult("ShellExecute"sv, APPINSTALLER_CLI_ERROR_SHELLEXEC_INSTALL_FAILED); } @@ -489,7 +268,6 @@ namespace AppInstaller::CLI::Workflow { context << GetInstallerArgs << - RenameDownloadedInstaller << DirectMSIInstallImpl << ReportInstallerResult("MsiInstallProduct"sv, APPINSTALLER_CLI_ERROR_MSI_INSTALL_FAILED); } @@ -576,30 +354,6 @@ namespace AppInstaller::CLI::Workflow } } - void RemoveInstaller(Execution::Context& context) - { - // Path may not be present if installed from a URL for MSIX - if (context.Contains(Execution::Data::InstallerPath)) - { - const auto& path = context.Get(); - AICLI_LOG(CLI, Info, << "Removing installer: " << path); - - try - { - // best effort - std::filesystem::remove(path); - } - catch (const std::exception& e) - { - AICLI_LOG(CLI, Warning, << "Failed to remove installer file after execution. Reason: " << e.what()); - } - catch (...) - { - AICLI_LOG(CLI, Warning, << "Failed to remove installer file after execution. Reason unknown."); - } - } - } - void ReportIdentityAndInstallationDisclaimer(Execution::Context& context) { context << @@ -610,8 +364,6 @@ namespace AppInstaller::CLI::Workflow void InstallPackageInstaller(Execution::Context& context) { context << - Workflow::ReportExecutionStage(ExecutionStage::Download) << - Workflow::DownloadInstaller << Workflow::ReportExecutionStage(ExecutionStage::PreExecution) << Workflow::SnapshotARPEntries << Workflow::ReportExecutionStage(ExecutionStage::Execution) << @@ -621,13 +373,20 @@ namespace AppInstaller::CLI::Workflow Workflow::RemoveInstaller; } - void InstallSinglePackage(Execution::Context& context) + void DownloadSinglePackage(Execution::Context& context) { context << Workflow::ReportIdentityAndInstallationDisclaimer << Workflow::ShowPackageAgreements(/* ensureAcceptance */ true) << - Workflow::GetDependenciesFromInstaller << + Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << + Workflow::DownloadInstaller; + } + + void InstallSinglePackage(Execution::Context& context) + { + context << + Workflow::DownloadSinglePackage << Workflow::InstallPackageInstaller; } @@ -670,6 +429,7 @@ namespace AppInstaller::CLI::Workflow installContext << Workflow::ReportIdentityAndInstallationDisclaimer << + Workflow::DownloadInstaller << Workflow::InstallPackageInstaller; installContext.Reporter.Info() << std::endl; diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h index 1fb2a5240b..40ccf60af8 100644 --- a/src/AppInstallerCLICore/Workflows/InstallFlow.h +++ b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -60,36 +60,6 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void EnsurePackageAgreementsAcceptanceForMultipleInstallers(Execution::Context& context); - // Composite flow that chooses what to do based on the installer type. - // Required Args: None - // Inputs: Manifest, Installer - // Outputs: None - void DownloadInstaller(Execution::Context& context); - - // Downloads the file referenced by the Installer. - // Required Args: None - // Inputs: Installer - // Outputs: HashPair, InstallerPath - void DownloadInstallerFile(Execution::Context& context); - - // Computes the hash of the MSIX signature file. - // Required Args: None - // Inputs: Installer - // Outputs: HashPair - void GetMsixSignatureHash(Execution::Context& context); - - // Gets the source list, filtering it if SourceName is present. - // Required Args: None - // Inputs: HashPair - // Outputs: SourceList - void VerifyInstallerHash(Execution::Context& context); - - // Update Motw of the downloaded installer if applicable - // Required Args: None - // Inputs: HashPair, InstallerPath?, SourceId? - // Outputs: None - void UpdateInstallerFileMotwIfApplicable(Execution::Context& context); - // Composite flow that chooses what to do based on the installer type. // Required Args: None // Inputs: Installer, InstallerPath @@ -134,13 +104,6 @@ namespace AppInstaller::CLI::Workflow bool m_isHResult; }; - - // Deletes the installer file. - // Required Args: None - // Inputs: InstallerPath - // Outputs: None - void RemoveInstaller(Execution::Context& context); - // Reports manifest identity and shows installation disclaimer // Required Args: None // Inputs: Manifest @@ -149,11 +112,17 @@ namespace AppInstaller::CLI::Workflow // Installs a specific package installer. See also InstallSinglePackage & InstallMultiplePackages. // Required Args: None - // Inputs: Manifest, Installer, PackageVersion, InstalledPackageVersion? + // Inputs: InstallerPath, Manifest, Installer, PackageVersion, InstalledPackageVersion? // Outputs: None void InstallPackageInstaller(Execution::Context& context); - // Installs a single package. This also does the reporting and user interaction + // Downloads the installer for a single package. This also does all the reporting and user interaction needed. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: InstallerPath + void DownloadSinglePackage(Execution::Context& context); + + // Installs a single package. This also does the reporting, user interaction, and installer download // for single-package installation. // RequiredArgs: None // Inputs: Manifest, Installer, PackageVersion, InstalledPackageVersion? diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp index 7611af7195..83ca60a863 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -181,68 +181,6 @@ namespace AppInstaller::CLI::Workflow return args; } - - // Complicated rename algorithm due to somewhat arbitrary failures. - // 1. First, try to rename. - // 2. Then, create an empty file for the target, and attempt to rename. - // 3. Then, try repeatedly for 500ms in case it is a timing thing. - // 4. Attempt to use a hard link if available. - // 5. Copy the file if nothing else has worked so far. - void RenameFile(const std::filesystem::path& from, const std::filesystem::path& to) - { - // 1. First, try to rename. - try - { - // std::filesystem::rename() handles motw correctly if applicable. - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - - // 2. Then, create an empty file for the target, and attempt to rename. - // This seems to fix things in certain cases, so we do it. - try - { - { - std::ofstream targetFile{ to }; - } - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - - // 3. Then, try repeatedly for 500ms in case it is a timing thing. - for (int i = 0; i < 5; ++i) - { - try - { - std::this_thread::sleep_for(100ms); - std::filesystem::rename(from, to); - return; - } - CATCH_LOG(); - } - - // 4. Attempt to use a hard link if available. - if (Runtime::SupportsHardLinks(from)) - { - try - { - // Create a hard link to the file; the installer will be left in the temp directory afterward - // but it is better to succeed the operation and leave a file around than to fail. - // First we have to remove the target file as the function will not overwrite. - std::filesystem::remove(to); - std::filesystem::create_hard_link(from, to); - return; - } - CATCH_LOG(); - } - - // 5. Copy the file if nothing else has worked so far. - // Create a copy of the file; the installer will be left in the temp directory afterward - // 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); - } } void ShellExecuteInstallImpl(Execution::Context& context) @@ -285,31 +223,6 @@ namespace AppInstaller::CLI::Workflow context.Add(std::move(installerArgs)); } - void RenameDownloadedInstaller(Execution::Context& context) - { - auto& installerPath = context.Get(); - std::filesystem::path renamedDownloadedInstaller(installerPath); - - switch(context.Get()->InstallerType) - { - case InstallerTypeEnum::Burn: - case InstallerTypeEnum::Exe: - case InstallerTypeEnum::Inno: - case InstallerTypeEnum::Nullsoft: - renamedDownloadedInstaller += L".exe"; - break; - case InstallerTypeEnum::Msi: - case InstallerTypeEnum::Wix: - renamedDownloadedInstaller += L".msi"; - break; - } - - RenameFile(installerPath, renamedDownloadedInstaller); - - 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; diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h index ee2142e27e..ff5721e3d4 100644 --- a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h +++ b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h @@ -34,12 +34,4 @@ namespace AppInstaller::CLI::Workflow // Inputs: Manifest?, Installer, InstallerPath // Outputs: InstallerArgs void GetInstallerArgs(Execution::Context& context); - - // This method appends appropriate extension to the downloaded installer. - // ShellExecute uses file extension to launch the installer appropriately. - // Required Args: None - // Inputs: Installer, InstallerPath - // Modifies: InstallerPath - // Outputs: None - void RenameDownloadedInstaller(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp index 439b6b67e3..b61e1fc43e 100644 --- a/src/AppInstallerCLITests/WorkFlow.cpp +++ b/src/AppInstallerCLITests/WorkFlow.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -400,8 +401,17 @@ void OverrideForUpdateInstallerMotw(TestContext& context) } }); } +void OverrideForCheckExistingInstaller(TestContext& context) +{ + context.Override({ CheckForExistingInstaller, [](TestContext&) + { + } }); +} + void OverrideForShellExecute(TestContext& context) { + OverrideForCheckExistingInstaller(context); + context.Override({ DownloadInstallerFile, [](TestContext& context) { context.Add({ {}, {} }); @@ -417,6 +427,8 @@ void OverrideForShellExecute(TestContext& context) void OverrideForDirectMsi(TestContext& context) { + OverrideForCheckExistingInstaller(context); + context.Override({ DownloadInstallerFile, [](TestContext& context) { context.Add({ {}, {} }); @@ -684,6 +696,7 @@ TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow][workflow]") std::ostringstream installOutput; TestContext context{ installOutput, std::cin }; OverrideForMSIX(context); + OverrideForCheckExistingInstaller(context); // Todo: point to files from our repo when the repo goes public context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Msix_StreamingFlow.yaml").GetPath().u8string()); diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp index 4fc2e0e6d3..af5970e406 100644 --- a/src/AppInstallerCommonCore/Downloader.cpp +++ b/src/AppInstallerCommonCore/Downloader.cpp @@ -275,11 +275,23 @@ namespace AppInstaller::Utility Microsoft::WRL::ComPtr zoneIdentifier; THROW_IF_FAILED(CoCreateInstance(CLSID_PersistentZoneIdentifier, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&zoneIdentifier))); - THROW_IF_FAILED(zoneIdentifier->Remove()); Microsoft::WRL::ComPtr persistFile; THROW_IF_FAILED(zoneIdentifier.As(&persistFile)); - THROW_IF_FAILED(persistFile->Save(filePath.c_str(), TRUE)); + + auto hr = persistFile->Load(filePath.c_str(), STGM_READ); + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) + { + // IPersistFile::Load returns same error for "file not found" and "motw not found". + // Check if the file exists to be sure we are on the "motw not found" case. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), !std::filesystem::exists(filePath)); + + AICLI_LOG(Core, Info, << "File does not contain motw. Skipped removing motw"); + return; + } + + THROW_IF_FAILED(zoneIdentifier->Remove()); + THROW_IF_FAILED(persistFile->Save(NULL, TRUE)); AICLI_LOG(Core, Info, << "Finished removing motw"); }