diff --git a/src/AppInstallerCLITests/PreIndexedPackageSource.cpp b/src/AppInstallerCLITests/PreIndexedPackageSource.cpp index fe36dd1b31..844dc8e783 100644 --- a/src/AppInstallerCLITests/PreIndexedPackageSource.cpp +++ b/src/AppInstallerCLITests/PreIndexedPackageSource.cpp @@ -6,6 +6,7 @@ #include "TestSettings.h" #include #include +#include #include #include #include @@ -61,6 +62,24 @@ void CleanSources() fs::remove_all(GetPathToFileDir()); } +TEST_CASE("PIPS_PackagedFallbackPrecedence", "[pips]") +{ + using Factory = AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory; + + const std::optional older{ AppInstaller::Msix::PackageVersion{ "1.0.0.0" } }; + const std::optional newer{ AppInstaller::Msix::PackageVersion{ "2.0.0.0" } }; + + // A validated fallback is active while the extension is missing or older. + REQUIRE(Factory::ShouldPreferDesktopContext(newer, std::nullopt, true)); + REQUIRE(Factory::ShouldPreferDesktopContext(newer, older, true)); + + // The extension wins when it catches up, or when we cannot safely inspect the fallback. + REQUIRE_FALSE(Factory::ShouldPreferDesktopContext(newer, newer, true)); + REQUIRE_FALSE(Factory::ShouldPreferDesktopContext(older, newer, true)); + REQUIRE_FALSE(Factory::ShouldPreferDesktopContext(newer, older, false)); + REQUIRE_FALSE(Factory::ShouldPreferDesktopContext(std::nullopt, older, true)); +} + TEST_CASE("PIPS_Add", "[pips]") { if (!Runtime::IsRunningAsAdmin()) @@ -226,3 +245,69 @@ TEST_CASE("PIPS_Remove", "[pips]") UninstallCertFromSignedPackage(index); } } + +TEST_CASE("PIPS_RejectsDifferentFamilyCachedPackage", "[pips]") +{ + if (Runtime::IsRunningInPackagedContext()) + { + WARN("Test exercises the desktop source implementation. Skipped in packaged context."); + return; + } + + if (!Runtime::IsRunningAsAdmin()) + { + WARN("Test requires admin privilege. Skipped."); + return; + } + + CleanSources(); + + TempDirectory dir("pipssource"); + TestDataFile index(s_MsixFile_1); + CopyIndexFileToDirectory(index, dir); + + bool shouldCleanCert = InstallCertFromSignedPackage(index); + bool shouldCleanOtherCert = false; + TestDataFile otherPackage("TestSignedApp.msix"); + auto cleanup = wil::scope_exit([&]() + { + try + { + CleanSources(); + if (shouldCleanOtherCert) + { + UninstallCertFromSignedPackage(otherPackage); + } + if (shouldCleanCert) + { + UninstallCertFromSignedPackage(index); + } + } + CATCH_LOG(); + }); + + SourceDetails details; + details.Name = "TestName"; + details.Type = AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Type(); + details.Arg = dir; + ProgressCallback callback; + + AddSource(details, callback); + + Repository::Source configuredSource{ details.Name }; + auto factory = AppInstaller::Repository::Microsoft::PreIndexedPackageSourceFactory::Create(); + auto reference = factory->Create(configuredSource.GetDetails()); + + // Both desktop opens and packaged fallback opens use OpenDesktopContextIndex. + // Trust must pass so that this test reaches the family-name check. + shouldCleanOtherCert = InstallCertFromSignedPackage(otherPackage); + { + AppInstaller::Msix::WriteLockedMsixFile trustedOtherPackage{ otherPackage.GetPath() }; + REQUIRE(trustedOtherPackage.ValidateTrustInfo(false)); + AppInstaller::Msix::MsixInfo otherInfo{ otherPackage.GetPath() }; + REQUIRE(AppInstaller::Msix::GetPackageFamilyNameFromFullName(otherInfo.GetPackageFullName()) != std::string{ s_Msix_FamilyName }); + } + CopyIndexFileToDirectory(otherPackage, GetPathToFileDir()); + REQUIRE_THROWS_HR(reference->Open(callback), APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); + +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 53a73e1835..e301eb968a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -384,7 +384,10 @@ namespace AppInstaller::Repository::Microsoft return catalog.FindByPackageFamilyAndId(GetPackageFamilyNameFromDetails(details), Deployment::IndexDBId); } - std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) + std::optional DesktopContextGetCurrentVersion(const SourceDetails& details); + std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details, bool removeInvalid = false); + + std::optional PackagedContextGetExtensionVersion(const SourceDetails& details) { auto extension = GetExtensionFromDetails(details); @@ -393,10 +396,31 @@ namespace AppInstaller::Repository::Microsoft auto version = extension->GetPackageVersion(); return Msix::PackageVersion{ version.Major, version.Minor, version.Build, version.Revision }; } + + return std::nullopt; + } + + std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) + { + auto extensionVersion = PackagedContextGetExtensionVersion(details); + std::optional desktopVersion; + + Synchronization::CrossProcessLock lock(CreateNameForCPL(details)); + if (lock.TryAcquireNoWait()) + { + desktopVersion = TryGetDesktopContextCurrentVersion(details, true); + } else { - return std::nullopt; + AICLI_LOG(Repo, Verbose, << "Skipping local state fallback version probe because source lock is held for source: " << details.Name); } + + if (PreIndexedPackageSourceFactory::ShouldPreferDesktopContext(desktopVersion, extensionVersion, static_cast(lock))) + { + return desktopVersion; + } + + return extensionVersion; } // Constructs the location that we will write files to. @@ -408,28 +432,199 @@ namespace AppInstaller::Repository::Microsoft return result; } - std::optional DesktopContextGetCurrentVersion(const SourceDetails& details) + void RemoveDesktopContextPackage(const SourceDetails& details) + { + try + { + std::filesystem::remove(GetStatePathFromDetails(details) / s_PreIndexedPackageSourceFactory_PackageFileName); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("Failed to remove unusable local state source package for source: %hs", details.Name.c_str()); + } + } + + struct DesktopContextVersionProbe + { + std::optional Version; + bool Invalid = false; + }; + + DesktopContextVersionProbe ProbeDesktopContextPackage(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. + // A cached package must pass the same trust and identity checks as a fresh update. Msix::WriteLockedMsixFile indexPackage{ packagePath }; - if (indexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) + if (!indexPackage.ValidateTrustInfo(WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin))) + { + return { {}, true }; + } + + Msix::MsixInfo msixInfo{ packagePath }; + if (msixInfo.GetIsBundle() || + GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(msixInfo.GetPackageFullName())) { - Msix::MsixInfo msixInfo{ packagePath }; - auto manifest = msixInfo.GetAppPackageManifests(); + return { {}, true }; + } - if (manifest.size() == 1) + auto manifest = msixInfo.GetAppPackageManifests(); + if (manifest.size() != 1) + { + return { {}, true }; + } + + return { manifest[0].GetIdentity().GetVersion(), false }; + } + + return {}; + } + + std::optional DesktopContextGetCurrentVersion(const SourceDetails& details) + { + return ProbeDesktopContextPackage(details).Version; + } + + std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details, bool removeInvalid) + { + try + { + auto probe = ProbeDesktopContextPackage(details); + if (removeInvalid && probe.Invalid) + { + AICLI_LOG(Repo, Warning, << "Removing invalid local state fallback for source: " << details.Name); + RemoveDesktopContextPackage(details); + } + + return probe.Version; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("Failed to read local state source package version for source: %hs", details.Name.c_str()); + if (removeInvalid) + { + AICLI_LOG(Repo, Warning, << "Removing unreadable local state fallback for source: " << details.Name); + RemoveDesktopContextPackage(details); + } + return std::nullopt; + } + } + + bool HasValidDesktopContextPackage(const SourceDetails& details) + { + return TryGetDesktopContextCurrentVersion(details, true).has_value(); + } + + bool IsDeploymentBlockedByUserLogOff(HRESULT hr) + { + return hr == HRESULT_FROM_WIN32(ERROR_DEPLOYMENT_BLOCKED_BY_USER_LOG_OFF); + } + + bool UpdateDesktopContextPackage(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress, std::optional& downloadedBytes) + { + // 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 (...) { - return manifest[0].GetIdentity().GetVersion(); + AICLI_LOG(Repo, Info, << "Failed to remove temp index file at: " << tempPackagePath); } + }); + + std::filesystem::remove(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, std::filesystem::copy_options::overwrite_existing); + 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())); + + 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); } } - return std::nullopt; + std::filesystem::rename(tempPackagePath, packagePath); + AICLI_LOG(Repo, Info, << "Source update success."); + + removeTempFileOnExit.release(); + + return true; + } + + SQLiteIndex OpenDesktopContextIndex(const SourceDetails& details, IProgressCallback& progress) + { + std::filesystem::path packageLocation = GetStatePathFromDetails(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(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); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE, packageInfo.GetIsBundle()); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, + GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(packageInfo.GetPackageFullName())); + packageInfo.WriteToFileHandle(s_PreIndexedPackageSourceFactory_IndexFilePath, tempIndexFile.GetFileHandle(), progress); + + if (progress.IsCancelledBy(CancelReason::Any)) + { + AICLI_LOG(Repo, Info, << "Cancelling open upon request"); + THROW_HR(E_ABORT); + } + + return SQLiteIndex::Open(tempIndexFile.GetFilePath().u8string(), SQLiteIndex::OpenDisposition::Immutable, std::move(tempIndexFile)); } bool CheckForUpdateBeforeOpen(const SourceDetails& details, std::optional currentVersion, const std::optional& requestedUpdateInterval) @@ -578,6 +773,41 @@ namespace AppInstaller::Repository::Microsoft std::optional index; bool retryUnderLock = false; + { + Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); + if (lock.TryAcquireNoWait()) + { + auto packagedVersion = PackagedContextGetExtensionVersion(m_details); + auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details, true); + + // The fallback is a persistent recovery cache, not a mode tied to the current session. + // Once the deployed extension catches up it wins; the cache stays dormant for later failures. + if (PreIndexedPackageSourceFactory::ShouldPreferDesktopContext(desktopVersion, packagedVersion, static_cast(lock))) + { + AICLI_LOG(Repo, Warning, << "Local state fallback is newer than packaged source extension; using fallback for source: " << m_details.Name); + try + { + index.emplace(OpenDesktopContextIndex(m_details, progress)); + return completeOpen(std::move(index.value())); + } + catch (...) + { + if (progress.IsCancelledBy(CancelReason::Any)) + { + throw; + } + + LOG_CAUGHT_EXCEPTION_MSG("Newer local state fallback failed to open; removing it and continuing with packaged source extension for source: %hs", m_details.Name.c_str()); + RemoveDesktopContextPackage(m_details); + } + } + } + else + { + AICLI_LOG(Repo, Verbose, << "Skipping local state fallback probe because source lock is held for source: " << m_details.Name); + } + } + try { index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); @@ -602,7 +832,41 @@ namespace AppInstaller::Repository::Microsoft return {}; } - index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); + try + { + index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); + } + catch (const wil::ResultException& re) + { + if (progress.IsCancelledBy(CancelReason::Any)) + { + throw; + } + + if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING && HasValidDesktopContextPackage(m_details)) + { + AICLI_LOG(Repo, Warning, << "Packaged source extension was not found; using trusted local state fallback for source: " << m_details.Name); + try + { + index.emplace(OpenDesktopContextIndex(m_details, progress)); + } + catch (...) + { + if (progress.IsCancelledBy(CancelReason::Any)) + { + throw; + } + + LOG_CAUGHT_EXCEPTION_MSG("Local state fallback failed to open after packaged source extension was missing; removing it for source: %hs", m_details.Name.c_str()); + RemoveDesktopContextPackage(m_details); + THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING); + } + } + else + { + throw; + } + } } return completeOpen(std::move(index.value())); @@ -645,37 +909,85 @@ namespace AppInstaller::Repository::Microsoft localFile = Utility::ConvertToUTF16(packageLocation); } + auto removeLocalFileOnExit = wil::scope_exit([&]() + { + if (download) + { + try + { + std::filesystem::remove(localFile); + } + CATCH_LOG(); + } + }); + // Verify the local file - Msix::WriteLockedMsixFile fileLock{ localFile }; - Msix::MsixInfo localMsixInfo{ localFile }; + { + 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()); + // 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())); + // 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); + 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); + try + { + Deployment::AddPackage( + uri, + Deployment::Options{ WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::Trusted) }, + progress); + } + catch (const wil::ResultException& re) + { + if (progress.IsCancelledBy(CancelReason::Any)) + { + throw; + } - if (download) + if (IsDeploymentBlockedByUserLogOff(re.GetErrorCode())) + { + AICLI_LOG(Repo, Warning, << "Packaged source deployment was blocked because the user is logged off; using local state fallback for source: " << details.Name); + return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + } + + throw; + } + + if (!GetExtensionFromDetails(details)) { + AICLI_LOG(Repo, Warning, << "Packaged source deployment completed, but the extension was not found; using local state fallback for source: " << details.Name); + return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + } + + if (HasValidDesktopContextPackage(details)) + { + // Keep a verified standby copy after deployment. It becomes active only if it is newer + // than the extension or the extension disappears; source removal deletes it. + AICLI_LOG(Repo, Info, << "Refreshing local state fallback after packaged source deployment for source: " << details.Name); try { - // If successful, delete the file - std::filesystem::remove(localFile); + UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + } + catch (...) + { + if (progress.IsCancelledBy(CancelReason::Any)) + { + throw; + } + + LOG_CAUGHT_EXCEPTION_MSG("Failed to refresh local state fallback after packaged source deployment for source: %hs", details.Name.c_str()); } - CATCH_LOG(); } return true; @@ -695,6 +1007,13 @@ namespace AppInstaller::Repository::Microsoft Deployment::RemovePackage(*fullName, winrt::Windows::Management::Deployment::RemovalOptions::None, callback); } + std::filesystem::path packageState = GetStatePathFromDetails(details); + if (std::filesystem::exists(packageState)) + { + AICLI_LOG(Repo, Info, << "Removing local state found for source: " << packageState.u8string()); + std::filesystem::remove_all(packageState); + } + return true; } }; @@ -726,36 +1045,7 @@ namespace AppInstaller::Repository::Microsoft 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)); + SQLiteIndex index = OpenDesktopContextIndex(m_details, progress); // We didn't use to store the source identifier, so we compute it here in case it's // missing from the details. @@ -782,67 +1072,7 @@ namespace AppInstaller::Repository::Microsoft bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress, std::optional& downloadedBytes) override { - // 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())); - - 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); - } - } - - std::filesystem::rename(tempPackagePath, packagePath); - AICLI_LOG(Repo, Info, << "Source update success."); - - removeTempFileOnExit.release(); - - return true; + return UpdateDesktopContextPackage(packageLocation, details, progress, downloadedBytes); } bool RemoveInternal(const SourceDetails& details, IProgressCallback&) override @@ -875,4 +1105,12 @@ namespace AppInstaller::Repository::Microsoft return std::make_unique(); } } + + bool PreIndexedPackageSourceFactory::ShouldPreferDesktopContext( + const std::optional& fallbackVersion, + const std::optional& extensionVersion, + bool sourceLockAcquired) + { + return sourceLockAcquired && fallbackVersion && (!extensionVersion || fallbackVersion.value() > extensionVersion.value()); + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h index 838750a314..0d9005791e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.h @@ -3,7 +3,9 @@ #pragma once #include "ISource.h" #include "SourceFactory.h" +#include +#include #include namespace AppInstaller::Repository::Microsoft @@ -29,5 +31,12 @@ namespace AppInstaller::Repository::Microsoft // Creates a source factory for this type. static std::unique_ptr Create(); + + // A validated local fallback is active only while it is newer than the deployed extension, + // or while the extension is missing. A busy source lock always leaves the extension in charge. + static bool ShouldPreferDesktopContext( + const std::optional& fallbackVersion, + const std::optional& extensionVersion, + bool sourceLockAcquired); }; }