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;