diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index 3f104dd6fa..02702b9cfd 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -281,6 +281,7 @@ minexample
minidump
minschema
missingdependency
+mkgmtime
MMmmbbbb
mof
monicka
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index 6ed7ea3ed6..9e94e68c91 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -204,6 +204,7 @@
+
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index cb51464dbc..4d68f04f77 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -323,6 +323,9 @@
Source Files\Repository
+
+ Source Files\Common
+
diff --git a/src/AppInstallerCLITests/DateTime.cpp b/src/AppInstallerCLITests/DateTime.cpp
new file mode 100644
index 0000000000..38575fee26
--- /dev/null
+++ b/src/AppInstallerCLITests/DateTime.cpp
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "TestCommon.h"
+#include
+
+using namespace AppInstaller::Utility;
+using namespace TestCommon;
+using namespace std::chrono;
+
+namespace Catch
+{
+ template<>
+ struct StringMaker
+ {
+ static std::string convert(const std::chrono::system_clock::time_point& value)
+ {
+ std::ostringstream stream;
+ OutputTimePoint(stream, value);
+ return std::move(stream).str();
+ }
+ };
+}
+
+void VerifyGetTimePointFromVersion(std::string_view version, int year, int month, int day, int hour, int minute)
+{
+ system_clock::time_point result = GetTimePointFromVersion(UInt64Version{ std::string{ version } });
+
+ tm time{};
+ auto tt = system_clock::to_time_t(result);
+ _gmtime64_s(&time, &tt);
+
+ REQUIRE(year == time.tm_year + 1900);
+ REQUIRE(month == time.tm_mon + 1);
+ REQUIRE(day == time.tm_mday);
+ REQUIRE(hour == time.tm_hour);
+ REQUIRE(minute == time.tm_min);
+}
+
+std::string StringFromTimePoint(system_clock::time_point input)
+{
+ tm time{};
+ auto tt = system_clock::to_time_t(input);
+ _gmtime64_s(&time, &tt);
+
+ std::ostringstream stream;
+ stream << time.tm_year + 1900 << '.' << ((time.tm_mon + 1) * 100) + time.tm_mday << '.' << ((time.tm_hour + 1) * 100) + time.tm_min;
+ return std::move(stream).str();
+}
+
+TEST_CASE("GetTimePointFromVersion", "[datetime]")
+{
+ // Years out of range
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "1969.1231.2459.0" }) == system_clock::time_point::min());
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "3001.101.100.0" }) == system_clock::time_point::min());
+
+ // Months out of range
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.1.100.0" }) == system_clock::time_point::min());
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.1301.100.0" }) == system_clock::time_point::min());
+
+ // Days out of range
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.100.100.0" }) == system_clock::time_point::min());
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.132.100.0" }) == system_clock::time_point::min());
+
+ // Hours out of range
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.101.0.0" }) == system_clock::time_point::min());
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.101.2500.0" }) == system_clock::time_point::min());
+
+ // Minutes out of range
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "2023.101.160.0" }) == system_clock::time_point::min());
+
+ // In range baseline
+ VerifyGetTimePointFromVersion("2023.101.100.0", 2023, 1, 1, 0, 0);
+
+ // Time for presents!
+ VerifyGetTimePointFromVersion("2023.1225.814.0", 2023, 12, 25, 7, 14);
+
+ // Epoch time
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ "1970.101.100.0" }) == system_clock::time_point{});
+
+ // Round trip now
+ system_clock::time_point now = system_clock::now();
+ REQUIRE(GetTimePointFromVersion(UInt64Version{ StringFromTimePoint(now) }) == time_point_cast(now));
+}
diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp
index 7e684b5f78..bb64689b63 100644
--- a/src/AppInstallerCLITests/InstallFlow.cpp
+++ b/src/AppInstallerCLITests/InstallFlow.cpp
@@ -644,17 +644,21 @@ TEST_CASE("InstallFlow_Portable_SymlinkCreationFail", "[InstallFlow][workflow]")
OverridePortableInstaller(installContext);
TestHook::SetCreateSymlinkResult_Override createSymlinkResultOverride(false);
const auto& targetDirectory = tempDirectory.GetPath();
+ const auto& portableTargetPath = targetDirectory / "AppInstallerTestExeInstaller.exe";
installContext.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Portable.yaml").GetPath().u8string());
installContext.Args.AddArg(Execution::Args::Type::InstallLocation, targetDirectory.u8string());
installContext.Args.AddArg(Execution::Args::Type::InstallScope, "user"sv);
InstallCommand install({});
install.Execute(installContext);
- INFO(installOutput.str());
- const auto& portableTargetPath = targetDirectory / "AppInstallerTestExeInstaller.exe";
- REQUIRE(std::filesystem::exists(portableTargetPath));
- REQUIRE(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory));
+ {
+ INFO(installOutput.str());
+
+ // Use CHECK to allow the uninstall to still occur
+ CHECK(std::filesystem::exists(portableTargetPath));
+ CHECK(AppInstaller::Registry::Environment::PathVariable(AppInstaller::Manifest::ScopeEnum::User).Contains(targetDirectory));
+ }
// Perform uninstall
std::ostringstream uninstallOutput;
diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp
index e0f4473365..dc8401ccfd 100644
--- a/src/AppInstallerCLITests/Sources.cpp
+++ b/src/AppInstallerCLITests/Sources.cpp
@@ -575,6 +575,7 @@ TEST_CASE("RepoSources_UpdateOnOpen", "[sources]")
bool updateCalledOnFactory = false;
TestSourceFactory factory{ SourcesTestSource::Create };
factory.OnUpdate = [&](const SourceDetails&) { updateCalledOnFactory = true; };
+ factory.ShouldUpdateBeforeOpenResult = true;
TestHook_SetSourceFactoryOverride(type, factory);
SetSetting(Stream::UserSources, s_SingleSource);
diff --git a/src/AppInstallerCLITests/TestSource.cpp b/src/AppInstallerCLITests/TestSource.cpp
index 29900632eb..7ad06c48b2 100644
--- a/src/AppInstallerCLITests/TestSource.cpp
+++ b/src/AppInstallerCLITests/TestSource.cpp
@@ -301,14 +301,20 @@ namespace TestCommon
std::shared_ptr TestSourceFactory::Create(const SourceDetails& details)
{
+ std::shared_ptr result;
+
if (OnOpenWithCustomHeader)
{
- return std::make_shared(details, OnOpenWithCustomHeader);
+ result = std::make_shared(details, OnOpenWithCustomHeader);
}
else
{
- return std::make_shared(details, OnOpen);
+ result = std::make_shared(details, OnOpen);
}
+
+ result->ShouldUpdateBeforeOpenResult = ShouldUpdateBeforeOpenResult;
+
+ return result;
}
bool TestSourceFactory::Add(SourceDetails& details, IProgressCallback&)
diff --git a/src/AppInstallerCLITests/TestSource.h b/src/AppInstallerCLITests/TestSource.h
index 38c8136c41..c1d64e627f 100644
--- a/src/AppInstallerCLITests/TestSource.h
+++ b/src/AppInstallerCLITests/TestSource.h
@@ -115,6 +115,9 @@ namespace TestCommon
bool SetCustomHeader(std::optional header) override { m_header = header; return true; }
+ bool ShouldUpdateBeforeOpenResult = false;
+ bool ShouldUpdateBeforeOpen(const std::optional&) override { return ShouldUpdateBeforeOpenResult; }
+
std::shared_ptr Open(AppInstaller::IProgressCallback&) override
{
if (m_onOpenWithCustomHeader)
@@ -156,6 +159,7 @@ namespace TestCommon
// Make copies of self when requested.
operator std::function()>();
+ bool ShouldUpdateBeforeOpenResult = false;
OpenFunctor OnOpen;
OpenFunctorWithCustomHeader OnOpenWithCustomHeader;
AddFunctor OnAdd;
diff --git a/src/AppInstallerCommonCore/Public/winget/MsixManifest.h b/src/AppInstallerCommonCore/Public/winget/MsixManifest.h
index a393775272..bf5eb15c87 100644
--- a/src/AppInstallerCommonCore/Public/winget/MsixManifest.h
+++ b/src/AppInstallerCommonCore/Public/winget/MsixManifest.h
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
-
#include "AppInstallerStrings.h"
#include "AppInstallerVersions.h"
diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
index ed017e7898..a509cc51ad 100644
--- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
+++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj
@@ -444,6 +444,7 @@
+
@@ -541,6 +542,7 @@
+
diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
index 5d7830e334..041a54dd37 100644
--- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
+++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters
@@ -414,6 +414,9 @@
Public\winget
+
+ Header Files
+
@@ -653,9 +656,12 @@
Source Files
-
+
Microsoft\Schema\Checkpoint_1_0
-
+
+
+ Source Files
+
diff --git a/src/AppInstallerRepositoryCore/ISource.h b/src/AppInstallerRepositoryCore/ISource.h
index ac01c377ec..ede591322d 100644
--- a/src/AppInstallerRepositoryCore/ISource.h
+++ b/src/AppInstallerRepositoryCore/ISource.h
@@ -86,6 +86,9 @@ namespace AppInstaller::Repository
// Set caller.
virtual void SetCaller(std::string) {}
+ // Determine if the source needs to be updated before being opened.
+ virtual bool ShouldUpdateBeforeOpen(const std::optional&) { return false; }
+
// Opens the source. This function should throw upon open failure rather than returning an empty pointer.
virtual std::shared_ptr Open(IProgressCallback& progress) = 0;
};
diff --git a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp
index e8a5e63e29..c1dd214a62 100644
--- a/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp
+++ b/src/AppInstallerRepositoryCore/Microsoft/PreIndexedPackageSourceFactory.cpp
@@ -4,7 +4,9 @@
#include "Microsoft/PreIndexedPackageSourceFactory.h"
#include "Microsoft/SQLiteIndex.h"
#include "Microsoft/SQLiteIndexSource.h"
+#include "SourceUpdateChecks.h"
+#include
#include
#include
#include
@@ -345,6 +347,82 @@ namespace AppInstaller::Repository::Microsoft
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 PackagedContextSourceReference : public ISourceReference
{
PackagedContextSourceReference(const SourceDetails& details) : m_details(details)
@@ -359,6 +437,11 @@ namespace AppInstaller::Repository::Microsoft
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
{
Synchronization::CrossProcessLock lock(CreateNameForCPL(m_details));
@@ -405,17 +488,7 @@ namespace AppInstaller::Repository::Microsoft
std::optional GetCurrentVersion(const SourceDetails& details) override
{
- 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;
- }
+ return PackagedContextGetCurrentVersion(details);
}
bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress) override
@@ -492,15 +565,6 @@ namespace AppInstaller::Repository::Microsoft
}
};
- // 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;
- }
-
struct DesktopContextSourceReference : public ISourceReference
{
DesktopContextSourceReference(const SourceDetails& details) : m_details(details)
@@ -515,6 +579,11 @@ namespace AppInstaller::Repository::Microsoft
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));
@@ -574,26 +643,7 @@ namespace AppInstaller::Repository::Microsoft
std::optional GetCurrentVersion(const SourceDetails& details) override
{
- 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;
+ return DesktopContextGetCurrentVersion(details);
}
bool UpdateInternal(const std::string& packageLocation, const SourceDetails& details, IProgressCallback& progress) override
diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp
index 8d8819eab7..0c514fe074 100644
--- a/src/AppInstallerRepositoryCore/RepositorySource.cpp
+++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp
@@ -11,6 +11,7 @@
#include "Microsoft/PreIndexedPackageSourceFactory.h"
#include "Rest/RestSourceFactory.h"
#include "PackageTrackingCatalogSourceFactory.h"
+#include "SourceUpdateChecks.h"
#ifndef AICLI_DISABLE_TEST_HOOKS
#include "Microsoft/ConfigurableTestSourceFactory.h"
@@ -50,11 +51,6 @@ namespace AppInstaller::Repository
}
}
- bool IsUpdateSuppressed(const SourceDetails& details)
- {
- return std::chrono::system_clock::now() < details.DoNotUpdateBefore;
- }
-
struct AddOrUpdateResult
{
bool UpdateChecked = false;
@@ -155,48 +151,6 @@ namespace AppInstaller::Repository
return (origin == SourceOrigin::Default || origin == SourceOrigin::GroupPolicy || origin == SourceOrigin::User);
}
- // Determines whether (and logs why) a source should be updated before it is opened.
- bool ShouldUpdateBeforeOpen(const SourceDetails& details, std::optional backgroundUpdateInterval)
- {
- if (!ContainsAvailablePackagesInternal(details.Origin))
- {
- return false;
- }
-
- // Do not update if we are still before the update block time.
- if (IsUpdateSuppressed(details))
- {
- AICLI_LOG(Repo, Info, << "Background update is suppressed until: " << details.DoNotUpdateBefore);
- return false;
- }
-
- constexpr static TimeSpan s_ZeroMins = 0min;
- TimeSpan autoUpdateTime;
- if (backgroundUpdateInterval.has_value())
- {
- autoUpdateTime = backgroundUpdateInterval.value();
- }
- else
- {
- autoUpdateTime = User().Get();
- }
-
- // A value of zero means no auto update, to get update the source run `winget update`
- if (autoUpdateTime != s_ZeroMins)
- {
- auto timeSinceLastUpdate = std::chrono::system_clock::now() - details.LastUpdateTime;
- if (timeSinceLastUpdate > autoUpdateTime)
- {
- AICLI_LOG(Repo, Info, << "Source past auto update time [" <<
- std::chrono::duration_cast(autoUpdateTime).count() << " mins]; it has been at least " <<
- std::chrono::duration_cast(timeSinceLastUpdate).count() << " mins");
- return true;
- }
- }
-
- return false;
- }
-
SourceDetails GetPredefinedSourceDetails(PredefinedSource source)
{
SourceDetails details;
@@ -704,9 +658,10 @@ namespace AppInstaller::Repository
// Check for updates before opening.
for (auto& sourceReference : m_sourceReferences)
{
- auto& details = sourceReference->GetDetails();
- if (ShouldUpdateBeforeOpen(details, m_backgroundUpdateInterval))
+ if (ShouldUpdateBeforeOpen(sourceReference.get(), m_backgroundUpdateInterval))
{
+ auto& details = sourceReference->GetDetails();
+
try
{
// TODO: Consider adding a context callback to indicate we are doing the same action
diff --git a/src/AppInstallerRepositoryCore/SourceUpdateChecks.cpp b/src/AppInstallerRepositoryCore/SourceUpdateChecks.cpp
new file mode 100644
index 0000000000..783bf95d7e
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/SourceUpdateChecks.cpp
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "SourceUpdateChecks.h"
+#include "ISource.h"
+#include
+
+using namespace std::chrono_literals;
+
+namespace AppInstaller::Repository
+{
+ bool ShouldUpdateBeforeOpen(ISourceReference* sourceReference, const std::optional& requestedUpdateInterval)
+ {
+ const SourceDetails& details = sourceReference->GetDetails();
+
+ // Always respect this value to prevent server overloading
+ if (IsBeforeDoNotUpdateBeforeTime(details))
+ {
+ return false;
+ }
+
+ // Allow the source reference to decide beyond this
+ return sourceReference->ShouldUpdateBeforeOpen(requestedUpdateInterval);
+ }
+
+ bool IsBeforeDoNotUpdateBeforeTime(const SourceDetails& details)
+ {
+ if (std::chrono::system_clock::now() < details.DoNotUpdateBefore)
+ {
+ AICLI_LOG(Repo, Info, << "Background update for `" << details.Name << "` is suppressed until: " << details.DoNotUpdateBefore);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ bool IsAfterUpdateCheckTime(const SourceDetails& details, std::optional requestedUpdateInterval)
+ {
+ return IsAfterUpdateCheckTime(details.Name, details.LastUpdateTime, requestedUpdateInterval);
+ }
+
+ bool IsAfterUpdateCheckTime(std::string_view name, std::chrono::system_clock::time_point lastUpdateTime, std::optional requestedUpdateInterval)
+ {
+ constexpr static TimeSpan s_ZeroMins = 0min;
+
+ TimeSpan autoUpdateTime;
+ if (requestedUpdateInterval)
+ {
+ autoUpdateTime = requestedUpdateInterval.value();
+ }
+ else
+ {
+ autoUpdateTime = Settings::User().Get();
+ }
+
+ // A value of zero means no auto update, to get update the source run `winget update`
+ if (autoUpdateTime != s_ZeroMins)
+ {
+ auto timeSinceLastUpdate = std::chrono::system_clock::now() - lastUpdateTime;
+ if (timeSinceLastUpdate > autoUpdateTime)
+ {
+ AICLI_LOG(Repo, Info, << "Source `" << name << "` after auto update time [" <<
+ (requestedUpdateInterval ? "(override) " : "") <<
+ std::chrono::duration_cast(autoUpdateTime).count() << " mins]; it has been at least " <<
+ std::chrono::duration_cast(timeSinceLastUpdate).count() << " mins");
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/AppInstallerRepositoryCore/SourceUpdateChecks.h b/src/AppInstallerRepositoryCore/SourceUpdateChecks.h
new file mode 100644
index 0000000000..250f8421ef
--- /dev/null
+++ b/src/AppInstallerRepositoryCore/SourceUpdateChecks.h
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "Public/winget/RepositorySource.h"
+#include
+
+namespace AppInstaller::Repository
+{
+ // Determines if the given source should update before opening.
+ bool ShouldUpdateBeforeOpen(ISourceReference* sourceReference, const std::optional& requestedUpdateInterval);
+
+ // Determines if the current time is before a previously stored "do note update before" time.
+ bool IsBeforeDoNotUpdateBeforeTime(const SourceDetails& details);
+
+ // Determines if the given details and desired update interval indicate an update check should occur.
+ bool IsAfterUpdateCheckTime(const SourceDetails& details, std::optional requestedUpdateInterval);
+
+ // Determines if the given details and desired update interval indicate an update check should occur.
+ bool IsAfterUpdateCheckTime(std::string_view name, std::chrono::system_clock::time_point lastUpdateTime, std::optional requestedUpdateInterval);
+}
diff --git a/src/AppInstallerSharedLib/DateTime.cpp b/src/AppInstallerSharedLib/DateTime.cpp
index 51ecc3c4fa..c7a03db6c3 100644
--- a/src/AppInstallerSharedLib/DateTime.cpp
+++ b/src/AppInstallerSharedLib/DateTime.cpp
@@ -80,4 +80,45 @@ namespace AppInstaller::Utility
{
return std::chrono::system_clock::from_time_t(static_cast(epoch));
}
+
+ std::chrono::system_clock::time_point GetTimePointFromVersion(const UInt64Version& version)
+ {
+ // Our custom format for converting UTC into a version is:
+ // Major :: `Year` [1, 9999]
+ // Minor :: `Month * 100 + Day` where Month [1, 12] and Day [1, 31]
+ // Build :: `Hour * 100 + Minute` where Hour [1, 24] and Minute [0, 59]
+ // Revision :: Milliseconds, but since no seconds are available we will disregard this
+
+ tm versionTime{};
+
+ // Limit to the range supported by _mkgmtime64, which is 1970 to 3000 (hello to Y3K maintainers from 2023!)
+ UINT64 majorVersion = version.Major();
+ if (majorVersion < 1970 || majorVersion > 3000)
+ {
+ return std::chrono::system_clock::time_point::min();
+ }
+ versionTime.tm_year = static_cast(majorVersion) - 1900;
+
+ UINT64 minorVersion = version.Minor();
+ UINT64 monthValue = minorVersion / 100;
+ UINT64 dayValue = minorVersion % 100;
+ if (monthValue < 1 || monthValue > 12 || dayValue < 1 || dayValue > 31)
+ {
+ return std::chrono::system_clock::time_point::min();
+ }
+ versionTime.tm_mon = static_cast(monthValue) - 1;
+ versionTime.tm_mday = static_cast(dayValue);
+
+ UINT64 buildVersion = version.Build();
+ UINT64 hourValue = buildVersion / 100;
+ UINT64 minuteValue = buildVersion % 100;
+ if (hourValue < 1 || hourValue > 24 || minuteValue > 59)
+ {
+ return std::chrono::system_clock::time_point::min();
+ }
+ versionTime.tm_hour = static_cast(hourValue) - 1;
+ versionTime.tm_min = static_cast(minuteValue);
+
+ return std::chrono::system_clock::from_time_t(_mkgmtime64(&versionTime));
+ }
}
diff --git a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h
index 9b11bda9d3..dc6d12915b 100644
--- a/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h
+++ b/src/AppInstallerSharedLib/Public/AppInstallerDateTime.h
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma once
-
+#include
#include
#include
@@ -26,4 +26,9 @@ namespace AppInstaller::Utility
// Converts the given unix epoch time to a system_clock::time_point.
std::chrono::system_clock::time_point ConvertUnixEpochToSystemClock(int64_t epoch);
+
+ // Converts the given package version into a time_point using our custom format.
+ // Ensure that the package is expected to use this format, or you may get strange times.
+ // If the version is not convertable, the minimum time is returned.
+ std::chrono::system_clock::time_point GetTimePointFromVersion(const UInt64Version& version);
}