diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 59080e06d7..61bdddfbdb 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -284,6 +284,9 @@ + + + @@ -427,6 +430,11 @@ + + + + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index c0bb76812e..3bfc9f0dac 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -16,6 +16,9 @@ {2d97761a-4967-4ece-b5fd-d96f346731ed} + + {5c051a95-47d9-4501-a17b-4f2320e69c2d} + {7996cf09-4e36-4009-b48d-53ffc3bfa956} @@ -180,6 +183,15 @@ Header Files + + Microsoft\PreIndexed + + + Microsoft\PreIndexed + + + Microsoft\PreIndexed + Microsoft @@ -560,6 +572,21 @@ Source Files + + Microsoft\PreIndexed + + + Microsoft\PreIndexed + + + Microsoft\PreIndexed + + + Microsoft\PreIndexed + + + Microsoft\PreIndexed + Microsoft diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/DeployedPackageStore.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/DeployedPackageStore.cpp new file mode 100644 index 0000000000..5262f2587f --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/DeployedPackageStore.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/PreIndexed/PackageStore.h" + +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + namespace anon + { + // Holds a source's packages by registering them with the platform. + struct DeployedPackageStore : public PackageStoreBase + { + using PackageStoreBase::PackageStoreBase; + + void Persist(AcquiredPackage&& package, IProgressCallback& progress) override + { + Deployment::Options options{ IsTrusted() }; + + // The file lock is deliberately held across deployment, so that the package the + // platform reads is the one that was validated. + winrt::Windows::Foundation::Uri uri = winrt::Windows::Foundation::Uri(package.Path.c_str()); + Deployment::AddPackage( + uri, + options, + progress); + + // Nothing further needs the file; it is removed when the package goes out of scope. + } + + std::optional GetVersion(const PackageKey& package) const override + { + auto extension = GetExtension(package); + + if (!extension) + { + return std::nullopt; + } + + auto version = extension->GetPackageVersion(); + return Msix::PackageVersion{ version.Major, version.Minor, version.Build, version.Revision }; + } + + std::optional GetIndex(const PackageKey& package, IProgressCallback& progress) override + { + auto extension = GetExtension(package); + + if (!extension) + { + AICLI_LOG(Repo, Info, << "Deployed " << GetSlotName(package.Slot) << " package not found: " << package.Identity); + return std::nullopt; + } + + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NEEDS_REMEDIATION), !extension->VerifyContentIntegrity(progress)); + + ExtractedIndex result; + + // To work around an issue with accessing the public folder, we are temporarily + // constructing the location ourself. This was already the case for the local file + // mechanism, and we can fix both in the future. The only problem with this is that + // the directory in the extension *must* be Public, rather than one set by the + // creator. + result.Path = extension->GetPackagePath(); + result.Path /= s_IndexFilePath; + + return std::optional{ std::move(result) }; + } + + void Remove(const std::vector& packages, IProgressCallback& progress) override + { + for (const auto& package : packages) + { + auto fullName = Msix::GetPackageFullNameFromFamilyName(package.Identity); + + if (!fullName) + { + AICLI_LOG(Repo, Info, << "No full name found for family name: " << package.Identity); + } + else + { + AICLI_LOG(Repo, Info, << "Removing package: " << *fullName); + Deployment::RemovePackage(*fullName, winrt::Windows::Management::Deployment::RemovalOptions::None, progress); + } + } + } + + // The deployed index is read in place, so a read can be attempted before taking the + // lock and retried under it only when that fails. + bool AllowsUnlockedRead() const override { return true; } + + private: + std::optional GetExtension(const PackageKey& package) const + { + THROW_HR_IF(E_UNEXPECTED, package.Identity.empty()); + Deployment::ExtensionCatalog catalog(Deployment::SourceExtensionName); + return catalog.FindByPackageFamilyAndId(package.Identity, Deployment::IndexDBId); + } + }; + } + + std::unique_ptr CreateDeployedPackageStore(const SourceDetails& details) + { + return std::make_unique(details); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/FullIndexForm.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/FullIndexForm.cpp new file mode 100644 index 0000000000..3eaf4bdbe6 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/FullIndexForm.cpp @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/PreIndexed/IndexForm.h" +#include "Microsoft/PreIndexed/RemotePackage.h" + +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + namespace anon + { + // A source composed of a single package holding the complete index. + struct FullIndexForm : public IIndexForm + { + FullIndexForm(const SourceDetails& details) : m_details(details) + { + m_identity = details.Data; + } + + std::optional DiscoverIdentities(IProgressCallback&) override + { + if (!m_identity.empty()) + { + return m_identity; + } + + PreIndexedPackageInfo packageInfo(GetFullIndexPackageLocations(m_details), [](const std::string& packageLocation) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NOT_SECURE, Utility::IsUrlRemote(packageLocation) && !Utility::IsUrlSecure(packageLocation)); + }); + + AICLI_LOG(Repo, Info, << "Initializing source from: " << m_details.Name << " => " << packageInfo.PackageLocation()); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.MsixInfo().GetIsBundle()); + + auto fullName = packageInfo.MsixInfo().GetPackageFullName(); + AICLI_LOG(Repo, Info, << "Found package full name: " << m_details.Name << " => " << fullName); + + m_identity = Msix::GetPackageFamilyNameFromFullName(fullName); + + return m_identity; + } + + std::vector GetPackages() const override + { + return { GetKey() }; + } + + bool HasIdentities() const override + { + return !m_identity.empty(); + } + + bool IsHeld(const IPackageStore& store) const override + { + return GetHeldVersion(store).has_value(); + } + + std::optional GetHeldVersion(const IPackageStore& store) const override + { + return store.GetVersion(GetKey()); + } + + UpdateResult Update(IPackageStore& store, bool isBackground, IProgressCallback& progress, UpdateReport& report) override + { + std::optional currentVersion = GetHeldVersion(store); + PreIndexedPackageUpdateCheck updateCheck(GetFullIndexPackageLocations(m_details)); + + if (currentVersion) + { + report.PreviousIndexPublishedAt = Utility::GetTimePointFromVersion(currentVersion.value()); + } + + report.NewIndexPublishedAt = Utility::GetTimePointFromVersion(updateCheck.AvailableVersion()); + + if (currentVersion) + { + if (currentVersion.value() >= updateCheck.AvailableVersion()) + { + AICLI_LOG(Repo, Verbose, << "Remote source data (" << updateCheck.AvailableVersion().ToString() << + ") was not newer than existing (" << currentVersion.value().ToString() << "), no update needed"); + return UpdateResult::Success; + } + else + { + AICLI_LOG(Repo, Verbose, << "Remote source data (" << updateCheck.AvailableVersion().ToString() << + ") was newer than existing (" << currentVersion.value().ToString() << "), updating"); + } + } + + if (progress.IsCancelledBy(CancelReason::Any)) + { + AICLI_LOG(Repo, Info, << "Cancelling update upon request"); + return UpdateResult::Aborted; + } + + auto lock = store.Lock(progress, isBackground); + if (!lock) + { + // The lock says nothing about whether this form would have worked. + return UpdateResult::Aborted; + } + + report.Reportable = true; + + return Acquire(store, updateCheck.PackageLocation(), progress, report); + } + + // Acquires the index from a location that the caller has already determined, under a + // lock that the caller already holds. + UpdateResult Acquire(IPackageStore& store, const std::string& location, IProgressCallback& progress, UpdateReport& report) + { + auto acquired = store.Acquire(GetKey(), location, progress); + if (!acquired) + { + // Acquisition reports nothing only when it was cancelled. + return UpdateResult::Aborted; + } + + report.DownloadedBytes = acquired->DownloadedBytes; + + store.Persist(std::move(acquired.value()), progress); + + return UpdateResult::Success; + } + + SQLiteIndex Open(IPackageStore& store, IProgressCallback& progress) override + { + auto extracted = store.GetIndex(GetKey(), progress); + + if (progress.IsCancelledBy(CancelReason::Any)) + { + THROW_WIN32(ERROR_CANCELLED); + } + + if (!extracted) + { + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING); + } + + return SQLiteIndex::Open(extracted->Path.u8string(), SQLiteIndex::OpenDisposition::Immutable, std::move(extracted->TemporaryFile)); + } + + private: + PackageKey GetKey() const + { + THROW_HR_IF(E_NOT_VALID_STATE, m_identity.empty()); + return PackageKey{ PackageSlot::FullIndex, m_identity }; + } + + SourceDetails m_details; + std::string m_identity; + }; + } + + std::unique_ptr CreateFullIndexForm(const SourceDetails& details) + { + return std::make_unique(details); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/IndexForm.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/IndexForm.h new file mode 100644 index 0000000000..8c72efb4a4 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/IndexForm.h @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/PreIndexed/PackageStore.h" +#include "Microsoft/SQLiteIndex.h" + +#include +#include +#include +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + // What an update did, in the terms that the telemetry event is defined in. + struct UpdateReport + { + std::optional PreviousIndexPublishedAt; + std::optional NewIndexPublishedAt; + bool UsedDeltaDownload = false; + std::optional PreviousBaselinePublishedAt; + std::optional NewBaselinePublishedAt; + bool BaselineUpdated = false; + + // Nothing when no bytes moved, which is how the add path decides whether to report at all. + std::optional DownloadedBytes; + + // Whether this update is one that should be reported. + bool Reportable = false; + }; + + // What an attempt to update a form concluded. + enum class UpdateResult + { + // The store now holds this form, up to date with the source. + Success, + + // The operation did not complete: it was cancelled, or the lock could not be taken. + // Nothing can be concluded about whether this form would have worked, so the caller + // reports failure rather than trying another form. + Aborted, + + // This form cannot serve the source. The caller should fall back to another. + Unusable, + }; + + // How a source's index is composed, and therefore which packages it has and how they relate. + // + // A form knows nothing about where its packages are kept. It is given a store to act on, and + // names the packages it wants by identity and slot. + struct IIndexForm + { + virtual ~IIndexForm() = default; + + // Determines the identities of this form's packages, probing the source where the details + // do not already record them. + // + // Probing is confined to the add path: it is the only operation that can persist what it + // learns. A form whose details are already populated reports what they say, so a source + // configured before this form existed is not silently migrated onto it. + // + // Returns the value that should be written to SourceDetails::Data, or nothing when the + // source does not publish the packages that this form needs. + virtual std::optional DiscoverIdentities(IProgressCallback& progress) = 0; + + // The packages that make up this form. + // Throws when the identities are not yet known; see DiscoverIdentities. + virtual std::vector GetPackages() const = 0; + + // Whether the identities of this form's packages are already known, so that GetPackages + // can be called and no probe is needed. + virtual bool HasIdentities() const = 0; + + // Whether the store holds every package that this form needs. + virtual bool IsHeld(const IPackageStore& store) const = 0; + + // The published version of the held index, which is what staleness is judged from. + virtual std::optional GetHeldVersion(const IPackageStore& store) const = 0; + + // Brings the store up to date with the source. + // + // A failure to update is reported by throwing; the result distinguishes the outcomes that + // are not failures. See UpdateResult. + virtual UpdateResult Update(IPackageStore& store, bool isBackground, IProgressCallback& progress, UpdateReport& report) = 0; + + // Opens the index that the store holds for this form. + virtual SQLiteIndex Open(IPackageStore& store, IProgressCallback& progress) = 0; + }; + + // The complete index, as acquired by a client that is not using a delta. + std::unique_ptr CreateFullIndexForm(const SourceDetails& details); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/LocalFilePackageStore.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/LocalFilePackageStore.cpp new file mode 100644 index 0000000000..c37741644e --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/LocalFilePackageStore.cpp @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/PreIndexed/PackageStore.h" +#include "Microsoft/PreIndexedPackageSourceFactory.h" + +#include +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + namespace anon + { + static constexpr std::string_view s_LocalFullIndexFileName = "source.msix"sv; + static constexpr std::string_view s_LocalDeltaFileName = "delta.msix"sv; + static constexpr std::string_view s_LocalBaselineFileName = "baseline.msix"sv; + + // The file name that a slot's package is stored under. + // + // We choose these names ourselves, so this mechanism needs no package identity at all. + // Note that the full index is stored as source.msix regardless of which of the two remote + // names it came from. + std::string_view GetLocalFileNameForSlot(PackageSlot slot) + { + switch (slot) + { + case PackageSlot::FullIndex: return s_LocalFullIndexFileName; + case PackageSlot::Delta: return s_LocalDeltaFileName; + case PackageSlot::Baseline: return s_LocalBaselineFileName; + } + + THROW_HR(E_UNEXPECTED); + } + + + // Holds a source's packages as files in our own local state. + struct LocalFilePackageStore : public PackageStoreBase + { + using PackageStoreBase::PackageStoreBase; + + void Persist(AcquiredPackage&& package, IProgressCallback&) override + { + std::filesystem::path packageState = GetStatePath(); + std::filesystem::create_directories(packageState); + + std::filesystem::path packagePath = packageState / GetLocalFileNameForSlot(package.Key.Slot); + std::filesystem::path stagedPath = packagePath.u8string() + ".dnld.msix"; + + auto removeStagedFileOnExit = wil::scope_exit([&]() + { + try + { + std::filesystem::remove(stagedPath); + } + catch (...) + { + AICLI_LOG(Repo, Info, << "Failed to remove temp index file at: " << stagedPath); + } + }); + + // The final move has to be a rename within the state directory, because that is + // what makes replacing the held package atomic. So the validated file is staged + // beside its destination first, which may cross a volume boundary, and only then + // renamed into place. + // + // The lock is released before the file is moved; everything it was guarding has + // already been checked, and a locked file cannot be moved or removed. + package.FileLock.reset(); + + if (package.IsTemporary()) + { + Filesystem::RenameFile(package.Path, stagedPath); + package.ReleaseTemporary(); + } + else + { + // The file is the location that the source names, not ours to consume. + std::filesystem::copy_file(package.Path, stagedPath, std::filesystem::copy_options::overwrite_existing); + } + + std::filesystem::rename(stagedPath, packagePath); + AICLI_LOG(Repo, Info, << "Source update success."); + + removeStagedFileOnExit.release(); + } + + std::optional GetVersion(const PackageKey& package) const override + { + std::filesystem::path packagePath = GetPathForSlot(package.Slot); + + if (std::filesystem::exists(packagePath)) + { + // If we already have a trusted index package, use it to determine if we need to update or not. + Msix::WriteLockedMsixFile indexPackage{ packagePath }; + if (ValidateTrust(indexPackage)) + { + Msix::MsixInfo msixInfo{ packagePath }; + auto manifest = msixInfo.GetAppPackageManifests(); + + if (manifest.size() == 1) + { + return manifest[0].GetIdentity().GetVersion(); + } + } + } + + return std::nullopt; + } + + std::optional GetIndex(const PackageKey& package, IProgressCallback& progress) override + { + std::filesystem::path packagePath = GetPathForSlot(package.Slot); + + if (!std::filesystem::exists(packagePath)) + { + AICLI_LOG(Repo, Info, << "Data not found at " << packagePath); + return std::nullopt; + } + + // Put a write exclusive lock on the index package. + Msix::WriteLockedMsixFile indexPackage{ packagePath }; + + // Validate index package trust info. + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, !ValidateTrust(indexPackage)); + + // Create a temp lock exclusive index file. + auto tempIndexFilePath = Runtime::GetNewTempFilePath(); + auto tempIndexFile = Utility::ManagedFile::CreateWriteLockedFile(tempIndexFilePath, GENERIC_WRITE, true); + + // Populate temp index file. + Msix::MsixInfo packageInfo{ packagePath }; + packageInfo.WriteToFileHandle(s_IndexFilePath, tempIndexFile.GetFileHandle(), progress); + + if (progress.IsCancelledBy(CancelReason::Any)) + { + AICLI_LOG(Repo, Info, << "Cancelling index extraction upon request"); + return std::nullopt; + } + + ExtractedIndex result; + result.Path = tempIndexFile.GetFilePath(); + result.TemporaryFile = std::move(tempIndexFile); + + return std::optional{ std::move(result) }; + } + + // The state directory holds this source's packages and nothing else, so removing it + // removes every slot. Nothing that a caller could name is left behind. + void Remove(const std::vector&, IProgressCallback&) override + { + std::filesystem::path packageState = GetStatePath(); + + if (!std::filesystem::exists(packageState)) + { + AICLI_LOG(Repo, Info, << "No state found for source: " << packageState.u8string()); + } + else + { + AICLI_LOG(Repo, Info, << "Removing state found for source: " << packageState.u8string()); + std::filesystem::remove_all(packageState); + } + } + + // Reading extracts from the held file, so the lock must be taken first. + bool AllowsUnlockedRead() const override { return false; } + + private: + std::filesystem::path GetStatePath() const + { + std::filesystem::path result = Runtime::GetPathTo(Runtime::PathName::LocalState); + result /= PreIndexedPackageSourceFactory::Type(); + result /= m_sourceIdentity; + return result; + } + + std::filesystem::path GetPathForSlot(PackageSlot slot) const + { + return GetStatePath() / GetLocalFileNameForSlot(slot); + } + }; + } + + std::unique_ptr CreateLocalFilePackageStore(const SourceDetails& details) + { + return std::make_unique(details); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/PackageStore.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/PackageStore.cpp new file mode 100644 index 0000000000..fd2736a63a --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/PackageStore.cpp @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/PreIndexed/PackageStore.h" +#include "Microsoft/PreIndexedPackageSourceFactory.h" + +#include +#include +#include + +using namespace std::string_literals; + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + std::string_view GetSlotName(PackageSlot slot) + { + switch (slot) + { + case PackageSlot::FullIndex: return "fullIndex"sv; + case PackageSlot::Delta: return "delta"sv; + case PackageSlot::Baseline: return "baseline"sv; + } + + THROW_HR(E_UNEXPECTED); + } + + AcquiredPackage::AcquiredPackage(AcquiredPackage&& other) noexcept + { + *this = std::move(other); + } + + AcquiredPackage& AcquiredPackage::operator=(AcquiredPackage&& other) noexcept + { + if (this != &other) + { + Key = std::move(other.Key); + Path = std::move(other.Path); + DownloadedBytes = std::move(other.DownloadedBytes); + FileLock = std::move(other.FileLock); + m_isTemporary = other.m_isTemporary; + + // The moved from object must not also remove the file. + other.m_isTemporary = false; + other.Path.clear(); + } + + return *this; + } + + AcquiredPackage::~AcquiredPackage() + { + if (m_isTemporary && !Path.empty()) + { + // Release the lock before attempting to remove the file that it holds. + FileLock.reset(); + + try + { + std::filesystem::remove(Path); + } + catch (...) + { + AICLI_LOG(Repo, Info, << "Failed to remove acquired package file at: " << Path); + } + } + } + + PackageStoreBase::PackageStoreBase(const SourceDetails& details) : + m_sourceName(details.Name), m_trustLevel(details.TrustLevel) + { + // Gets the identity of the source itself, as distinct from the identity of any one of the + // packages it is composed of. + // + // The cross process lock name and the local state directory are both derived from this, + // which is why it must be stable: changing either one orphans an existing source's local + // state, and -- worse -- lets an old and a new client take different locks over the same data. + // + // The fallback exists because Identifier was not always stored; a source written by an old + // enough client has only Data, which is the value such a client would have put there. + m_sourceIdentity = details.Identifier.empty() ? details.Data : details.Identifier; + THROW_HR_IF(E_UNEXPECTED, m_sourceIdentity.empty()); + } + + bool PackageStoreBase::RequireStoreOrigin() const + { + return WI_IsFlagSet(m_trustLevel, SourceTrustLevel::StoreOrigin); + } + + bool PackageStoreBase::IsTrusted() const + { + return WI_IsFlagSet(m_trustLevel, SourceTrustLevel::Trusted); + } + + bool PackageStoreBase::ValidateTrust(const Msix::WriteLockedMsixFile& package) const + { + return package.ValidateTrustInfo(RequireStoreOrigin()); + } + + std::optional PackageStoreBase::Acquire(const PackageKey& package, const std::string& location, IProgressCallback& progress) + { + AcquiredPackage result; + result.Key = package; + + if (Utility::IsUrlRemote(location)) + { + std::filesystem::path localFile = Runtime::GetPathTo(Runtime::PathName::Temp); + localFile /= m_sourceIdentity + "." + std::string{ GetSlotName(package.Slot) } + ".msix"; + + // Set the path before downloading so that a partial download is still cleaned up. + result.Path = localFile; + result.MarkTemporary(); + + auto downloadResult = Utility::Download(location, localFile, Utility::DownloadType::Index, progress); + result.DownloadedBytes = downloadResult.SizeInBytes; + } + else + { + // A local location is not ours, so it is used in place and never removed. + result.Path = Utility::ConvertToUTF16(location); + progress.OnProgress(100, 100, ProgressType::Percent); + } + + if (progress.IsCancelledBy(CancelReason::Any)) + { + AICLI_LOG(Repo, Info, << "Cancelling acquisition upon request"); + return std::nullopt; + } + + // Hold the file against modification from the moment that it is validated, so that no + // store can commit to something other than what was checked here. + Msix::WriteLockedMsixFile fileLock{ result.Path }; + Msix::MsixInfo localMsixInfo{ result.Path }; + + // The package should not be a bundle + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, localMsixInfo.GetIsBundle()); + + std::string packageFamilyName = Msix::GetPackageFamilyNameFromFullName(localMsixInfo.GetPackageFullName()); + + // A package whose identity is not the one that was asked for is an integrity failure. + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, package.Identity != packageFamilyName); + + if (!ValidateTrust(fileLock)) + { + AICLI_LOG(Repo, Error, << "Source update failed. Source package failed trust validation."); + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); + } + + result.FileLock = std::move(fileLock); + + return std::optional{ std::move(result) }; + } + + Synchronization::CrossProcessLock PackageStoreBase::Lock(IProgressCallback& progress, bool isBackground) + { + Synchronization::CrossProcessLock result("PreIndexedSourceCPL_"s + m_sourceIdentity); + + if (isBackground) + { + // If this is a background update, don't wait on the lock. + result.TryAcquireNoWait(); + } + else + { + result.Acquire(progress); + } + + return result; + } + + bool CanUseDeployedPackage() + { + return Runtime::IsRunningInPackagedContext() && Runtime::IsRunningAsInteractiveUser(); + } + + namespace anon + { + // A store over every store that this process can reach a source's packages through. + // + // Reading answers from whichever of them holds the most recently published copy, and + // removing clears all of them. Acquiring and persisting throw: a store that spans + // mechanisms has no single place to put a package, and that decision belongs to + // CreateStore. + struct CompositePackageStore : public IPackageStore + { + CompositePackageStore(const SourceDetails& details) + { + // The store that this process would write to comes first, so that an equally + // recent copy keeps a read on the store that it maintains itself. + if (CanUseDeployedPackage()) + { + m_stores.emplace_back(CreateDeployedPackageStore(details)); + m_stores.emplace_back(CreateLocalFilePackageStore(details)); + } + else + { + m_stores.emplace_back(CreateLocalFilePackageStore(details)); + if (Runtime::IsRunningInPackagedContext()) + { + m_stores.emplace_back(CreateDeployedPackageStore(details)); + } + } + } + + std::optional Acquire(const PackageKey&, const std::string&, IProgressCallback&) override + { + THROW_WIN32(ERROR_NOT_SUPPORTED); + } + + void Persist(AcquiredPackage&&, IProgressCallback&) override + { + THROW_WIN32(ERROR_NOT_SUPPORTED); + } + + std::optional GetVersion(const PackageKey& package) const override + { + return Resolve(package).GetVersion(package); + } + + std::optional GetIndex(const PackageKey& package, IProgressCallback& progress) override + { + return Resolve(package).GetIndex(package, progress); + } + + void Remove(const std::vector& packages, IProgressCallback& progress) override + { + HRESULT firstStoreFailure = S_OK; + bool firstStore = true; + + for (auto& store : m_stores) + { + try + { + store->Remove(packages, progress); + } + catch (...) + { + HRESULT hr = LOG_CAUGHT_EXCEPTION(); + + // Only throw if the primary (first) store fails to remove. + if (firstStore) + { + firstStoreFailure = hr; + } + } + + firstStore = false; + } + + THROW_IF_FAILED(firstStoreFailure); + } + + Synchronization::CrossProcessLock Lock(IProgressCallback& progress, bool isBackground = false) override + { + // The lock is named from the source rather than from the mechanism, so every store + // for a source shares one and any of them can take it. + return m_stores.front()->Lock(progress, isBackground); + } + + bool AllowsUnlockedRead() const override + { + // Before anything has been asked about, the store this process maintains is the + // one most likely to answer, and deferring to it keeps this identical to what a + // single store would have reported. A later read that resolves elsewhere and + // cannot be served unlocked fails, which the caller already retries under the + // lock. + return (m_resolved ? m_resolved : m_stores.front().get())->AllowsUnlockedRead(); + } + + private: + IPackageStore& Resolve(const PackageKey& package) const + { + if (!m_resolved) + { + m_resolved = m_stores.front().get(); + std::optional bestVersion = m_resolved->GetVersion(package); + + for (auto itr = m_stores.begin() + 1; itr != m_stores.end(); ++itr) + { + std::optional version = (*itr)->GetVersion(package); + + // The store this process would write to is first, so only a strictly more + // recent copy moves it off the store that it maintains itself. + if (version && (!bestVersion || bestVersion.value() < version.value())) + { + bestVersion = std::move(version); + m_resolved = itr->get(); + } + } + } + + return *m_resolved; + } + + std::vector> m_stores; + mutable IPackageStore* m_resolved = nullptr; + }; + } + + std::unique_ptr CreateStore(const SourceDetails& details) + { + return CanUseDeployedPackage() ? CreateDeployedPackageStore(details) : CreateLocalFilePackageStore(details); + } + + std::unique_ptr CreateCompositeStore(const SourceDetails& details) + { + return std::make_unique(details); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/PackageStore.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/PackageStore.h new file mode 100644 index 0000000000..1fdf292694 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/PackageStore.h @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SourceFactory.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + using namespace std::string_view_literals; + + // TODO: This being hard coded to force using the Public directory name is not ideal. + static constexpr std::string_view s_IndexFilePath = "Public\\index.db"sv; + + // The packages that a source can hold locally. + enum class PackageSlot + { + // The complete index, as acquired by a client that is not using a delta. + FullIndex, + + // The changes since the baseline. + Delta, + + // The index that the delta's changes apply to. + Baseline, + }; + + // A short token for a slot, used in diagnostics and in temporary file names. + std::string_view GetSlotName(PackageSlot slot); + + // Identifies one of a source's packages to a store. + // + // The two fields are the two ways that a store can find a package, and which one it uses is + // the store's business: the local file store names its own files and so keys on the slot, + // while the platform keys deployed packages on their identity. + struct PackageKey + { + PackageSlot Slot = PackageSlot::FullIndex; + + // The package family name of this package. + std::string Identity; + }; + + // Makes the index database of a package that we hold locally readable. + // + // The deployed store already has it on disk inside the installed package; the local file store + // has to extract it from the package file, so the result may own a temporary file that must + // outlive any read of the path. + struct ExtractedIndex + { + std::filesystem::path Path; + Utility::ManagedFile TemporaryFile; + }; + + // A package that has been fetched and validated for a key, but is not yet stored anywhere that + // it can be found again. + // + // Acquisition is entirely store independent, which is why it is separated from Persist and + // lives on the base rather than on either mechanism. Holding the validation lock here is what + // makes committing to it safe: the file cannot be altered between the point at which it was + // checked and the point at which a store takes it. + // + // Note that this does not currently support handing a refused package to another store. + // Persist takes the package by rvalue reference and the mechanisms consume it -- the local + // file store releases the lock and moves the file -- so a failed Persist leaves the caller + // with nothing to retry. Supporting that would need Persist to be defined as leaving the + // package untouched on failure, which neither mechanism does today. + struct AcquiredPackage + { + AcquiredPackage() = default; + + AcquiredPackage(const AcquiredPackage&) = delete; + AcquiredPackage& operator=(const AcquiredPackage&) = delete; + + AcquiredPackage(AcquiredPackage&& other) noexcept; + AcquiredPackage& operator=(AcquiredPackage&& other) noexcept; + + // Removes the file when it was ours and no store took it. + ~AcquiredPackage(); + + // The package that this was acquired to be. + PackageKey Key; + + // Where the validated package currently is. + std::filesystem::path Path; + + // The bytes transferred, when it came from a remote location. + std::optional DownloadedBytes; + + // Holds the file against modification from the moment it was validated. + // A store that needs to move the file must release this first. + std::optional FileLock; + + // Declares that we created the file at Path and must remove it if nothing takes it. + void MarkTemporary() { m_isTemporary = true; } + + // Declares that a store has taken responsibility for the file. + void ReleaseTemporary() { m_isTemporary = false; } + + // Whether the file at Path is ours to consume. When it is not, the location named by the + // source is being used in place and a store must copy rather than move it. + bool IsTemporary() const { return m_isTemporary; } + + private: + bool m_isTemporary = false; + }; + + // Holds a source's packages locally, so that they can be found again in a later process. + // A store knows nothing about how the index is composed. + struct IPackageStore + { + virtual ~IPackageStore() = default; + + // Fetches a package to a temporary location and validates it. + // Nothing when the operation was cancelled. + virtual std::optional Acquire(const PackageKey& package, const std::string& location, IProgressCallback& progress) = 0; + + // Stores an acquired package so that it can be found again. + virtual void Persist(AcquiredPackage&& package, IProgressCallback& progress) = 0; + + // The version currently held for a package, or nothing when it is not held. + virtual std::optional GetVersion(const PackageKey& package) const = 0; + + // Makes the index inside a held package readable. Nothing when it is not held. + virtual std::optional GetIndex(const PackageKey& package, IProgressCallback& progress) = 0; + + // Removes everything held for the given packages. + virtual void Remove(const std::vector& packages, IProgressCallback& progress) = 0; + + // Takes the cross process lock guarding this source's local copy. + virtual Synchronization::CrossProcessLock Lock(IProgressCallback& progress, bool isBackground = false) = 0; + + // Whether a read may be attempted before taking the lock. + virtual bool AllowsUnlockedRead() const = 0; + }; + + // The store independent half of a store: the fields every store needs, acquisition, and the + // lock, all of which are properties of the source rather than of the mechanism. + struct PackageStoreBase : public IPackageStore + { + PackageStoreBase(const SourceDetails& details); + + std::optional Acquire(const PackageKey& package, const std::string& location, IProgressCallback& progress) override; + + Synchronization::CrossProcessLock Lock(IProgressCallback& progress, bool isBackground = false) override; + + protected: + // Whether the trust validation of packages should require a Microsoft origin. + bool RequireStoreOrigin() const; + + // Whether a package may be deployed without being explicitly trusted. + bool IsTrusted() const; + + // Whether a source package is trusted enough to be used. + // + // Every store validates the same way, and this is the single point at which it happens, so + // that no mechanism can diverge on what it accepts. + bool ValidateTrust(const Msix::WriteLockedMsixFile& package) const; + + std::string m_sourceName; + + // The identity of the source itself, which every store derives its storage location from. + std::string m_sourceIdentity; + + SourceTrustLevel m_trustLevel = SourceTrustLevel::None; + }; + + // Creates the store that this process should maintain a source in. + std::unique_ptr CreateStore(const SourceDetails& details); + + // Creates a store over every store that this process can reach a source's packages through. + // Reading answers from whichever of them holds the most recently published copy, and removing + // clears all of them. + std::unique_ptr CreateCompositeStore(const SourceDetails& details); + + // The individual mechanisms. + std::unique_ptr CreateDeployedPackageStore(const SourceDetails& details); + std::unique_ptr CreateLocalFilePackageStore(const SourceDetails& details); + + // Whether deployment is available to this process at all. + bool CanUseDeployedPackage(); +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/RemotePackage.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/RemotePackage.cpp new file mode 100644 index 0000000000..d6f000c0f5 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/RemotePackage.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/PreIndexed/RemotePackage.h" + +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + namespace + { + // The header that a well behaved server sets so that a client can learn the published + // version without downloading the package. + static constexpr std::string_view s_PackageVersionHeader = "x-ms-meta-sourceversion"sv; + + // Construct the package location from the given details. + // Currently expects that the arg is an https uri pointing to the root of the data. + std::string GetPackageLocation(const std::string& basePath, std::string_view fileName) + { + std::string result = basePath; + if (result.back() != '/') + { + result += '/'; + } + result += fileName; + return result; + } + + Msix::PackageVersion GetAvailableVersionFrom(const std::string& packageLocation) + { + if (Utility::IsUrlRemote(packageLocation)) + { + std::map headers = Utility::GetHeaders(packageLocation); + auto itr = headers.find(std::string{ s_PackageVersionHeader }); + if (itr != headers.end()) + { + AICLI_LOG(Repo, Verbose, << "Header indicates version is: " << itr->second); + return { itr->second }; + } + + // We did not find the header we were looking for, log the ones we did find + AICLI_LOG(Repo, Verbose, << "Did not find " << s_PackageVersionHeader << " in:\n" << [&]() + { + std::ostringstream headerLog; + for (const auto& header : headers) + { + headerLog << " " << header.first << " : " << header.second << '\n'; + } + return std::move(headerLog).str(); + }()); + } + + AICLI_LOG(Repo, Verbose, << "Reading package data to determine version"); + Msix::MsixInfo info{ packageLocation }; + auto manifest = info.GetAppPackageManifests(); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, manifest.size() > 1); + THROW_HR_IF(E_UNEXPECTED, manifest.size() == 0); + + return manifest[0].GetIdentity().GetVersion(); + } + } + + std::vector GetPackageLocations(const SourceDetails& details, const std::vector& relativeLocations) + { + THROW_HR_IF(E_INVALIDARG, details.Arg.empty()); + + std::vector result; + + for (std::string_view relativeLocation : relativeLocations) + { + result.emplace_back(GetPackageLocation(details.Arg, relativeLocation)); + } + + if (!details.AlternateArg.empty()) + { + for (std::string_view relativeLocation : relativeLocations) + { + result.emplace_back(GetPackageLocation(details.AlternateArg, relativeLocation)); + } + } + + return result; + } + + std::vector GetFullIndexPackageLocations(const SourceDetails& details) + { + return GetPackageLocations(details, { s_V2_PackageFileName, s_PackageFileName }); + } + + std::vector GetDeltaPackageLocations(const SourceDetails& details) + { + return GetPackageLocations(details, { s_DeltaPackageFileName }); + } + + std::vector GetBaselinePackageLocations(const SourceDetails& details, const std::string& relativeSourcePath) + { + THROW_HR_IF(E_INVALIDARG, relativeSourcePath.empty()); + + return GetPackageLocations(details, { relativeSourcePath }); + } + + PreIndexedPackageUpdateCheck::PreIndexedPackageUpdateCheck(std::vector potentialLocations) + { + std::exception_ptr primaryException; + + for (const auto& location : potentialLocations) + { + try + { + m_availableVersion = GetAvailableVersionFrom(location); + m_packageLocation = location; + return; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageUpdateCheck failed on location: %hs", location.c_str()); + if (!primaryException) + { + primaryException = std::current_exception(); + } + } + } + + std::rethrow_exception(primaryException); + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/RemotePackage.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/RemotePackage.h new file mode 100644 index 0000000000..35ccc681b2 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexed/RemotePackage.h @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SourceFactory.h" + +#include +#include + +#include +#include +#include +#include + +namespace AppInstaller::Repository::Microsoft::PreIndexed +{ + using namespace std::string_view_literals; + + // The full index, published under two names so that a client too old to read the current + // format can still find one that it understands. + static constexpr std::string_view s_PackageFileName = "source.msix"sv; + static constexpr std::string_view s_V2_PackageFileName = "source2.msix"sv; + + // The delta, published at a fixed name beside the full index. + static constexpr std::string_view s_DeltaPackageFileName = "delta.msix"sv; + + // Gets the set of package locations that should be tried, in order. + // Every relative location is tried against the primary arg before any is tried against the + // alternate, so that a source stays on its primary location whenever that location can + // serve the request at all. + std::vector GetPackageLocations(const SourceDetails& details, const std::vector& relativeLocations); + + // The locations of the full index, which is what a client that is not using a delta acquires. + std::vector GetFullIndexPackageLocations(const SourceDetails& details); + + // The locations of the delta. + std::vector GetDeltaPackageLocations(const SourceDetails& details); + + // The locations of the baseline that a delta names. + std::vector GetBaselinePackageLocations(const SourceDetails& details, const std::string& relativeSourcePath); + + // Abstracts the fallback for package location when the MsixInfo is needed. + struct PreIndexedPackageInfo + { + template + PreIndexedPackageInfo(std::vector potentialLocations, LocationCheck&& locationCheck) + { + for (const auto& location : potentialLocations) + { + locationCheck(location); + } + + std::exception_ptr primaryException; + + for (const auto& location : potentialLocations) + { + try + { + m_msixInfo = std::make_unique(location); + m_packageLocation = location; + return; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageInfo failed on location: %hs", location.c_str()); + if (!primaryException) + { + primaryException = std::current_exception(); + } + } + } + + std::rethrow_exception(primaryException); + } + + const std::string& PackageLocation() const { return m_packageLocation; } + Msix::MsixInfo& MsixInfo() { return *m_msixInfo; } + + private: + std::string m_packageLocation; + std::unique_ptr m_msixInfo; + }; + + // Abstracts the fallback for package location when an update is being done. + struct PreIndexedPackageUpdateCheck + { + PreIndexedPackageUpdateCheck(std::vector potentialLocations); + + const std::string& PackageLocation() const { return m_packageLocation; } + const Msix::PackageVersion& AvailableVersion() const { return m_availableVersion; } + + private: + std::string m_packageLocation; + Msix::PackageVersion m_availableVersion; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 53a73e1835..6af448b163 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -2,212 +2,166 @@ // Licensed under the MIT License. #include "pch.h" #include "Microsoft/PreIndexedPackageSourceFactory.h" -#include "Microsoft/SQLiteIndex.h" +#include "Microsoft/PreIndexed/IndexForm.h" +#include "Microsoft/PreIndexed/PackageStore.h" #include "Microsoft/SQLiteIndexSource.h" #include "SourceUpdateChecks.h" #include -#include -#include -#include -#include -#include using namespace std::string_literals; using namespace std::string_view_literals; +using namespace AppInstaller::Repository::Microsoft::PreIndexed; namespace AppInstaller::Repository::Microsoft { namespace { - static constexpr std::string_view s_PreIndexedPackageSourceFactory_PackageFileName = "source.msix"sv; - static constexpr std::string_view s_PreIndexedPackageSourceFactory_V2_PackageFileName = "source2.msix"sv; - static constexpr std::string_view s_PreIndexedPackageSourceFactory_PackageVersionHeader = "x-ms-meta-sourceversion"sv; - static constexpr std::string_view s_PreIndexedPackageSourceFactory_IndexFileName = "index.db"sv; - // TODO: This being hard coded to force using the Public directory name is not ideal. - static constexpr std::string_view s_PreIndexedPackageSourceFactory_IndexFilePath = "Public\\index.db"sv; - - // Construct the package location from the given details. - // Currently expects that the arg is an https uri pointing to the root of the data. - std::string GetPackageLocation(const std::string& basePath, std::string_view fileName) + // Measures how long opening a source took, and which path got there. + struct SourceOpenTimer { - std::string result = basePath; - if (result.back() != '/') + using clock = std::chrono::steady_clock; + + SourceOpenTimer(const std::string& sourceName) : m_sourceName(sourceName), m_start(clock::now()) {} + + ~SourceOpenTimer() { - result += '/'; + const auto totalMs = std::chrono::duration_cast(clock::now() - m_start).count(); + AICLI_LOG(Repo, Info, << "Packaged source open for '" << m_sourceName << "' " << (m_succeeded ? "succeeded" : "failed") << + " in " << totalMs << " ms [mode=" << (m_usedLockFallback ? "fallbackLocked" : "optimistic") << "]"); } - result += fileName; - return result; - } - // Gets the set of package locations that should be tried, in order. - std::vector GetPackageLocations(const SourceDetails& details) - { - THROW_HR_IF(E_INVALIDARG, details.Arg.empty()); - - std::vector result; + void MarkFallbackLocked() { m_usedLockFallback = true; } + void MarkSucceeded() { m_succeeded = true; } - result.emplace_back(GetPackageLocation(details.Arg, s_PreIndexedPackageSourceFactory_V2_PackageFileName)); - result.emplace_back(GetPackageLocation(details.Arg, s_PreIndexedPackageSourceFactory_PackageFileName)); + private: + const std::string& m_sourceName; + clock::time_point m_start; + bool m_succeeded = false; + bool m_usedLockFallback = false; + }; - if (!details.AlternateArg.empty()) + // A reference to a preindexed package source. + struct PreIndexedSourceReference : public ISourceReference + { + PreIndexedSourceReference(const SourceDetails& details) : m_details(details) { - result.emplace_back(GetPackageLocation(details.AlternateArg, s_PreIndexedPackageSourceFactory_V2_PackageFileName)); - result.emplace_back(GetPackageLocation(details.AlternateArg, s_PreIndexedPackageSourceFactory_PackageFileName)); + if (m_details.Identifier.empty() && !m_details.Data.empty()) + { + // We didn't use to store the source identifier, so we compute it here in case + // it's missing from the details. + m_details.Identifier = m_details.Data; + } } - return result; - } + std::string GetIdentifier() override { return m_details.Identifier; } - // Abstracts the fallback for package location when the MsixInfo is needed. - struct PreIndexedPackageInfo - { - template - PreIndexedPackageInfo(const SourceDetails& details, LocationCheck&& locationCheck) + SourceDetails& GetDetails() override { return m_details; }; + + bool ShouldUpdateBeforeOpen(const std::optional& requestedUpdateInterval) override { - std::vector potentialLocations = GetPackageLocations(details); + auto store = CreateCompositeStore(m_details); + auto form = CreateFullIndexForm(m_details); + auto currentVersion = form->GetHeldVersion(*store); - for (const auto& location : potentialLocations) + // If we can't find a good package, then we have to update to operate + if (!currentVersion) { - locationCheck(location); + AICLI_LOG(Repo, Verbose, << "Source `" << m_details.Name << "` has no data"); + return true; } - std::exception_ptr primaryException; + using namespace std::chrono_literals; + using clock = std::chrono::system_clock; + + // Attempt to convert the package version to a time_point + clock::time_point versionTime = Utility::GetTimePointFromVersion(currentVersion.value()); - for (const auto& location : potentialLocations) + // Since we expect that the version time indicates creation time, don't let it be far in the future. + auto now = clock::now(); + if (versionTime > now && versionTime - now > 24h) { - try - { - m_msixInfo = std::make_unique(location); - m_packageLocation = location; - return; - } - catch (...) - { - LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageInfo failed on location: %hs", location.c_str()); - if (!primaryException) - { - primaryException = std::current_exception(); - } - } + versionTime = clock::time_point::min(); } - std::rethrow_exception(primaryException); - } - - const std::string& PackageLocation() const { return m_packageLocation; } - Msix::MsixInfo& MsixInfo() { return *m_msixInfo; } + // Use the later of the version and last update times + clock::time_point timeToCheck = (versionTime > m_details.LastUpdateTime ? versionTime : m_details.LastUpdateTime); - private: - std::string m_packageLocation; - std::unique_ptr m_msixInfo; - }; + return IsAfterUpdateCheckTime(m_details.Name, timeToCheck, requestedUpdateInterval); + } - // Abstracts the fallback for package location when an update is being done. - struct PreIndexedPackageUpdateCheck - { - PreIndexedPackageUpdateCheck(const SourceDetails& details) + std::shared_ptr Open(IProgressCallback& progress) override { - std::vector potentialLocations = GetPackageLocations(details); + SourceOpenTimer openTimer{ m_details.Name }; - std::exception_ptr primaryException; + auto store = CreateCompositeStore(m_details); + auto form = CreateFullIndexForm(m_details); - for (const auto& location : potentialLocations) + std::optional index; + + if (store->AllowsUnlockedRead()) { + // The optimistic open reads in place, so it is attempted without the lock and + // retried under it only when that fails. try { - m_availableVersion = GetAvailableVersionFrom(location); - m_packageLocation = location; - return; + index.emplace(form->Open(*store, progress)); } catch (...) { - LOG_CAUGHT_EXCEPTION_MSG("PreIndexedPackageUpdateCheck failed on location: %hs", location.c_str()); - if (!primaryException) + if (progress.IsCancelledBy(CancelReason::Any)) { - primaryException = std::current_exception(); + throw; } + + LOG_CAUGHT_EXCEPTION_MSG("Optimistic packaged source open failed, retrying under lock for source: %hs", m_details.Name.c_str()); } } - std::rethrow_exception(primaryException); - } - - const std::string& PackageLocation() const { return m_packageLocation; } - const Msix::PackageVersion& AvailableVersion() const { return m_availableVersion; } - - private: - std::string m_packageLocation; - Msix::PackageVersion m_availableVersion; - - Msix::PackageVersion GetAvailableVersionFrom(const std::string& packageLocation) - { - if (Utility::IsUrlRemote(packageLocation)) + if (!index) { - std::map headers = Utility::GetHeaders(packageLocation); - auto itr = headers.find(std::string{ s_PreIndexedPackageSourceFactory_PackageVersionHeader }); - if (itr != headers.end()) + openTimer.MarkFallbackLocked(); + + auto lock = store->Lock(progress); + if (!lock) { - AICLI_LOG(Repo, Verbose, << "Header indicates version is: " << itr->second); - return { itr->second }; + return {}; } - // We did not find the header we were looking for, log the ones we did find - AICLI_LOG(Repo, Verbose, << "Did not find " << s_PreIndexedPackageSourceFactory_PackageVersionHeader << " in:\n" << [&]() - { - std::ostringstream headerLog; - for (const auto& header : headers) - { - headerLog << " " << header.first << " : " << header.second << '\n'; - } - return std::move(headerLog).str(); - }()); + index.emplace(form->Open(*store, progress)); } - AICLI_LOG(Repo, Verbose, << "Reading package data to determine version"); - Msix::MsixInfo info{ packageLocation }; - auto manifest = info.GetAppPackageManifests(); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, manifest.size() > 1); - THROW_HR_IF(E_UNEXPECTED, manifest.size() == 0); + if (progress.IsCancelledBy(CancelReason::Any)) + { + AICLI_LOG(Repo, Info, << "Cancelling open upon request"); + return {}; + } - return manifest[0].GetIdentity().GetVersion(); + openTimer.MarkSucceeded(); + return std::make_shared(m_details, std::move(index.value()), false, true); } - }; - - // Gets the package family name from the details. - std::string GetPackageFamilyNameFromDetails(const SourceDetails& details) - { - THROW_HR_IF(E_UNEXPECTED, details.Data.empty()); - return details.Data; - } - // Creates a name for the cross process reader-writer lock given the details. - std::string CreateNameForCPL(const SourceDetails& details) - { - // The only relevant data is the package family name - return "PreIndexedSourceCPL_"s + GetPackageFamilyNameFromDetails(details); - } + private: + SourceDetails m_details; + }; - // The base class for a package that comes from a preindexed packaged source. - struct PreIndexedFactoryBase : public ISourceFactory + // The factory for a preindexed package source. + struct PreIndexedFactory : public ISourceFactory { - std::string_view TypeName() const override final + std::string_view TypeName() const override { return PreIndexedPackageSourceFactory::Type(); } - std::shared_ptr Create(const SourceDetails& details) override final + std::shared_ptr Create(const SourceDetails& details) override { // With more than one source implementation, we will probably need to probe first THROW_HR_IF(E_INVALIDARG, !details.Type.empty() && details.Type != PreIndexedPackageSourceFactory::Type()); - return CreateInternal(details); + return std::make_shared(details); } - virtual std::shared_ptr CreateInternal(const SourceDetails& details) = 0; - - bool Add(SourceDetails& details, IProgressCallback& progress) override final + bool Add(SourceDetails& details, IProgressCallback& progress) override { if (details.Type.empty()) { @@ -220,659 +174,123 @@ namespace AppInstaller::Repository::Microsoft THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); } - PreIndexedPackageInfo packageInfo(details, [](const std::string& packageLocation) - { - THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_NOT_SECURE, Utility::IsUrlRemote(packageLocation) && !Utility::IsUrlSecure(packageLocation)); - }); - - AICLI_LOG(Repo, Info, << "Initializing source from: " << details.Name << " => " << packageInfo.PackageLocation()); + // Adding is the only operation that can establish what a source's packages are, so + // it is the only one that discovers them and writes the result back. + UpdateReport report; - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.MsixInfo().GetIsBundle()); + auto form = CreateFullIndexForm(details); + auto data = form->DiscoverIdentities(progress); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING, !data); - auto fullName = packageInfo.MsixInfo().GetPackageFullName(); - AICLI_LOG(Repo, Info, << "Found package full name: " << details.Name << " => " << fullName); + WriteDiscoveredIdentities(details, data.value()); - details.Data = Msix::GetPackageFamilyNameFromFullName(fullName); - details.Identifier = Msix::GetPackageFamilyNameFromFullName(fullName); + auto store = CreateStore(details); + UpdateResult result = form->Update(*store, false, progress, report); - auto lock = LockExclusive(details, progress); - if (!lock) - { - return false; - } - - std::optional downloadedBytes; - bool result = UpdateInternal(packageInfo.PackageLocation(), details, progress, downloadedBytes); - - if (downloadedBytes) + // Unlike an update, an add reports only when something was transferred. + if (report.DownloadedBytes && report.NewIndexPublishedAt) { try { - auto manifests = packageInfo.MsixInfo().GetAppPackageManifests(); - if (!manifests.empty()) - { - Logging::Telemetry().LogPreindexedPackageUpdate( - details.Identifier, - std::nullopt, - Utility::GetTimePointFromVersion(manifests[0].GetIdentity().GetVersion()), - false, - std::nullopt, - std::nullopt, - false, - downloadedBytes.value(), - true); - } + Logging::Telemetry().LogPreindexedPackageUpdate( + details.Identifier, + report.PreviousIndexPublishedAt, + report.NewIndexPublishedAt.value(), + report.UsedDeltaDownload, + report.PreviousBaselinePublishedAt, + report.NewBaselinePublishedAt, + report.BaselineUpdated, + report.DownloadedBytes.value(), + true); } CATCH_LOG(); } - return result; + return result == UpdateResult::Success; } - bool Update(const SourceDetails& details, IProgressCallback& progress) override final + bool Update(const SourceDetails& details, IProgressCallback& progress) override { return UpdateBase(details, false, progress); } - bool BackgroundUpdate(const SourceDetails& details, IProgressCallback& progress) override final + bool BackgroundUpdate(const SourceDetails& details, IProgressCallback& progress) override { return UpdateBase(details, true, progress); } - // Retrieves the currently cached version of the package. - virtual std::optional GetCurrentVersion(const SourceDetails& details) = 0; - - virtual bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress, std::optional& downloadedBytes) = 0; - - bool Remove(const SourceDetails& details, IProgressCallback& progress) override final - { - THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); - auto lock = LockExclusive(details, progress); - if (!lock) - { - return false; - } - - return RemoveInternal(details, progress); - } - - virtual bool RemoveInternal(const SourceDetails& details, IProgressCallback&) = 0; - - private: - Synchronization::CrossProcessLock LockExclusive(const SourceDetails& details, IProgressCallback& progress, bool isBackground = false) - { - Synchronization::CrossProcessLock result(CreateNameForCPL(details)); - - if (isBackground) - { - // If this is a background update, don't wait on the lock. - result.TryAcquireNoWait(); - } - else - { - result.Acquire(progress); - } - - return result; - } - - bool UpdateBase(const SourceDetails& details, bool isBackground, IProgressCallback& progress) + bool Remove(const SourceDetails& details, IProgressCallback& progress) override { THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); - std::optional currentVersion = GetCurrentVersion(details); - PreIndexedPackageUpdateCheck updateCheck(details); - - if (currentVersion) - { - if (currentVersion.value() >= updateCheck.AvailableVersion()) - { - AICLI_LOG(Repo, Verbose, << "Remote source data (" << updateCheck.AvailableVersion().ToString() << - ") was not newer than existing (" << currentVersion.value().ToString() << "), no update needed"); - return true; - } - else - { - AICLI_LOG(Repo, Verbose, << "Remote source data (" << updateCheck.AvailableVersion().ToString() << - ") was newer than existing (" << currentVersion.value().ToString() << "), updating"); - } - } - - if (progress.IsCancelledBy(CancelReason::Any)) - { - AICLI_LOG(Repo, Info, << "Cancelling update upon request"); - return false; - } + auto store = CreateCompositeStore(details); - auto lock = LockExclusive(details, progress, isBackground); + auto lock = store->Lock(progress); if (!lock) { return false; } - std::optional downloadedBytes = 0; - bool result = UpdateInternal(updateCheck.PackageLocation(), details, progress, downloadedBytes); - - if (downloadedBytes) - { - std::optional previousIndexPublishedAt; - if (currentVersion) - { - previousIndexPublishedAt = Utility::GetTimePointFromVersion(currentVersion.value()); - } - - Logging::Telemetry().LogPreindexedPackageUpdate( - details.Identifier, - previousIndexPublishedAt, - Utility::GetTimePointFromVersion(updateCheck.AvailableVersion()), - false, - std::nullopt, - std::nullopt, - false, - downloadedBytes.value(), - !isBackground); - } - - return result; - } - }; - - // Optimistic packaged source open may call this without the cross process lock and retry under the lock on failure. - std::optional GetExtensionFromDetails(const SourceDetails& details) - { - Deployment::ExtensionCatalog catalog(Deployment::SourceExtensionName); - return catalog.FindByPackageFamilyAndId(GetPackageFamilyNameFromDetails(details), Deployment::IndexDBId); - } - - std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) - { - auto extension = GetExtensionFromDetails(details); - - if (extension) - { - auto version = extension->GetPackageVersion(); - return Msix::PackageVersion{ version.Major, version.Minor, version.Build, version.Revision }; - } - else - { - return std::nullopt; - } - } - - // Constructs the location that we will write files to. - std::filesystem::path GetStatePathFromDetails(const SourceDetails& details) - { - std::filesystem::path result = Runtime::GetPathTo(Runtime::PathName::LocalState); - result /= PreIndexedPackageSourceFactory::Type(); - result /= GetPackageFamilyNameFromDetails(details); - return result; - } - - std::optional DesktopContextGetCurrentVersion(const SourceDetails& details) - { - std::filesystem::path packageState = GetStatePathFromDetails(details); - std::filesystem::path packagePath = packageState / s_PreIndexedPackageSourceFactory_PackageFileName; - - if (std::filesystem::exists(packagePath)) - { - // If we already have a trusted index package, use it to determine if we need to update or not. - Msix::WriteLockedMsixFile indexPackage{ packagePath }; - if (indexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) - { - Msix::MsixInfo msixInfo{ packagePath }; - auto manifest = msixInfo.GetAppPackageManifests(); - - if (manifest.size() == 1) - { - return manifest[0].GetIdentity().GetVersion(); - } - } - } - - return std::nullopt; - } - - bool CheckForUpdateBeforeOpen(const SourceDetails& details, std::optional currentVersion, const std::optional& requestedUpdateInterval) - { - // If we can't find a good package, then we have to update to operate - if (!currentVersion) - { - AICLI_LOG(Repo, Verbose, << "Source `" << details.Name << "` has no data"); - return true; - } - - using namespace std::chrono_literals; - using clock = std::chrono::system_clock; - - // Attempt to convert the package version to a time_point - clock::time_point versionTime = Utility::GetTimePointFromVersion(currentVersion.value()); - - // Since we expect that the version time indicates creation time, don't let it be far in the future. - auto now = clock::now(); - if (versionTime > now && versionTime - now > 24h) - { - versionTime = clock::time_point::min(); - } - - // Use the later of the version and last update times - clock::time_point timeToCheck = (versionTime > details.LastUpdateTime ? versionTime : details.LastUpdateTime); - - return IsAfterUpdateCheckTime(details.Name, timeToCheck, requestedUpdateInterval); - } - - struct PackagedSourceOpenTimer - { - using clock = std::chrono::steady_clock; - - struct SingleTimer - { - SingleTimer(long long& durationMs) : m_durationMs(durationMs), m_start(clock::now()) {} - - ~SingleTimer() - { - Stop(); - } - - void Stop() - { - if (!m_stopped) - { - m_durationMs += std::chrono::duration_cast(clock::now() - m_start).count(); - m_stopped = true; - } - } - - private: - long long& m_durationMs; - clock::time_point m_start; - bool m_stopped = false; - }; - - PackagedSourceOpenTimer(const std::string& sourceName) : m_sourceName(sourceName), m_start(clock::now()) {} - - ~PackagedSourceOpenTimer() - { - const auto totalMs = std::chrono::duration_cast(clock::now() - m_start).count(); - AICLI_LOG(Repo, Info, << "Packaged source open for '" << m_sourceName << "' " << (m_succeeded ? "succeeded" : "failed") << - " in " << totalMs << " ms [extensionLookup=" << m_extensionLookupMs << - " ms, verifyContentIntegrity=" << m_verifyContentIntegrityMs << - " ms, sqliteOpen=" << m_sqliteOpenMs << " ms, mode=" << (m_usedLockFallback ? "fallbackLocked" : "optimistic") << "]"); - } - - SingleTimer MeasureExtensionLookup() { return SingleTimer{ m_extensionLookupMs }; } - SingleTimer MeasureVerifyContentIntegrity() { return SingleTimer{ m_verifyContentIntegrityMs }; } - SingleTimer MeasureSQLiteOpen() { return SingleTimer{ m_sqliteOpenMs }; } - void MarkFallbackLocked() { m_usedLockFallback = true; } - void MarkSucceeded() { m_succeeded = true; } - - private: - const std::string& m_sourceName; - clock::time_point m_start; - bool m_succeeded = false; - bool m_usedLockFallback = false; - long long m_extensionLookupMs = 0; - long long m_verifyContentIntegrityMs = 0; - long long m_sqliteOpenMs = 0; - }; - - SQLiteIndex OpenPackagedContextIndex(const SourceDetails& details, IProgressCallback& progress, PackagedSourceOpenTimer& openTimer) - { - auto extensionLookupTimer = openTimer.MeasureExtensionLookup(); - auto extension = GetExtensionFromDetails(details); - extensionLookupTimer.Stop(); - if (!extension) - { - AICLI_LOG(Repo, Info, << "Package not found " << details.Data); - THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING); - } - - auto verifyTimer = openTimer.MeasureVerifyContentIntegrity(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NEEDS_REMEDIATION), !extension->VerifyContentIntegrity(progress)); - verifyTimer.Stop(); - - // To work around an issue with accessing the public folder, we are temporarily - // constructing the location ourself. This was already the case for the non-packaged - // runtime, and we can fix both in the future. The only problem with this is that - // the directory in the extension *must* be Public, rather than one set by the creator. - std::filesystem::path indexLocation = extension->GetPackagePath(); - indexLocation /= s_PreIndexedPackageSourceFactory_IndexFilePath; - - auto sqliteOpenTimer = openTimer.MeasureSQLiteOpen(); - auto index = SQLiteIndex::Open(indexLocation.u8string(), SQLiteIndex::OpenDisposition::Immutable); - sqliteOpenTimer.Stop(); - - return index; - } - - struct PackagedContextSourceReference : public ISourceReference - { - PackagedContextSourceReference(const SourceDetails& details) : m_details(details) - { - if (!m_details.Data.empty()) - { - m_details.Identifier = GetPackageFamilyNameFromDetails(details); - } - } - - std::string GetIdentifier() override { return m_details.Identifier; } - - SourceDetails& GetDetails() override { return m_details; }; - - bool ShouldUpdateBeforeOpen(const std::optional& requestedUpdateInterval) override - { - return CheckForUpdateBeforeOpen(m_details, PackagedContextGetCurrentVersion(m_details), requestedUpdateInterval); - } - - std::shared_ptr Open(IProgressCallback& progress) override - { - PackagedSourceOpenTimer openTimer{ m_details.Name }; - auto completeOpen = [&](SQLiteIndex index) - { - // We didn't use to store the source identifier, so we compute it here in case it's - // missing from the details. - m_details.Identifier = GetPackageFamilyNameFromDetails(m_details); - openTimer.MarkSucceeded(); - return std::make_shared(m_details, std::move(index), false, true); - }; - - std::optional index; - bool retryUnderLock = false; - - try - { - index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); - } - catch (...) - { - if (progress.IsCancelledBy(CancelReason::Any)) - { - throw; - } - - LOG_CAUGHT_EXCEPTION_MSG("Optimistic packaged source open failed, retrying under lock for source: %hs", m_details.Name.c_str()); - retryUnderLock = true; - } - - if (retryUnderLock) - { - openTimer.MarkFallbackLocked(); - Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); - if (!lock.Acquire(progress)) - { - return {}; - } - - index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); - } - - return completeOpen(std::move(index.value())); - } - - private: - SourceDetails m_details; - }; - - // Source factory for running within a packaged context - struct PackagedContextFactory : public PreIndexedFactoryBase - { - std::shared_ptr CreateInternal(const SourceDetails& details) override - { - return std::make_shared(details); - } - - std::optional GetCurrentVersion(const SourceDetails& details) override - { - return PackagedContextGetCurrentVersion(details); - } - - bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress, std::optional& downloadedBytes) override - { - // Due to complications with deployment, download the file and deploy from - // a local source while we investigate further. - bool download = Utility::IsUrlRemote(packageLocation); - std::filesystem::path localFile; - - if (download) - { - localFile = Runtime::GetPathTo(Runtime::PathName::Temp); - localFile /= GetPackageFamilyNameFromDetails(details) + ".msix"; - - auto downloadResult = Utility::Download(packageLocation, localFile, Utility::DownloadType::Index, progress); - downloadedBytes = downloadResult.SizeInBytes; - } - else - { - localFile = Utility::ConvertToUTF16(packageLocation); - } - - // Verify the local file - Msix::WriteLockedMsixFile fileLock{ localFile }; - Msix::MsixInfo localMsixInfo{ localFile }; - - // The package should not be a bundle - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, localMsixInfo.GetIsBundle()); - - // Ensure that family name has not changed - THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, - GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(localMsixInfo.GetPackageFullName())); - - if (!fileLock.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) - { - AICLI_LOG(Repo, Error, << "Source update failed. Source package failed trust validation."); - THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); - } - - winrt::Windows::Foundation::Uri uri = winrt::Windows::Foundation::Uri(localFile.c_str()); - Deployment::AddPackage( - uri, - Deployment::Options{ WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::Trusted) }, - progress); + auto form = CreateFullIndexForm(details); - if (download) + std::vector packages; + if (form->HasIdentities()) { - try - { - // If successful, delete the file - std::filesystem::remove(localFile); - } - CATCH_LOG(); + packages = form->GetPackages(); } - return true; - } - - bool RemoveInternal(const SourceDetails& details, IProgressCallback& callback) override - { - auto fullName = Msix::GetPackageFullNameFromFamilyName(GetPackageFamilyNameFromDetails(details)); - - if (!fullName) - { - AICLI_LOG(Repo, Info, << "No full name found for family name: " << GetPackageFamilyNameFromDetails(details)); - } - else - { - AICLI_LOG(Repo, Info, << "Removing package: " << *fullName); - Deployment::RemovePackage(*fullName, winrt::Windows::Management::Deployment::RemovalOptions::None, callback); - } + store->Remove(packages, progress); return true; } - }; - - struct DesktopContextSourceReference : public ISourceReference - { - DesktopContextSourceReference(const SourceDetails& details) : m_details(details) - { - if (!m_details.Data.empty()) - { - m_details.Identifier = GetPackageFamilyNameFromDetails(details); - } - } - - std::string GetIdentifier() override { return m_details.Identifier; } - - SourceDetails& GetDetails() override { return m_details; }; - - bool ShouldUpdateBeforeOpen(const std::optional& requestedUpdateInterval) override - { - return CheckForUpdateBeforeOpen(m_details, DesktopContextGetCurrentVersion(m_details), requestedUpdateInterval); - } - - std::shared_ptr Open(IProgressCallback& progress) override - { - Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); - if (!lock.Acquire(progress)) - { - return {}; - } - - std::filesystem::path packageLocation = GetStatePathFromDetails(m_details); - packageLocation /= s_PreIndexedPackageSourceFactory_PackageFileName; - - if (!std::filesystem::exists(packageLocation)) - { - AICLI_LOG(Repo, Info, << "Data not found at " << packageLocation); - THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING); - } - - // Put a write exclusive lock on the index package. - Msix::WriteLockedMsixFile indexPackage{ packageLocation }; - - // Validate index package trust info. - THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, !indexPackage.ValidateTrustInfo(WI_IsFlagSet(m_details.TrustLevel, SourceTrustLevel::StoreOrigin))); - - // Create a temp lock exclusive index file. - auto tempIndexFilePath = Runtime::GetNewTempFilePath(); - auto tempIndexFile = Utility::ManagedFile::CreateWriteLockedFile(tempIndexFilePath, GENERIC_WRITE, true); - - // Populate temp index file. - Msix::MsixInfo packageInfo(packageLocation); - packageInfo.WriteToFileHandle(s_PreIndexedPackageSourceFactory_IndexFilePath, tempIndexFile.GetFileHandle(), progress); - - if (progress.IsCancelledBy(CancelReason::Any)) - { - AICLI_LOG(Repo, Info, << "Cancelling open upon request"); - return {}; - } - - SQLiteIndex index = SQLiteIndex::Open(tempIndexFile.GetFilePath().u8string(), SQLiteIndex::OpenDisposition::Immutable, std::move(tempIndexFile)); - - // We didn't use to store the source identifier, so we compute it here in case it's - // missing from the details. - m_details.Identifier = GetPackageFamilyNameFromDetails(m_details); - return std::make_shared(m_details, std::move(index), false, true); - } private: - SourceDetails m_details; - }; - - // Source factory for running outside of a package. - struct DesktopContextFactory : public PreIndexedFactoryBase - { - std::shared_ptr CreateInternal(const SourceDetails& details) override + // Records what a form discovered about the source's packages. + static void WriteDiscoveredIdentities(SourceDetails& details, const std::string& data) { - return std::make_shared(details); + details.Data = data; + details.Identifier = data; } - std::optional GetCurrentVersion(const SourceDetails& details) override - { - return DesktopContextGetCurrentVersion(details); - } - - bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress, std::optional& downloadedBytes) override + bool UpdateBase(const SourceDetails& details, bool isBackground, IProgressCallback& progress) { - // We will extract the manifest and index files directly to this location - std::filesystem::path packageState = GetStatePathFromDetails(details); - std::filesystem::create_directories(packageState); - - std::filesystem::path packagePath = packageState / s_PreIndexedPackageSourceFactory_PackageFileName; - - std::filesystem::path tempPackagePath = packagePath.u8string() + ".dnld.msix"; - auto removeTempFileOnExit = wil::scope_exit([&]() - { - try - { - std::filesystem::remove(tempPackagePath); - } - catch (...) - { - AICLI_LOG(Repo, Info, << "Failed to remove temp index file at: " << tempPackagePath); - } - }); - - if (Utility::IsUrlRemote(packageLocation)) - { - auto downloadResult = AppInstaller::Utility::Download(packageLocation, tempPackagePath, AppInstaller::Utility::DownloadType::Index, progress); - downloadedBytes = downloadResult.SizeInBytes; - } - else - { - std::filesystem::copy(packageLocation, tempPackagePath); - progress.OnProgress(100, 100, ProgressType::Percent); - } - - if (progress.IsCancelledBy(CancelReason::Any)) - { - AICLI_LOG(Repo, Info, << "Cancelling update upon request"); - return false; - } - - { - // Extra scope to release the file lock right after trust validation. - Msix::WriteLockedMsixFile tempIndexPackage{ tempPackagePath }; - Msix::MsixInfo tempMsixInfo{ tempPackagePath }; - - // The package should not be a bundle - THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, tempMsixInfo.GetIsBundle()); - - // Ensure that family name has not changed - THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, - GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(tempMsixInfo.GetPackageFullName())); + THROW_HR_IF(E_INVALIDARG, details.Type != PreIndexedPackageSourceFactory::Type()); - if (!tempIndexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) - { - AICLI_LOG(Repo, Error, << "Source update failed. Source package failed trust validation."); - THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); - } - } + auto store = CreateStore(details); + auto form = CreateFullIndexForm(details); - std::filesystem::rename(tempPackagePath, packagePath); - AICLI_LOG(Repo, Info, << "Source update success."); + UpdateReport report; + UpdateResult result = form->Update(*store, isBackground, progress, report); - removeTempFileOnExit.release(); + LogUpdate(details, report, isBackground); - return true; + return result == UpdateResult::Success; } - bool RemoveInternal(const SourceDetails& details, IProgressCallback&) override + // Whether an update is reported is the form's decision; see UpdateReport::Reportable. + void LogUpdate(const SourceDetails& details, const UpdateReport& report, bool isBackground) { - std::filesystem::path packageState = GetStatePathFromDetails(details); - - if (!std::filesystem::exists(packageState)) + if (!report.Reportable || !report.NewIndexPublishedAt) { - AICLI_LOG(Repo, Info, << "No state found for source: " << packageState.u8string()); - } - else - { - AICLI_LOG(Repo, Info, << "Removing state found for source: " << packageState.u8string()); - std::filesystem::remove_all(packageState); + return; } - return true; + Logging::Telemetry().LogPreindexedPackageUpdate( + details.Identifier, + report.PreviousIndexPublishedAt, + report.NewIndexPublishedAt.value(), + report.UsedDeltaDownload, + report.PreviousBaselinePublishedAt, + report.NewBaselinePublishedAt, + report.BaselineUpdated, + report.DownloadedBytes.value_or(0), + !isBackground); } }; } std::unique_ptr PreIndexedPackageSourceFactory::Create() { - if (Runtime::IsRunningInPackagedContext()) - { - return std::make_unique(); - } - else - { - return std::make_unique(); - } + return std::make_unique(); } } diff --git a/src/AppInstallerSharedLib/Public/winget/Runtime.h b/src/AppInstallerSharedLib/Public/winget/Runtime.h index ac40d28023..5625737069 100644 --- a/src/AppInstallerSharedLib/Public/winget/Runtime.h +++ b/src/AppInstallerSharedLib/Public/winget/Runtime.h @@ -43,6 +43,9 @@ namespace AppInstaller::Runtime // Determines whether the process is running with administrator or system privileges. bool IsRunningAsAdminOrSystem(); + // Determines whether the process is running for an interactively logged on user. + bool IsRunningAsInteractiveUser(); + // Determines whether the current token can be elevated. // This only returns true for tokens that are TokenElevationTypeLimited. // Thus, it will only be true if: diff --git a/src/AppInstallerSharedLib/Runtime.cpp b/src/AppInstallerSharedLib/Runtime.cpp index 6cfaa3baaf..de48562489 100644 --- a/src/AppInstallerSharedLib/Runtime.cpp +++ b/src/AppInstallerSharedLib/Runtime.cpp @@ -214,6 +214,11 @@ namespace AppInstaller::Runtime return IsRunningAsAdmin() || IsRunningAsSystem(); } + bool IsRunningAsInteractiveUser() + { + return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_INTERACTIVE_RID); + } + bool IsRunningWithLimitedToken() { return wil::get_token_information() == TokenElevationTypeLimited;