From ebe046a66be1567aa5d5ccfd5ba3cd00a98502ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Thu, 2 Jul 2026 22:41:38 +0200 Subject: [PATCH 1/8] Fallback source package storage for noninteractive packaged runs --- .../PreIndexedPackageSourceFactory.cpp | 283 +++++++++++------- 1 file changed, 169 insertions(+), 114 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 53a73e1835..8c4d9aa68c 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -384,6 +384,8 @@ namespace AppInstaller::Repository::Microsoft return catalog.FindByPackageFamilyAndId(GetPackageFamilyNameFromDetails(details), Deployment::IndexDBId); } + std::optional DesktopContextGetCurrentVersion(const SourceDetails& details); + std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) { auto extension = GetExtensionFromDetails(details); @@ -395,7 +397,7 @@ namespace AppInstaller::Repository::Microsoft } else { - return std::nullopt; + return DesktopContextGetCurrentVersion(details); } } @@ -432,6 +434,105 @@ namespace AppInstaller::Repository::Microsoft return std::nullopt; } + 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 (...) + { + 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; + } + + 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); + 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) { // If we can't find a good package, then we have to update to operate @@ -602,7 +703,20 @@ namespace AppInstaller::Repository::Microsoft return {}; } - index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); + try + { + index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); + } + catch (...) + { + if (progress.IsCancelledBy(CancelReason::Any)) + { + throw; + } + + LOG_CAUGHT_EXCEPTION_MSG("Locked packaged source open failed, falling back to local state for source: %hs", m_details.Name.c_str()); + index.emplace(OpenDesktopContextIndex(m_details, progress)); + } } return completeOpen(std::move(index.value())); @@ -645,37 +759,60 @@ 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); - - if (download) + try { - try + Deployment::AddPackage( + uri, + Deployment::Options{ WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::Trusted) }, + progress); + } + catch (...) + { + if (progress.IsCancelledBy(CancelReason::Any)) { - // If successful, delete the file - std::filesystem::remove(localFile); + throw; } - CATCH_LOG(); + + LOG_CAUGHT_EXCEPTION_MSG("Packaged source deployment failed, falling back to local state for source: %hs", details.Name.c_str()); + return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + } + + if (!GetExtensionFromDetails(details)) + { + AICLI_LOG(Repo, Warning, << "Packaged source deployment completed, but the extension was not found; falling back to local state for source: " << details.Name); + return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); } return true; @@ -695,6 +832,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 +870,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 +897,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 From f9eebd345175f6d2c31a09504e90a7466450296b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Thu, 2 Jul 2026 23:00:32 +0200 Subject: [PATCH 2/8] Narrow source package fallback for logged-off users --- .../PreIndexedPackageSourceFactory.cpp | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 8c4d9aa68c..e6e5b82d3e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -434,6 +434,24 @@ namespace AppInstaller::Repository::Microsoft return std::nullopt; } + bool HasTrustedDesktopContextPackage(const SourceDetails& details) + { + try + { + return DesktopContextGetCurrentVersion(details).has_value(); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + return false; + } + } + + 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 @@ -707,15 +725,22 @@ namespace AppInstaller::Repository::Microsoft { index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); } - catch (...) + catch (const wil::ResultException& re) { if (progress.IsCancelledBy(CancelReason::Any)) { throw; } - LOG_CAUGHT_EXCEPTION_MSG("Locked packaged source open failed, falling back to local state for source: %hs", m_details.Name.c_str()); - index.emplace(OpenDesktopContextIndex(m_details, progress)); + if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING && HasTrustedDesktopContextPackage(m_details)) + { + AICLI_LOG(Repo, Warning, << "Packaged source extension was not found; using trusted local state fallback for source: " << m_details.Name); + index.emplace(OpenDesktopContextIndex(m_details, progress)); + } + else + { + throw; + } } } @@ -798,21 +823,31 @@ namespace AppInstaller::Repository::Microsoft Deployment::Options{ WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::Trusted) }, progress); } - catch (...) + catch (const wil::ResultException& re) { if (progress.IsCancelledBy(CancelReason::Any)) { throw; } - LOG_CAUGHT_EXCEPTION_MSG("Packaged source deployment failed, falling back to local state for source: %hs", details.Name.c_str()); - return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + 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; falling back to local state for source: " << details.Name); - return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + if (HasTrustedDesktopContextPackage(details)) + { + AICLI_LOG(Repo, Warning, << "Packaged source deployment completed, but the extension was not found; refreshing local state fallback for source: " << details.Name); + return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + } + + THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING, "Packaged source deployment completed, but the extension was not found for source: %hs", details.Name.c_str()); } return true; From 6e992df458e0559497b47939115372bbb9e416b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Thu, 2 Jul 2026 23:22:51 +0200 Subject: [PATCH 3/8] Handle stale source package fallback state --- .../PreIndexedPackageSourceFactory.cpp | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index e6e5b82d3e..bbe91c5c99 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -385,8 +385,9 @@ namespace AppInstaller::Repository::Microsoft } std::optional DesktopContextGetCurrentVersion(const SourceDetails& details); + std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details); - std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) + std::optional PackagedContextGetExtensionVersion(const SourceDetails& details) { auto extension = GetExtensionFromDetails(details); @@ -395,10 +396,20 @@ namespace AppInstaller::Repository::Microsoft auto version = extension->GetPackageVersion(); return Msix::PackageVersion{ version.Major, version.Minor, version.Build, version.Revision }; } - else + + return std::nullopt; + } + + std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) + { + auto extensionVersion = PackagedContextGetExtensionVersion(details); + + if (extensionVersion) { - return DesktopContextGetCurrentVersion(details); + return extensionVersion; } + + return TryGetDesktopContextCurrentVersion(details); } // Constructs the location that we will write files to. @@ -434,19 +445,29 @@ namespace AppInstaller::Repository::Microsoft return std::nullopt; } - bool HasTrustedDesktopContextPackage(const SourceDetails& details) + std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details) { try { - return DesktopContextGetCurrentVersion(details).has_value(); + return DesktopContextGetCurrentVersion(details); } catch (...) { - LOG_CAUGHT_EXCEPTION(); - return false; + LOG_CAUGHT_EXCEPTION_MSG("Failed to read local state source package version for source: %hs", details.Name.c_str()); + return std::nullopt; } } + bool HasTrustedDesktopContextPackage(const SourceDetails& details) + { + return TryGetDesktopContextCurrentVersion(details).has_value(); + } + + bool ShouldPreferDesktopContext(std::optional desktopVersion, std::optional packagedVersion) + { + return desktopVersion && (!packagedVersion || desktopVersion.value() > packagedVersion.value()); + } + bool IsDeploymentBlockedByUserLogOff(HRESULT hr) { return hr == HRESULT_FROM_WIN32(ERROR_DEPLOYMENT_BLOCKED_BY_USER_LOG_OFF); @@ -697,6 +718,27 @@ namespace AppInstaller::Repository::Microsoft std::optional index; bool retryUnderLock = false; + auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details); + auto packagedVersion = PackagedContextGetExtensionVersion(m_details); + if (ShouldPreferDesktopContext(desktopVersion, packagedVersion)) + { + 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, continuing with packaged source extension for source: %hs", m_details.Name.c_str()); + } + } + try { index.emplace(OpenPackagedContextIndex(m_details, progress, openTimer)); @@ -841,13 +883,14 @@ namespace AppInstaller::Repository::Microsoft if (!GetExtensionFromDetails(details)) { - if (HasTrustedDesktopContextPackage(details)) - { - AICLI_LOG(Repo, Warning, << "Packaged source deployment completed, but the extension was not found; refreshing local state fallback for source: " << details.Name); - return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); - } + 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); + } - THROW_HR_MSG(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING, "Packaged source deployment completed, but the extension was not found for source: %hs", details.Name.c_str()); + if (HasTrustedDesktopContextPackage(details)) + { + AICLI_LOG(Repo, Info, << "Refreshing local state fallback after packaged source deployment for source: " << details.Name); + UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); } return true; From 18af6558ab8ab5d53791bfebd1f8daf1bca5e339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Thu, 2 Jul 2026 23:51:06 +0200 Subject: [PATCH 4/8] Fix packaged source fallback review issues --- .../Microsoft/PreIndexedPackageSourceFactory.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index bbe91c5c99..078cf89c06 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -386,6 +386,7 @@ namespace AppInstaller::Repository::Microsoft std::optional DesktopContextGetCurrentVersion(const SourceDetails& details); std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details); + bool ShouldPreferDesktopContext(std::optional desktopVersion, std::optional packagedVersion); std::optional PackagedContextGetExtensionVersion(const SourceDetails& details) { @@ -403,13 +404,14 @@ namespace AppInstaller::Repository::Microsoft std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) { auto extensionVersion = PackagedContextGetExtensionVersion(details); + auto desktopVersion = TryGetDesktopContextCurrentVersion(details); - if (extensionVersion) + if (ShouldPreferDesktopContext(desktopVersion, extensionVersion)) { - return extensionVersion; + return desktopVersion; } - return TryGetDesktopContextCurrentVersion(details); + return extensionVersion; } // Constructs the location that we will write files to. @@ -725,6 +727,12 @@ namespace AppInstaller::Repository::Microsoft AICLI_LOG(Repo, Warning, << "Local state fallback is newer than packaged source extension; using fallback for source: " << m_details.Name); try { + Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); + if (!lock.Acquire(progress)) + { + return {}; + } + index.emplace(OpenDesktopContextIndex(m_details, progress)); return completeOpen(std::move(index.value())); } From 186bbe4726e736812afff911ef90a77d454e241e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Fri, 3 Jul 2026 00:27:29 +0200 Subject: [PATCH 5/8] Harden packaged source fallback refresh --- .../PreIndexedPackageSourceFactory.cpp | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 078cf89c06..7251b120b9 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -720,30 +720,33 @@ namespace AppInstaller::Repository::Microsoft std::optional index; bool retryUnderLock = false; - auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details); auto packagedVersion = PackagedContextGetExtensionVersion(m_details); - if (ShouldPreferDesktopContext(desktopVersion, packagedVersion)) + { - AICLI_LOG(Repo, Warning, << "Local state fallback is newer than packaged source extension; using fallback for source: " << m_details.Name); - try + Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); + if (!lock.Acquire(progress)) { - Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); - if (!lock.Acquire(progress)) - { - return {}; - } - - index.emplace(OpenDesktopContextIndex(m_details, progress)); - return completeOpen(std::move(index.value())); + return {}; } - catch (...) + + auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details); + if (ShouldPreferDesktopContext(desktopVersion, packagedVersion)) { - if (progress.IsCancelledBy(CancelReason::Any)) + AICLI_LOG(Repo, Warning, << "Local state fallback is newer than packaged source extension; using fallback for source: " << m_details.Name); + try { - throw; + 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, continuing with packaged source extension for source: %hs", m_details.Name.c_str()); + LOG_CAUGHT_EXCEPTION_MSG("Newer local state fallback failed to open, continuing with packaged source extension for source: %hs", m_details.Name.c_str()); + } } } @@ -898,7 +901,19 @@ namespace AppInstaller::Repository::Microsoft if (HasTrustedDesktopContextPackage(details)) { AICLI_LOG(Repo, Info, << "Refreshing local state fallback after packaged source deployment for source: " << details.Name); - UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); + try + { + 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()); + } } return true; From 84fad21214daa370e1ed95a1d3e0db82d1b6f445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Fri, 3 Jul 2026 00:57:19 +0200 Subject: [PATCH 6/8] Protect packaged fallback updates --- .../Microsoft/PreIndexedPackageSourceFactory.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 7251b120b9..3b7afb25a8 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -404,7 +404,17 @@ namespace AppInstaller::Repository::Microsoft std::optional PackagedContextGetCurrentVersion(const SourceDetails& details) { auto extensionVersion = PackagedContextGetExtensionVersion(details); - auto desktopVersion = TryGetDesktopContextCurrentVersion(details); + std::optional desktopVersion; + + Synchronization::CrossProcessLock lock(CreateNameForCPL(details)); + if (lock.TryAcquireNoWait()) + { + desktopVersion = TryGetDesktopContextCurrentVersion(details); + } + else + { + AICLI_LOG(Repo, Verbose, << "Skipping local state fallback version probe because source lock is held for source: " << details.Name); + } if (ShouldPreferDesktopContext(desktopVersion, extensionVersion)) { @@ -496,6 +506,8 @@ namespace AppInstaller::Repository::Microsoft } }); + std::filesystem::remove(tempPackagePath); + if (Utility::IsUrlRemote(packageLocation)) { auto downloadResult = AppInstaller::Utility::Download(packageLocation, tempPackagePath, AppInstaller::Utility::DownloadType::Index, progress); @@ -503,7 +515,7 @@ namespace AppInstaller::Repository::Microsoft } else { - std::filesystem::copy(packageLocation, tempPackagePath); + std::filesystem::copy(packageLocation, tempPackagePath, std::filesystem::copy_options::overwrite_existing); progress.OnProgress(100, 100, ProgressType::Percent); } From 12ccebc78c76a7006dff06a564dfe68c72f401aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Fri, 3 Jul 2026 01:20:49 +0200 Subject: [PATCH 7/8] Keep packaged opens optimistic --- .../PreIndexedPackageSourceFactory.cpp | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp index 3b7afb25a8..d2c3d3807e 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -470,6 +470,18 @@ namespace AppInstaller::Repository::Microsoft } } + 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()); + } + } + bool HasTrustedDesktopContextPackage(const SourceDetails& details) { return TryGetDesktopContextCurrentVersion(details).has_value(); @@ -732,34 +744,37 @@ namespace AppInstaller::Repository::Microsoft std::optional index; bool retryUnderLock = false; - auto packagedVersion = PackagedContextGetExtensionVersion(m_details); - { Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details)); - if (!lock.Acquire(progress)) + if (lock.TryAcquireNoWait()) { - return {}; - } + auto packagedVersion = PackagedContextGetExtensionVersion(m_details); + auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details); - auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details); - if (ShouldPreferDesktopContext(desktopVersion, packagedVersion)) - { - 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 (ShouldPreferDesktopContext(desktopVersion, packagedVersion)) { - if (progress.IsCancelledBy(CancelReason::Any)) + AICLI_LOG(Repo, Warning, << "Local state fallback is newer than packaged source extension; using fallback for source: " << m_details.Name); + try { - throw; + 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, continuing with packaged source extension for source: %hs", m_details.Name.c_str()); + 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 From 77fa9964189a5866c7b87faccc039df1bc5bfbe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20K=C5=82ys?= Date: Sat, 19 Sep 2026 11:00:20 +0200 Subject: [PATCH 8/8] Validate and test packaged source fallback lifecycle --- .../PreIndexedPackageSource.cpp | 85 +++++++++++ .../PreIndexedPackageSourceFactory.cpp | 133 +++++++++++++----- .../PreIndexedPackageSourceFactory.h | 9 ++ 3 files changed, 188 insertions(+), 39 deletions(-) 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 d2c3d3807e..e301eb968a 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp @@ -385,8 +385,7 @@ namespace AppInstaller::Repository::Microsoft } std::optional DesktopContextGetCurrentVersion(const SourceDetails& details); - std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details); - bool ShouldPreferDesktopContext(std::optional desktopVersion, std::optional packagedVersion); + std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details, bool removeInvalid = false); std::optional PackagedContextGetExtensionVersion(const SourceDetails& details) { @@ -409,14 +408,14 @@ namespace AppInstaller::Repository::Microsoft Synchronization::CrossProcessLock lock(CreateNameForCPL(details)); if (lock.TryAcquireNoWait()) { - desktopVersion = TryGetDesktopContextCurrentVersion(details); + desktopVersion = TryGetDesktopContextCurrentVersion(details, true); } else { AICLI_LOG(Repo, Verbose, << "Skipping local state fallback version probe because source lock is held for source: " << details.Name); } - if (ShouldPreferDesktopContext(desktopVersion, extensionVersion)) + if (PreIndexedPackageSourceFactory::ShouldPreferDesktopContext(desktopVersion, extensionVersion, static_cast(lock))) { return desktopVersion; } @@ -433,63 +432,90 @@ 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))) { - Msix::MsixInfo msixInfo{ packagePath }; - auto manifest = msixInfo.GetAppPackageManifests(); + return { {}, true }; + } - if (manifest.size() == 1) - { - return manifest[0].GetIdentity().GetVersion(); - } + Msix::MsixInfo msixInfo{ packagePath }; + if (msixInfo.GetIsBundle() || + GetPackageFamilyNameFromDetails(details) != Msix::GetPackageFamilyNameFromFullName(msixInfo.GetPackageFullName())) + { + return { {}, true }; } + + auto manifest = msixInfo.GetAppPackageManifests(); + if (manifest.size() != 1) + { + return { {}, true }; + } + + return { manifest[0].GetIdentity().GetVersion(), false }; } - return std::nullopt; + return {}; } - std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details) + std::optional DesktopContextGetCurrentVersion(const SourceDetails& details) { - try - { - return DesktopContextGetCurrentVersion(details); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION_MSG("Failed to read local state source package version for source: %hs", details.Name.c_str()); - return std::nullopt; - } + return ProbeDesktopContextPackage(details).Version; } - void RemoveDesktopContextPackage(const SourceDetails& details) + std::optional TryGetDesktopContextCurrentVersion(const SourceDetails& details, bool removeInvalid) { try { - std::filesystem::remove(GetStatePathFromDetails(details) / s_PreIndexedPackageSourceFactory_PackageFileName); + 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 remove unusable local state source package for source: %hs", details.Name.c_str()); + 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 HasTrustedDesktopContextPackage(const SourceDetails& details) - { - return TryGetDesktopContextCurrentVersion(details).has_value(); - } - - bool ShouldPreferDesktopContext(std::optional desktopVersion, std::optional packagedVersion) + bool HasValidDesktopContextPackage(const SourceDetails& details) { - return desktopVersion && (!packagedVersion || desktopVersion.value() > packagedVersion.value()); + return TryGetDesktopContextCurrentVersion(details, true).has_value(); } bool IsDeploymentBlockedByUserLogOff(HRESULT hr) @@ -587,6 +613,9 @@ namespace AppInstaller::Repository::Microsoft // 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)) @@ -749,9 +778,11 @@ namespace AppInstaller::Repository::Microsoft if (lock.TryAcquireNoWait()) { auto packagedVersion = PackagedContextGetExtensionVersion(m_details); - auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details); + auto desktopVersion = TryGetDesktopContextCurrentVersion(m_details, true); - if (ShouldPreferDesktopContext(desktopVersion, packagedVersion)) + // 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 @@ -812,10 +843,24 @@ namespace AppInstaller::Repository::Microsoft throw; } - if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING && HasTrustedDesktopContextPackage(m_details)) + 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); - index.emplace(OpenDesktopContextIndex(m_details, progress)); + 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 { @@ -925,8 +970,10 @@ namespace AppInstaller::Repository::Microsoft return UpdateDesktopContextPackage(localFile.u8string(), details, progress, downloadedBytes); } - if (HasTrustedDesktopContextPackage(details)) + 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 { @@ -1058,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); }; }