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); }