diff options
| author | zousar <[email protected]> | 2025-09-22 16:38:15 -0600 |
|---|---|---|
| committer | zousar <[email protected]> | 2025-09-22 16:38:15 -0600 |
| commit | 1c591941878e5c5009d51a82ff61986a63bd2e11 (patch) | |
| tree | 3860ab5ebddf1919aab3cca019422fa76cfcdda4 /src | |
| parent | Change batch put responses for client reporting (diff) | |
| parent | 5.7.2-pre1 (diff) | |
| download | zen-1c591941878e5c5009d51a82ff61986a63bd2e11.tar.xz zen-1c591941878e5c5009d51a82ff61986a63bd2e11.zip | |
Merge branch 'main' into zs/put-overwrite-policy-response
Diffstat (limited to 'src')
26 files changed, 1647 insertions, 842 deletions
diff --git a/src/zen/authutils.cpp b/src/zen/authutils.cpp new file mode 100644 index 000000000..69ea3f9c2 --- /dev/null +++ b/src/zen/authutils.cpp @@ -0,0 +1,246 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "authutils.h" + +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/logging.h> + +#include <zenhttp/auth/authmgr.h> +#include <zenhttp/httpclient.h> +#include <zenhttp/httpclientauth.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <json11.hpp> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { +using namespace std::literals; + +std::string_view +GetDefaultAccessTokenEnvVariableName() +{ +#if ZEN_PLATFORM_WINDOWS + return "UE-CloudDataCacheAccessToken"sv; +#endif +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + return "UE_CloudDataCacheAccessToken"sv; +#endif +} + +std::string +ReadAccessTokenFromJsonFile(const std::filesystem::path& Path) +{ + if (!IsFile(Path)) + { + throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); + } + IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); + std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()); + std::string JsonError; + json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); + if (!JsonError.empty()) + { + throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); + } + const std::string AuthToken = TokenInfo["Token"].string_value(); + if (AuthToken.empty()) + { + throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); + } + return AuthToken; +} + +std::filesystem::path +FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath) +{ + if (OidcTokenAuthExecutablePath.empty()) + { + const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; + std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); + if (IsFile(OidcTokenPath)) + { + return OidcTokenPath; + } + OidcTokenPath = (std::filesystem::current_path() / OidcExecutableName).make_preferred(); + if (IsFile(OidcTokenPath)) + { + return OidcTokenPath; + } + } + else + { + std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); + if (IsFile(OidcTokenPath)) + { + return OidcTokenPath; + } + } + return {}; +}; + +void +AuthCommandLineOptions::AddOptions(cxxopts::Options& Ops) +{ + // Direct access token (may expire) + Ops.add_option("auth-token", "", "access-token", "Remote host access token", cxxopts::value(m_AccessToken), "<accesstoken>"); + Ops.add_option("auth-token", + "", + "access-token-env", + "Name of environment variable that holds the remote host access token", + cxxopts::value(m_AccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), + "<envvariable>"); + Ops.add_option("auth-token", + "", + "access-token-path", + "Path to json file that holds the remote host access token", + cxxopts::value(m_AccessTokenPath), + "<filepath>"); + + // Auth manager token encryption + Ops.add_option("security", "", "encryption-aes-key", "256 bit AES encryption key", cxxopts::value<std::string>(m_EncryptionKey), ""); + Ops.add_option("security", + "", + "encryption-aes-iv", + "128 bit AES encryption initialization vector", + cxxopts::value<std::string>(m_EncryptionIV), + ""); + + // OpenId acccess token + Ops.add_option("openid", + "", + "openid-provider-name", + "Open ID provider name", + cxxopts::value<std::string>(m_OpenIdProviderName), + "Default"); + Ops.add_option("openid", "", "openid-provider-url", "Open ID provider url", cxxopts::value<std::string>(m_OpenIdProviderUrl), ""); + Ops.add_option("openid", "", "openid-client-id", "Open ID client id", cxxopts::value<std::string>(m_OpenIdClientId), ""); + Ops.add_option("openid", "", "openid-refresh-token", "Open ID refresh token", cxxopts::value<std::string>(m_OpenIdRefreshToken), ""); + + // OAuth acccess token + Ops.add_option("oauth", "", "oauth-url", "OAuth provier url", cxxopts::value<std::string>(m_OAuthUrl)->default_value(""), ""); + Ops.add_option("oauth", "", "oauth-clientid", "OAuth client id", cxxopts::value<std::string>(m_OAuthClientId)->default_value(""), ""); + Ops.add_option("oauth", + "", + "oauth-clientsecret", + "OAuth client secret", + cxxopts::value<std::string>(m_OAuthClientSecret)->default_value(""), + ""); + Ops.add_option("auth", + "", + "oidctoken-exe-path", + "Path to OidcToken executable", + cxxopts::value<std::string>(m_OidcTokenAuthExecutablePath)->default_value(""), + ""); + Ops.add_option("auth", + "", + "oidctoken-exe-unattended", + "Set mode to unattended when launcing OidcToken executable", + cxxopts::value<bool>(m_OidcTokenUnattended), + ""); +}; + +void +AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops, + const std::filesystem::path& SystemRootDir, + HttpClientSettings& ClientSettings, + std::string_view HostUrl, + std::unique_ptr<AuthMgr>& Auth, + bool Quiet) +{ + auto CreateAuthMgr = [&]() { + ZEN_ASSERT(!SystemRootDir.empty()); + if (!Auth) + { + if (m_EncryptionKey.empty()) + { + m_EncryptionKey = "abcdefghijklmnopqrstuvxyz0123456"; + if (!Quiet) + { + ZEN_CONSOLE_WARN("Using default encryption key"); + } + } + + if (m_EncryptionIV.empty()) + { + m_EncryptionIV = "0123456789abcdef"; + if (!Quiet) + { + ZEN_CONSOLE_WARN("Using default encryption initialization vector"); + } + } + + AuthConfig AuthMgrConfig = {.RootDirectory = SystemRootDir / "auth", + .EncryptionKey = AesKey256Bit::FromString(m_EncryptionKey), + .EncryptionIV = AesIV128Bit::FromString(m_EncryptionIV)}; + if (!AuthMgrConfig.EncryptionKey.IsValid()) + { + throw OptionParseException(fmt::format("'--encryption-aes-key' ('{}') is malformed", m_EncryptionKey), Ops.help()); + } + if (!AuthMgrConfig.EncryptionIV.IsValid()) + { + throw OptionParseException(fmt::format("'--encryption-aes-iv' ('{}') is malformed", m_EncryptionIV), Ops.help()); + } + Auth = AuthMgr::Create(AuthMgrConfig); + } + }; + + if (!m_OpenIdProviderUrl.empty() && !m_OpenIdClientId.empty()) + { + CreateAuthMgr(); + std::string ProviderName = m_OpenIdProviderName.empty() ? "Default" : m_OpenIdProviderName; + Auth->AddOpenIdProvider({.Name = ProviderName, .Url = m_OpenIdProviderUrl, .ClientId = m_OpenIdClientId}); + if (!m_OpenIdRefreshToken.empty()) + { + Auth->AddOpenIdToken({.ProviderName = ProviderName, .RefreshToken = m_OpenIdRefreshToken}); + } + } + + auto GetEnvAccessToken = [](const std::string& AccessTokenEnv) -> std::string { + if (!AccessTokenEnv.empty()) + { + return GetEnvVariable(AccessTokenEnv); + } + return {}; + }; + + if (!m_AccessToken.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(m_AccessToken); + } + else if (!m_AccessTokenPath.empty()) + { + MakeSafeAbsolutePathÍnPlace(m_AccessTokenPath); + std::string ResolvedAccessToken = ReadAccessTokenFromJsonFile(m_AccessTokenPath); + if (!ResolvedAccessToken.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); + } + } + else if (!m_OAuthUrl.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOAuthClientCredentials( + {.Url = m_OAuthUrl, .ClientId = m_OAuthClientId, .ClientSecret = m_OAuthClientSecret}); + } + else if (!m_OpenIdProviderName.empty()) + { + CreateAuthMgr(); + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOpenIdProvider(*Auth, m_OpenIdProviderName); + } + else if (std::string ResolvedAccessToken = GetEnvAccessToken(m_AccessTokenEnv); !ResolvedAccessToken.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); + } + else if (std::filesystem::path OidcTokenExePath = FindOidcTokenExePath(m_OidcTokenAuthExecutablePath); !OidcTokenExePath.empty()) + { + ClientSettings.AccessTokenProvider = + httpclientauth::CreateFromOidcTokenExecutable(OidcTokenExePath, HostUrl, Quiet, m_OidcTokenUnattended); + } + + if (!ClientSettings.AccessTokenProvider) + { + CreateAuthMgr(); + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromDefaultOpenIdProvider(*Auth); + } +} +} // namespace zen diff --git a/src/zen/authutils.h b/src/zen/authutils.h new file mode 100644 index 000000000..3cc9d4d4a --- /dev/null +++ b/src/zen/authutils.h @@ -0,0 +1,51 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "zen.h" + +namespace zen { + +struct HttpClientSettings; +class AuthMgr; + +struct AuthCommandLineOptions +{ + // Direct access token (may expire) + std::string m_AccessToken; + std::string m_AccessTokenEnv; + std::filesystem::path m_AccessTokenPath; + + // Auth manager token encryption + std::string m_EncryptionKey; // 256 bit AES encryption key + std::string m_EncryptionIV; // 128 bit AES initialization vector + + // OpenId acccess token + std::string m_OpenIdProviderName; + std::string m_OpenIdProviderUrl; + std::string m_OpenIdClientId; + std::string m_OpenIdRefreshToken; + + // OAuth acccess token + std::string m_OAuthUrl; + std::string m_OAuthClientId; + std::string m_OAuthClientSecret; + + std::string m_OidcTokenAuthExecutablePath; + bool m_OidcTokenUnattended = false; + + void AddOptions(cxxopts::Options& Ops); + + void ParseOptions(cxxopts::Options& Ops, + const std::filesystem::path& SystemRootDir, + HttpClientSettings& InOutClientSettings, + std::string_view HostUrl, + std::unique_ptr<AuthMgr>& OutAuthMgr, + bool Quiet); +}; + +std::string ReadAccessTokenFromJsonFile(const std::filesystem::path& Path); +std::string_view GetDefaultAccessTokenEnvVariableName(); +std::filesystem::path FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath); + +} // namespace zen diff --git a/src/zen/cmds/builds_cmd.cpp b/src/zen/cmds/builds_cmd.cpp index 949e40085..d9f57959e 100644 --- a/src/zen/cmds/builds_cmd.cpp +++ b/src/zen/cmds/builds_cmd.cpp @@ -31,6 +31,7 @@ #include <zenutil/chunkingcontroller.h> #include <zenutil/filebuildstorage.h> #include <zenutil/jupiter/jupiterbuildstorage.h> +#include <zenutil/jupiter/jupiterhost.h> #include <zenutil/jupiter/jupitersession.h> #include <zenutil/parallelwork.h> #include <zenutil/wildcard.h> @@ -39,7 +40,6 @@ #include <signal.h> #include <memory> -#include <regex> ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_map.h> @@ -823,72 +823,6 @@ namespace { return CleanWipe; } - bool ParseCloudUrl(std::string_view InUrl, - std::string& OutHost, - std::string& OutNamespace, - std::string& OutBucket, - std::string& OutBuildId) - { - std::string Url(InUrl); - const std::string_view ExtendedApiString = "api/v2/builds/"; - if (auto ApiString = ToLower(Url).find(ExtendedApiString); ApiString != std::string::npos) - { - Url.erase(ApiString, ExtendedApiString.length()); - } - - const std::string ArtifactURLRegExString = R"((http[s]?:\/\/.*?)\/(.*?)\/(.*?)\/(.*))"; - const std::regex ArtifactURLRegEx(ArtifactURLRegExString, std::regex::ECMAScript | std::regex::icase); - std::match_results<std::string_view::const_iterator> MatchResults; - std::string_view UrlToParse(Url); - if (regex_match(begin(UrlToParse), end(UrlToParse), MatchResults, ArtifactURLRegEx) && MatchResults.size() == 5) - { - auto GetMatch = [&MatchResults](uint32_t Index) -> std::string_view { - ZEN_ASSERT(Index < MatchResults.size()); - - const auto& Match = MatchResults[Index]; - - return std::string_view(&*Match.first, Match.second - Match.first); - }; - - const std::string_view Host = GetMatch(1); - const std::string_view Namespace = GetMatch(2); - const std::string_view Bucket = GetMatch(3); - const std::string_view BuildId = GetMatch(4); - - OutHost = Host; - OutNamespace = Namespace; - OutBucket = Bucket; - OutBuildId = BuildId; - return true; - } - else - { - return false; - } - } - - std::string ReadAccessTokenFromFile(const std::filesystem::path& Path) - { - if (!IsFile(Path)) - { - throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); - } - IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); - std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()); - std::string JsonError; - json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); - if (!JsonError.empty()) - { - throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); - } - const std::string AuthToken = TokenInfo["Token"].string_value(); - if (AuthToken.empty()) - { - throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); - } - return AuthToken; - } - IoBuffer WriteToTempFile(CompositeBuffer&& Buffer, const std::filesystem::path& TempFolderPath, const IoHash& Hash, @@ -1721,6 +1655,275 @@ namespace { return Result; } + struct BlockRangeDescriptor + { + uint32_t BlockIndex = (uint32_t)-1; + uint64_t RangeStart = 0; + uint64_t RangeLength = 0; + uint32_t ChunkBlockIndexStart = 0; + uint32_t ChunkBlockIndexCount = 0; + }; + + BlockRangeDescriptor MergeBlockRanges(std::span<const BlockRangeDescriptor> Ranges) + { + ZEN_ASSERT(Ranges.size() > 1); + const BlockRangeDescriptor& First = Ranges.front(); + const BlockRangeDescriptor& Last = Ranges.back(); + + return BlockRangeDescriptor{ + .BlockIndex = First.BlockIndex, + .RangeStart = First.RangeStart, + .RangeLength = Last.RangeStart + Last.RangeLength - First.RangeStart, + .ChunkBlockIndexStart = First.ChunkBlockIndexStart, + .ChunkBlockIndexCount = Last.ChunkBlockIndexStart + Last.ChunkBlockIndexCount - First.ChunkBlockIndexStart}; + } + + std::optional<std::vector<BlockRangeDescriptor>> MakeOptionalBlockRangeVector(uint64_t TotalBlockSize, + const BlockRangeDescriptor& Range) + { + if (Range.RangeLength == TotalBlockSize) + { + return {}; + } + else + { + return std::vector<BlockRangeDescriptor>{Range}; + } + }; + + struct BlockRangeLimit + { + uint16_t SizePercent; + uint16_t MaxRangeCount; + }; + + static const uint16_t FullBlockRangePercentLimit = 95; + + static const std::vector<BlockRangeLimit> ForceMergeLimits = {{.SizePercent = FullBlockRangePercentLimit, .MaxRangeCount = 1}, + {.SizePercent = 90, .MaxRangeCount = 2}, + {.SizePercent = 85, .MaxRangeCount = 8}, + {.SizePercent = 80, .MaxRangeCount = 16}, + {.SizePercent = 70, .MaxRangeCount = 32}, + {.SizePercent = 60, .MaxRangeCount = 48}, + {.SizePercent = 2, .MaxRangeCount = 56}, + {.SizePercent = 0, .MaxRangeCount = 64}}; + + const BlockRangeLimit* GetBlockRangeLimitForRange(std::span<const BlockRangeLimit> Limits, + uint64_t TotalBlockSize, + std::span<const BlockRangeDescriptor> Ranges) + { + if (Ranges.size() > 1) + { + const std::uint64_t WantedSize = + std::accumulate(Ranges.begin(), Ranges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) { + return Current + Range.RangeLength; + }); + + const double RangeRequestedPercent = (WantedSize * 100.0) / TotalBlockSize; + + for (const BlockRangeLimit& Limit : Limits) + { + if (RangeRequestedPercent >= Limit.SizePercent && Ranges.size() > Limit.MaxRangeCount) + { + return &Limit; + } + } + } + return nullptr; + }; + + std::vector<BlockRangeDescriptor> CollapseBlockRanges(const uint64_t AlwaysAcceptableGap, + std::span<const BlockRangeDescriptor> BlockRanges) + { + ZEN_ASSERT(BlockRanges.size() > 1); + std::vector<BlockRangeDescriptor> CollapsedBlockRanges; + + auto BlockRangesIt = BlockRanges.begin(); + CollapsedBlockRanges.push_back(*BlockRangesIt++); + for (; BlockRangesIt != BlockRanges.end(); BlockRangesIt++) + { + BlockRangeDescriptor& LastRange = CollapsedBlockRanges.back(); + + const uint64_t BothRangeSize = BlockRangesIt->RangeLength + LastRange.RangeLength; + + const uint64_t Gap = BlockRangesIt->RangeStart - (LastRange.RangeStart + LastRange.RangeLength); + if (Gap <= Max(BothRangeSize / 16, AlwaysAcceptableGap)) + { + LastRange.ChunkBlockIndexCount = + (BlockRangesIt->ChunkBlockIndexStart + BlockRangesIt->ChunkBlockIndexCount) - LastRange.ChunkBlockIndexStart; + LastRange.RangeLength = (BlockRangesIt->RangeStart + BlockRangesIt->RangeLength) - LastRange.RangeStart; + } + else + { + CollapsedBlockRanges.push_back(*BlockRangesIt); + } + } + + return CollapsedBlockRanges; + }; + + uint64_t CalculateNextGap(std::span<const BlockRangeDescriptor> BlockRanges) + { + ZEN_ASSERT(BlockRanges.size() > 1); + uint64_t AcceptableGap = (uint64_t)-1; + for (size_t RangeIndex = 0; RangeIndex < BlockRanges.size() - 1; RangeIndex++) + { + const BlockRangeDescriptor& Range = BlockRanges[RangeIndex]; + const BlockRangeDescriptor& NextRange = BlockRanges[RangeIndex + 1]; + + const uint64_t Gap = NextRange.RangeStart - (Range.RangeStart + Range.RangeLength); + AcceptableGap = Min(Gap, AcceptableGap); + } + AcceptableGap = RoundUp(AcceptableGap, 16u * 1024u); + return AcceptableGap; + }; + + std::optional<std::vector<BlockRangeDescriptor>> CalculateBlockRanges(uint32_t BlockIndex, + const ChunkBlockDescription& BlockDescription, + std::span<const uint32_t> BlockChunkIndexNeeded, + bool LimitToSingleRange, + const uint64_t ChunkStartOffsetInBlock, + const uint64_t TotalBlockSize, + uint64_t& OutTotalWantedChunksSize) + { + ZEN_TRACE_CPU("UpdateFolder_HandleBlocks_PartialAnalysis"); + + std::vector<BlockRangeDescriptor> BlockRanges; + { + uint64_t CurrentOffset = ChunkStartOffsetInBlock; + uint32_t ChunkBlockIndex = 0; + uint32_t NeedBlockChunkIndexOffset = 0; + BlockRangeDescriptor NextRange{.BlockIndex = BlockIndex}; + while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && ChunkBlockIndex < BlockDescription.ChunkRawHashes.size()) + { + const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; + if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) + { + if (NextRange.RangeLength > 0) + { + BlockRanges.push_back(NextRange); + NextRange = {.BlockIndex = BlockIndex}; + } + ChunkBlockIndex++; + CurrentOffset += ChunkCompressedLength; + } + else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) + { + if (NextRange.RangeLength == 0) + { + NextRange.RangeStart = CurrentOffset; + NextRange.ChunkBlockIndexStart = ChunkBlockIndex; + } + NextRange.RangeLength += ChunkCompressedLength; + NextRange.ChunkBlockIndexCount++; + ChunkBlockIndex++; + CurrentOffset += ChunkCompressedLength; + NeedBlockChunkIndexOffset++; + } + else + { + ZEN_ASSERT(false); + } + } + if (NextRange.RangeLength > 0) + { + BlockRanges.push_back(NextRange); + } + } + ZEN_ASSERT(!BlockRanges.empty()); + + OutTotalWantedChunksSize = + std::accumulate(BlockRanges.begin(), BlockRanges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) { + return Current + Range.RangeLength; + }); + + double RangeWantedPercent = (OutTotalWantedChunksSize * 100.0) / TotalBlockSize; + + if (BlockRanges.size() == 1) + { + ZEN_CONSOLE_VERBOSE("Range request of {} ({:.2f}%) using single range from block {} ({}) as is", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize)); + return BlockRanges; + } + + if (LimitToSingleRange) + { + const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges); + const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize; + const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength; + ZEN_CONSOLE_VERBOSE( + "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) limited to single block range {} ({:.2f}%) wasting " + "{:.2f}% ({})", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockRanges.size(), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + NiceBytes(MergedRange.RangeLength), + RangeRequestedPercent, + WastedPercent, + NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize)); + return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange); + } + + if (RangeWantedPercent > FullBlockRangePercentLimit) + { + const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges); + const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize; + const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength; + ZEN_CONSOLE_VERBOSE( + "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) exceeds {}%. Merged to single block range {} " + "({:.2f}%) wasting {:.2f}% ({})", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockRanges.size(), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + FullBlockRangePercentLimit, + NiceBytes(MergedRange.RangeLength), + RangeRequestedPercent, + WastedPercent, + NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize)); + return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange); + } + + std::vector<BlockRangeDescriptor> CollapsedBlockRanges = CollapseBlockRanges(16u * 1024u, BlockRanges); + while (GetBlockRangeLimitForRange(ForceMergeLimits, TotalBlockSize, CollapsedBlockRanges)) + { + CollapsedBlockRanges = CollapseBlockRanges(CalculateNextGap(CollapsedBlockRanges), CollapsedBlockRanges); + } + + const std::uint64_t WantedCollapsedSize = + std::accumulate(CollapsedBlockRanges.begin(), + CollapsedBlockRanges.end(), + uint64_t(0), + [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); + + const double CollapsedRangeRequestedPercent = (WantedCollapsedSize * 100.0) / TotalBlockSize; + + { + const double WastedPercent = ((WantedCollapsedSize - OutTotalWantedChunksSize) * 100.0) / WantedCollapsedSize; + + ZEN_CONSOLE_VERBOSE( + "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) collapsed to {} {:.2f}% using {} ranges wasting {:.2f}% " + "({})", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockRanges.size(), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + NiceBytes(WantedCollapsedSize), + CollapsedRangeRequestedPercent, + CollapsedBlockRanges.size(), + WastedPercent, + NiceBytes(WantedCollapsedSize - OutTotalWantedChunksSize)); + return CollapsedBlockRanges; + } + }; + class BufferedOpenFile { public: @@ -5758,17 +5961,42 @@ namespace { return Result; } + enum EPartialBlockRequestMode + { + Off, + ZenCacheOnly, + Mixed, + All, + Invalid + }; + static EPartialBlockRequestMode PartialBlockRequestModeFromString(const std::string_view ModeString) + { + switch (HashStringAsLowerDjb2(ModeString)) + { + case HashStringDjb2("false"): + return EPartialBlockRequestMode::Off; + case HashStringDjb2("zencacheonly"): + return EPartialBlockRequestMode::ZenCacheOnly; + case HashStringDjb2("mixed"): + return EPartialBlockRequestMode::Mixed; + case HashStringDjb2("true"): + return EPartialBlockRequestMode::All; + default: + return EPartialBlockRequestMode::Invalid; + } + } + struct UpdateOptions { - std::filesystem::path SystemRootDir; - std::filesystem::path ZenFolderPath; - std::uint64_t LargeAttachmentSize = DefaultPreferredMultipartChunkSize * 4u; - std::uint64_t PreferredMultipartChunkSize = DefaultPreferredMultipartChunkSize; - bool AllowPartialBlockRequests = true; - bool WipeTargetFolder = false; - bool PrimeCacheOnly = false; - bool EnableOtherDownloadsScavenging = true; - bool EnableTargetFolderScavenging = true; + std::filesystem::path SystemRootDir; + std::filesystem::path ZenFolderPath; + std::uint64_t LargeAttachmentSize = DefaultPreferredMultipartChunkSize * 4u; + std::uint64_t PreferredMultipartChunkSize = DefaultPreferredMultipartChunkSize; + EPartialBlockRequestMode PartialBlockRequestMode = EPartialBlockRequestMode::Mixed; + bool WipeTargetFolder = false; + bool PrimeCacheOnly = false; + bool EnableOtherDownloadsScavenging = true; + bool EnableTargetFolderScavenging = true; }; void UpdateFolder(StorageInstance& Storage, @@ -5788,7 +6016,8 @@ namespace { { ZEN_TRACE_CPU("UpdateFolder"); - ZEN_ASSERT((!Options.PrimeCacheOnly) || (Options.PrimeCacheOnly && (!Options.AllowPartialBlockRequests))); + ZEN_ASSERT((!Options.PrimeCacheOnly) || + (Options.PrimeCacheOnly && (Options.PartialBlockRequestMode == EPartialBlockRequestMode::Off))); Stopwatch IndexTimer; @@ -6558,33 +6787,20 @@ namespace { return NeededBlockChunkIndexes; }; - std::vector<uint32_t> CachedChunkBlockIndexes; - - struct BlockRangeDescriptor - { - uint32_t BlockIndex = (uint32_t)-1; - uint64_t RangeStart = 0; - uint64_t RangeLength = 0; - uint32_t ChunkBlockIndexStart = 0; - uint32_t ChunkBlockIndexCount = 0; - }; - std::vector<BlockRangeDescriptor> BlockRangeWorks; - - std::vector<uint32_t> FullBlockWorks; + std::vector<uint32_t> CachedChunkBlockIndexes; + std::vector<uint32_t> FetchBlockIndexes; + std::vector<std::vector<uint32_t>> AllBlockChunkIndexNeeded; for (uint32_t BlockIndex = 0; BlockIndex < BlockCount; BlockIndex++) { const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; - const std::vector<uint32_t> BlockChunkIndexNeeded = GetNeededChunkBlockIndexes(BlockDescription); + std::vector<uint32_t> BlockChunkIndexNeeded = GetNeededChunkBlockIndexes(BlockDescription); if (!BlockChunkIndexNeeded.empty()) { if (Options.PrimeCacheOnly) { - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); + FetchBlockIndexes.push_back(BlockIndex); } else { @@ -6603,188 +6819,13 @@ namespace { UsingCachedBlock = true; } } - if (!UsingCachedBlock) { - bool WantsToDoPartialBlockDownload = BlockChunkIndexNeeded.size() < BlockDescription.ChunkRawHashes.size(); - bool CanDoPartialBlockDownload = - (BlockDescription.HeaderSize > 0) && - (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size()); - if (Options.AllowPartialBlockRequests && WantsToDoPartialBlockDownload && CanDoPartialBlockDownload) - { - std::vector<BlockRangeDescriptor> BlockRanges; - - ZEN_TRACE_CPU("UpdateFolder_HandleBlocks_PartialAnalysis"); - - uint32_t NeedBlockChunkIndexOffset = 0; - uint32_t ChunkBlockIndex = 0; - uint32_t CurrentOffset = - gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); - - const uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), - BlockDescription.ChunkCompressedLengths.end(), - std::uint64_t(CurrentOffset)); - BlockRangeDescriptor NextRange{.BlockIndex = BlockIndex}; - while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && - ChunkBlockIndex < BlockDescription.ChunkRawHashes.size()) - { - const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; - if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) - { - if (NextRange.RangeLength > 0) - { - BlockRanges.push_back(NextRange); - NextRange = {.BlockIndex = BlockIndex}; - } - ChunkBlockIndex++; - CurrentOffset += ChunkCompressedLength; - } - else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) - { - if (NextRange.RangeLength == 0) - { - NextRange.RangeStart = CurrentOffset; - NextRange.ChunkBlockIndexStart = ChunkBlockIndex; - } - NextRange.RangeLength += ChunkCompressedLength; - NextRange.ChunkBlockIndexCount++; - ChunkBlockIndex++; - CurrentOffset += ChunkCompressedLength; - NeedBlockChunkIndexOffset++; - } - else - { - ZEN_ASSERT(false); - } - } - if (NextRange.RangeLength > 0) - { - BlockRanges.push_back(NextRange); - } - - ZEN_ASSERT(!BlockRanges.empty()); - - std::vector<BlockRangeDescriptor> CollapsedBlockRanges; - auto It = BlockRanges.begin(); - CollapsedBlockRanges.push_back(*It++); - while (It != BlockRanges.end()) - { - BlockRangeDescriptor& LastRange = CollapsedBlockRanges.back(); - uint64_t Slack = It->RangeStart - (LastRange.RangeStart + LastRange.RangeLength); - uint64_t BothRangeSize = It->RangeLength + LastRange.RangeLength; - if (Slack <= Max(BothRangeSize / 8, 64u * 1024u)) // Made up heuristic - we'll see how it pans out - { - LastRange.ChunkBlockIndexCount = - (It->ChunkBlockIndexStart + It->ChunkBlockIndexCount) - LastRange.ChunkBlockIndexStart; - LastRange.RangeLength = (It->RangeStart + It->RangeLength) - LastRange.RangeStart; - } - else - { - CollapsedBlockRanges.push_back(*It); - } - ++It; - } - - const std::uint64_t WantedSize = std::accumulate( - CollapsedBlockRanges.begin(), - CollapsedBlockRanges.end(), - uint64_t(0), - [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); - ZEN_ASSERT(WantedSize <= TotalBlockSize); - if (WantedSize > ((TotalBlockSize * 95) / 100)) - { - ZEN_CONSOLE_VERBOSE("Using more than 95% ({}) of block {} ({}), requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize)); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 9) / 10)) && CollapsedBlockRanges.size() > 1) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 90% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 8) / 10)) && (CollapsedBlockRanges.size() > 16)) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 80% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 7) / 10)) && (CollapsedBlockRanges.size() > 48)) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 70% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 6) / 10)) && (CollapsedBlockRanges.size() > 64)) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 60% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else - { - if (WantedSize > ((TotalBlockSize * 5) / 10)) - { - ZEN_CONSOLE_VERBOSE("Using {}% ({}) of block {} ({}) using {} requests, requesting partial block", - (WantedSize * 100) / TotalBlockSize, - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - } - TotalRequestCount += CollapsedBlockRanges.size(); - TotalPartWriteCount += CollapsedBlockRanges.size(); - - BlockRangeWorks.insert(BlockRangeWorks.end(), CollapsedBlockRanges.begin(), CollapsedBlockRanges.end()); - } - } - else - { - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } + FetchBlockIndexes.push_back(BlockIndex); } } } - else - { - ZEN_CONSOLE_VERBOSE("Skipping block {} due to cache reuse", BlockDescriptions[BlockIndex].BlockHash); - } + AllBlockChunkIndexNeeded.emplace_back(std::move(BlockChunkIndexNeeded)); } struct BlobsExistsResult @@ -6802,18 +6843,12 @@ namespace { tsl::robin_set<IoHash> BlobHashesSet; - BlobHashesSet.reserve(LooseChunkHashWorks.size() + FullBlockWorks.size()); + BlobHashesSet.reserve(LooseChunkHashWorks.size() + FetchBlockIndexes.size()); for (LooseChunkHashWorkData& LooseChunkHashWork : LooseChunkHashWorks) { BlobHashesSet.insert(RemoteContent.ChunkedContent.ChunkHashes[LooseChunkHashWork.RemoteChunkIndex]); } - for (const BlockRangeDescriptor& BlockRange : BlockRangeWorks) - { - const uint32_t BlockIndex = BlockRange.BlockIndex; - const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; - BlobHashesSet.insert(BlockDescription.BlockHash); - } - for (uint32_t BlockIndex : FullBlockWorks) + for (uint32_t BlockIndex : FetchBlockIndexes) { const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; BlobHashesSet.insert(BlockDescription.BlockHash); @@ -6847,6 +6882,162 @@ namespace { } } + std::vector<BlockRangeDescriptor> BlockRangeWorks; + std::vector<uint32_t> FullBlockWorks; + { + Stopwatch Timer; + + std::vector<uint32_t> PartialBlockIndexes; + + for (uint32_t BlockIndex : FetchBlockIndexes) + { + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + + const std::vector<uint32_t> BlockChunkIndexNeeded = std::move(AllBlockChunkIndexNeeded[BlockIndex]); + if (!BlockChunkIndexNeeded.empty()) + { + bool WantsToDoPartialBlockDownload = BlockChunkIndexNeeded.size() < BlockDescription.ChunkRawHashes.size(); + bool CanDoPartialBlockDownload = + (BlockDescription.HeaderSize > 0) && + (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size()); + + bool AllowedToDoPartialRequest = false; + bool BlockExistInCache = ExistsResult.ExistingBlobs.contains(BlockDescription.BlockHash); + switch (Options.PartialBlockRequestMode) + { + case EPartialBlockRequestMode::Off: + break; + case EPartialBlockRequestMode::ZenCacheOnly: + AllowedToDoPartialRequest = BlockExistInCache; + break; + case EPartialBlockRequestMode::Mixed: + case EPartialBlockRequestMode::All: + AllowedToDoPartialRequest = true; + break; + default: + ZEN_ASSERT(false); + break; + } + + const uint32_t ChunkStartOffsetInBlock = + gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); + + const uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), + BlockDescription.ChunkCompressedLengths.end(), + std::uint64_t(ChunkStartOffsetInBlock)); + + if (AllowedToDoPartialRequest && WantsToDoPartialBlockDownload && CanDoPartialBlockDownload) + { + ZEN_TRACE_CPU("UpdateFolder_HandleBlocks_PartialAnalysis"); + + bool LimitToSingleRange = + BlockExistInCache ? false : Options.PartialBlockRequestMode == EPartialBlockRequestMode::Mixed; + uint64_t TotalWantedChunksSize = 0; + std::optional<std::vector<BlockRangeDescriptor>> MaybeBlockRanges = + CalculateBlockRanges(BlockIndex, + BlockDescription, + BlockChunkIndexNeeded, + LimitToSingleRange, + ChunkStartOffsetInBlock, + TotalBlockSize, + TotalWantedChunksSize); + ZEN_ASSERT(TotalWantedChunksSize <= TotalBlockSize); + + if (MaybeBlockRanges.has_value()) + { + const std::vector<BlockRangeDescriptor>& BlockRanges = MaybeBlockRanges.value(); + ZEN_ASSERT(!BlockRanges.empty()); + BlockRangeWorks.insert(BlockRangeWorks.end(), BlockRanges.begin(), BlockRanges.end()); + TotalRequestCount += BlockRanges.size(); + TotalPartWriteCount += BlockRanges.size(); + + uint64_t RequestedSize = std::accumulate( + BlockRanges.begin(), + BlockRanges.end(), + uint64_t(0), + [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); + PartialBlockIndexes.push_back(BlockIndex); + + if (RequestedSize > TotalWantedChunksSize) + { + ZEN_CONSOLE_VERBOSE("Requesting {} chunks ({}) from block {} ({}) using {} requests (extra bytes {})", + BlockChunkIndexNeeded.size(), + NiceBytes(RequestedSize), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + BlockRanges.size(), + NiceBytes(RequestedSize - TotalWantedChunksSize)); + } + } + else + { + FullBlockWorks.push_back(BlockIndex); + TotalRequestCount++; + TotalPartWriteCount++; + } + } + else + { + FullBlockWorks.push_back(BlockIndex); + TotalRequestCount++; + TotalPartWriteCount++; + } + } + } + + if (!PartialBlockIndexes.empty()) + { + uint64_t TotalFullBlockRequestBytes = 0; + for (uint32_t BlockIndex : FullBlockWorks) + { + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + uint32_t CurrentOffset = + gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); + + TotalFullBlockRequestBytes += std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), + BlockDescription.ChunkCompressedLengths.end(), + std::uint64_t(CurrentOffset)); + } + + uint64_t TotalPartialBlockBytes = 0; + for (uint32_t BlockIndex : PartialBlockIndexes) + { + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + uint32_t CurrentOffset = + gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); + + TotalPartialBlockBytes += std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), + BlockDescription.ChunkCompressedLengths.end(), + std::uint64_t(CurrentOffset)); + } + + uint64_t NonPartialTotalBlockBytes = TotalFullBlockRequestBytes + TotalPartialBlockBytes; + + const uint64_t TotalPartialBlockRequestBytes = + std::accumulate(BlockRangeWorks.begin(), + BlockRangeWorks.end(), + uint64_t(0), + [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); + uint64_t TotalExtraPartialBlocksRequests = BlockRangeWorks.size() - PartialBlockIndexes.size(); + + uint64_t TotalSavedBlocksSize = TotalPartialBlockBytes - TotalPartialBlockRequestBytes; + double SavedSizePercent = (TotalSavedBlocksSize * 100.0) / NonPartialTotalBlockBytes; + + if (!IsQuiet) + { + ZEN_CONSOLE( + "Analisys of partial block requests saves download of {} out of {} ({:.1f}%) using {} extra " + "requests. Completed in {}", + NiceBytes(TotalSavedBlocksSize), + NiceBytes(NonPartialTotalBlockBytes), + SavedSizePercent, + TotalExtraPartialBlocksRequests, + NiceTimeSpanMs(ExistsResult.ElapsedTimeMs)); + } + // exit(0); + } + } + BufferedWriteFileCache WriteCache; for (uint32_t ScavengeOpIndex = 0; ScavengeOpIndex < ScavengeCopyOperations.size(); ScavengeOpIndex++) @@ -7204,7 +7395,7 @@ namespace { Storage.BuildCacheStorage->PutBuildBlob( BuildId, ChunkHash, - BuildBlob.GetContentType(), + ZenContentType::kCompressedBinary, CompositeBuffer(SharedBuffer(BuildBlob))); } if (!BuildBlob) @@ -7792,7 +7983,7 @@ namespace { { Storage.BuildCacheStorage->PutBuildBlob(BuildId, BlockDescription.BlockHash, - BlockBuffer.GetContentType(), + ZenContentType::kCompressedBinary, CompositeBuffer(SharedBuffer(BlockBuffer))); } } @@ -9396,17 +9587,17 @@ namespace { struct DownloadOptions { - std::filesystem::path SystemRootDir; - std::filesystem::path ZenFolderPath; - bool AllowMultiparts = true; - bool AllowPartialBlockRequests = true; - bool CleanTargetFolder = false; - bool PostDownloadVerify = false; - bool PrimeCacheOnly = false; - bool EnableOtherDownloadsScavenging = true; - bool EnableTargetFolderScavenging = true; - std::string IncludeWildcard; - std::string ExcludeWildcard; + std::filesystem::path SystemRootDir; + std::filesystem::path ZenFolderPath; + bool AllowMultiparts = true; + EPartialBlockRequestMode PartialBlockRequestMode = EPartialBlockRequestMode::Mixed; + bool CleanTargetFolder = false; + bool PostDownloadVerify = false; + bool PrimeCacheOnly = false; + bool EnableOtherDownloadsScavenging = true; + bool EnableTargetFolderScavenging = true; + std::string IncludeWildcard; + std::string ExcludeWildcard; }; void DownloadFolder(StorageInstance& Storage, @@ -9433,7 +9624,8 @@ namespace { auto EndProgress = MakeGuard([&]() { ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::StepCount, TaskSteps::StepCount); }); - ZEN_ASSERT((!Options.PrimeCacheOnly) || (Options.PrimeCacheOnly && (!Options.AllowPartialBlockRequests))); + ZEN_ASSERT((!Options.PrimeCacheOnly) || + (Options.PrimeCacheOnly && (Options.PartialBlockRequestMode == EPartialBlockRequestMode::Off))); Stopwatch DownloadTimer; @@ -9601,7 +9793,7 @@ namespace { .ZenFolderPath = Options.ZenFolderPath, .LargeAttachmentSize = LargeAttachmentSize, .PreferredMultipartChunkSize = PreferredMultipartChunkSize, - .AllowPartialBlockRequests = Options.AllowPartialBlockRequests, + .PartialBlockRequestMode = Options.PartialBlockRequestMode, .WipeTargetFolder = Options.CleanTargetFolder, .PrimeCacheOnly = Options.PrimeCacheOnly, .EnableOtherDownloadsScavenging = Options.EnableOtherDownloadsScavenging, @@ -9988,81 +10180,9 @@ BuildsCommand::BuildsCommand() "<usesparsefiles>"); }; - auto AddAuthOptions = [this](cxxopts::Options& Ops) { - // Direct access token (may expire) - Ops.add_option("auth-token", - "", - "access-token", - "Cloud/Builds Storage access token", - cxxopts::value(m_AccessToken), - "<accesstoken>"); - Ops.add_option("auth-token", - "", - "access-token-env", - "Name of environment variable that holds the cloud/builds Storage access token", - cxxopts::value(m_AccessTokenEnv)->default_value(DefaultAccessTokenEnvVariableName), - "<envvariable>"); - Ops.add_option("auth-token", - "", - "access-token-path", - "Path to json file that holds the cloud/builds Storage access token", - cxxopts::value(m_AccessTokenPath), - "<filepath>"); - - // Auth manager token encryption - Ops.add_option("security", - "", - "encryption-aes-key", - "256 bit AES encryption key", - cxxopts::value<std::string>(m_EncryptionKey), - ""); - Ops.add_option("security", - "", - "encryption-aes-iv", - "128 bit AES encryption initialization vector", - cxxopts::value<std::string>(m_EncryptionIV), - ""); + auto AddCloudOptions = [this](cxxopts::Options& Ops) { + m_AuthOptions.AddOptions(Ops); - // OpenId acccess token - Ops.add_option("openid", - "", - "openid-provider-name", - "Open ID provider name", - cxxopts::value<std::string>(m_OpenIdProviderName), - "Default"); - Ops.add_option("openid", "", "openid-provider-url", "Open ID provider url", cxxopts::value<std::string>(m_OpenIdProviderUrl), ""); - Ops.add_option("openid", "", "openid-client-id", "Open ID client id", cxxopts::value<std::string>(m_OpenIdClientId), ""); - Ops.add_option("openid", - "", - "openid-refresh-token", - "Open ID refresh token", - cxxopts::value<std::string>(m_OpenIdRefreshToken), - ""); - - // OAuth acccess token - Ops.add_option("oauth", "", "oauth-url", "OAuth provier url", cxxopts::value<std::string>(m_OAuthUrl)->default_value(""), ""); - Ops.add_option("oauth", - "", - "oauth-clientid", - "OAuth client id", - cxxopts::value<std::string>(m_OAuthClientId)->default_value(""), - ""); - Ops.add_option("oauth", - "", - "oauth-clientsecret", - "OAuth client secret", - cxxopts::value<std::string>(m_OAuthClientSecret)->default_value(""), - ""); - Ops.add_option("auth", - "", - "oidctoken-exe-path", - "Path to OidcToken executable", - cxxopts::value<std::string>(m_OidcTokenAuthExecutablePath)->default_value(""), - ""); - }; - - auto AddCloudOptions = [this, &AddAuthOptions](cxxopts::Options& Ops) { - AddAuthOptions(Ops); Ops.add_option("cloud build", "", "override-host", "Cloud Builds URL", cxxopts::value(m_OverrideHost), "<override-host>"); Ops.add_option("cloud build", "", @@ -10154,6 +10274,31 @@ BuildsCommand::BuildsCommand() "<excludewildcard>"); }; + auto AddMultipartOptions = [this](cxxopts::Options& Ops) { + Ops.add_option("", + "", + "allow-multipart", + "Allow large attachments to be transfered using multipart protocol. Defaults to true.", + cxxopts::value(m_AllowMultiparts), + "<allowmultipart>"); + }; + + auto AddPartialBlockRequestOptions = [this](cxxopts::Options& Ops) { + Ops.add_option("", + "", + "allow-partial-block-requests", + "Allow request for partial chunk blocks.\n" + " false = only full block requests allowed\n" + " mixed = multiple partial block ranges requests per block allowed to zen cache, single partial block range " + "request per block to host\n" + " zencacheonly = multiple partial block ranges requests per block allowed to zen cache, only full block requests " + "allowed to host\n" + " true = multiple partial block ranges requests per block allowed to zen cache and host\n" + "Defaults to 'mixed'.", + cxxopts::value(m_AllowPartialBlockRequests), + "<allowpartialblockrequests>"); + }; + m_Options.add_option("", "v", "verb", @@ -10256,12 +10401,9 @@ BuildsCommand::BuildsCommand() "Percent of an existing block that must be relevant for it to be resused. Defaults to 85.", cxxopts::value(m_BlockReuseMinPercentLimit), "<minreuse>"); - m_UploadOptions.add_option("", - "", - "allow-multipart", - "Allow large attachments to be transfered using multipart protocol. Defaults to true.", - cxxopts::value(m_AllowMultiparts), - "<allowmultipart>"); + + AddMultipartOptions(m_UploadOptions); + m_UploadOptions.add_option("", "", "manifest-path", @@ -10326,18 +10468,9 @@ BuildsCommand::BuildsCommand() "Force download of all content by ignoring any existing local content", cxxopts::value(m_Force), "<force>"); - m_DownloadOptions.add_option("", - "", - "allow-multipart", - "Allow large attachments to be transfered using multipart protocol. Defaults to true.", - cxxopts::value(m_AllowMultiparts), - "<allowmultipart>"); - m_DownloadOptions.add_option("", - "", - "allow-partial-block-requests", - "Allow request for partial chunk blocks. Defaults to true.", - cxxopts::value(m_AllowPartialBlockRequests), - "<allowpartialblockrequests>"); + AddMultipartOptions(m_DownloadOptions); + + AddPartialBlockRequestOptions(m_DownloadOptions); m_DownloadOptions .add_option("", "", "verify", "Enable post download verify of all tracked files", cxxopts::value(m_PostDownloadVerify), "<verify>"); @@ -10404,18 +10537,8 @@ BuildsCommand::BuildsCommand() AddWorkerOptions(m_TestOptions); m_TestOptions.add_options()("h,help", "Print help"); m_TestOptions.add_option("", "l", "local-path", "Root file system folder used as base", cxxopts::value(m_Path), "<local-path>"); - m_TestOptions.add_option("", - "", - "allow-multipart", - "Allow large attachments to be transfered using multipart protocol. Defaults to true.", - cxxopts::value(m_AllowMultiparts), - "<allowmultipart>"); - m_TestOptions.add_option("", - "", - "allow-partial-block-requests", - "Allow request for partial chunk blocks. Defaults to true.", - cxxopts::value(m_AllowPartialBlockRequests), - "<allowpartialblockrequests>"); + AddMultipartOptions(m_TestOptions); + AddPartialBlockRequestOptions(m_TestOptions); m_TestOptions.add_option("", "", "enable-scavenge", @@ -10529,13 +10652,12 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) return; } - std::filesystem::path SystemRootDir; - auto ParseSystemOptions = [&]() { - if (m_SystemRootDir.empty()) - { - m_SystemRootDir = PickDefaultSystemRootDirectory(); - } - MakeSafeAbsolutePathÍnPlace(m_SystemRootDir); + auto ParseSystemOptions = [&]() { + if (m_SystemRootDir.empty()) + { + m_SystemRootDir = PickDefaultSystemRootDirectory(); + } + MakeSafeAbsolutePathÍnPlace(m_SystemRootDir); }; ParseSystemOptions(); @@ -10556,7 +10678,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) throw OptionParseException(fmt::format("'--buildid' ('{}') conflicts with '--url' ('{}')", m_BuildId, m_Url), SubOption->help()); } - if (!ParseCloudUrl(m_Url, m_Host, m_Namespace, m_Bucket, m_BuildId)) + if (!ParseBuildStorageUrl(m_Url, m_Host, m_Namespace, m_Bucket, m_BuildId)) { throw OptionParseException("'--url' ('{}') is malformed, it does not match the Cloud Artifact URL format", SubOption->help()); @@ -10587,130 +10709,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) MakeSafeAbsolutePathÍnPlace(m_StoragePath); }; - std::unique_ptr<AuthMgr> Auth; - HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", - .AssumeHttp2 = m_AssumeHttp2, - .AllowResume = true, - .RetryCount = 2}; - - auto CreateAuthMgr = [&]() { - ZEN_ASSERT(!m_SystemRootDir.empty()); - if (!Auth) - { - if (m_EncryptionKey.empty()) - { - m_EncryptionKey = "abcdefghijklmnopqrstuvxyz0123456"; - if (!IsQuiet) - { - ZEN_CONSOLE_WARN("Using default encryption key"); - } - } - - if (m_EncryptionIV.empty()) - { - m_EncryptionIV = "0123456789abcdef"; - if (!IsQuiet) - { - ZEN_CONSOLE_WARN("Using default encryption initialization vector"); - } - } - - AuthConfig AuthMgrConfig = {.RootDirectory = m_SystemRootDir / "auth", - .EncryptionKey = AesKey256Bit::FromString(m_EncryptionKey), - .EncryptionIV = AesIV128Bit::FromString(m_EncryptionIV)}; - if (!AuthMgrConfig.EncryptionKey.IsValid()) - { - throw OptionParseException(fmt::format("'--encryption-aes-key' ('{}') is malformed", m_EncryptionKey), SubOption->help()); - } - if (!AuthMgrConfig.EncryptionIV.IsValid()) - { - throw OptionParseException(fmt::format("'--encryption-aes-iv' ('{}') is malformed", m_EncryptionIV), SubOption->help()); - } - Auth = AuthMgr::Create(AuthMgrConfig); - } - }; - - auto ParseAuthOptions = [&]() { - if (!m_OpenIdProviderUrl.empty() && !m_OpenIdClientId.empty()) - { - CreateAuthMgr(); - std::string ProviderName = m_OpenIdProviderName.empty() ? "Default" : m_OpenIdProviderName; - Auth->AddOpenIdProvider({.Name = ProviderName, .Url = m_OpenIdProviderUrl, .ClientId = m_OpenIdClientId}); - if (!m_OpenIdRefreshToken.empty()) - { - Auth->AddOpenIdToken({.ProviderName = ProviderName, .RefreshToken = m_OpenIdRefreshToken}); - } - } - - auto GetEnvAccessToken = [](const std::string& AccessTokenEnv) -> std::string { - if (!AccessTokenEnv.empty()) - { - return GetEnvVariable(AccessTokenEnv); - } - return {}; - }; - - auto FindOidcTokenExePath = [](const std::string& OidcTokenAuthExecutablePath) -> std::filesystem::path { - if (OidcTokenAuthExecutablePath.empty()) - { - const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; - std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - else - { - std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - return {}; - }; - - if (!m_AccessToken.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(m_AccessToken); - } - else if (!m_AccessTokenPath.empty()) - { - MakeSafeAbsolutePathÍnPlace(m_AccessTokenPath); - std::string ResolvedAccessToken = ReadAccessTokenFromFile(m_AccessTokenPath); - if (!ResolvedAccessToken.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); - } - } - else if (!m_OAuthUrl.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOAuthClientCredentials( - {.Url = m_OAuthUrl, .ClientId = m_OAuthClientId, .ClientSecret = m_OAuthClientSecret}); - } - else if (!m_OpenIdProviderName.empty()) - { - CreateAuthMgr(); - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOpenIdProvider(*Auth, m_OpenIdProviderName); - } - else if (std::string ResolvedAccessToken = GetEnvAccessToken(m_AccessTokenEnv); !ResolvedAccessToken.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); - } - else if (std::filesystem::path OidcTokenExePath = FindOidcTokenExePath(m_OidcTokenAuthExecutablePath); !OidcTokenExePath.empty()) - { - const std::string& CloudHost = m_OverrideHost.empty() ? m_Host : m_OverrideHost; - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOidcTokenExecutable(OidcTokenExePath, CloudHost, IsQuiet); - } - - if (!ClientSettings.AccessTokenProvider) - { - CreateAuthMgr(); - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromDefaultOpenIdProvider(*Auth); - } - }; - auto ParseOutputOptions = [&]() { if (m_Verbose && m_Quiet) { @@ -10749,6 +10747,12 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) }; ParseOutputOptions(); + std::unique_ptr<AuthMgr> Auth; + HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", + .AssumeHttp2 = m_AssumeHttp2, + .AllowResume = true, + .RetryCount = 2}; + auto CreateBuildStorage = [&](BuildStorage::Statistics& StorageStats, BuildStorageCache::Statistics& StorageCacheStats, const std::filesystem::path& TempPath, @@ -10766,123 +10770,73 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!m_Host.empty() || !m_OverrideHost.empty()) { - ParseAuthOptions(); + m_AuthOptions + .ParseOptions(*SubOption, m_SystemRootDir, ClientSettings, m_OverrideHost.empty() ? m_Host : m_OverrideHost, Auth, IsQuiet); } std::string CloudHost; - auto TestCacheEndpoint = [](std::string_view BaseUrl, const bool AssumeHttp2) -> std::pair<bool, std::string> { - HttpClientSettings TestClientSettings{.LogCategory = "httpcacheclient", - .ConnectTimeout = std::chrono::milliseconds{1000}, - .Timeout = std::chrono::milliseconds{2000}, - .AssumeHttp2 = AssumeHttp2, - .AllowResume = true, - .RetryCount = 0}; - HttpClient TestHttpClient(BaseUrl, TestClientSettings); - HttpClient::Response TestResponse = TestHttpClient.Get("/status/builds"); - if (TestResponse.IsSuccess()) - { - return {true, ""}; - } - return {false, TestResponse.ErrorMessage("")}; - }; - if (!m_Host.empty()) { if (m_OverrideHost.empty() || m_ZenCacheHost.empty()) { - HttpClient DiscoveryHttpClient(m_Host, ClientSettings); - HttpClient::Response ServerInfoResponse = - DiscoveryHttpClient.Get("/api/v1/status/servers", HttpClient::Accept(HttpContentType::kJSON)); - if (!ServerInfoResponse.IsSuccess()) - { - ServerInfoResponse.ThrowError(fmt::format("Failed to get list of servers from discovery url '{}'", m_Host)); - } - - std::string_view JsonResponse = ServerInfoResponse.AsText(); - CbObject ResponseObjectView = LoadCompactBinaryFromJson(JsonResponse).AsObject(); - - auto TestHostEndpoint = [](std::string_view BaseUrl, const bool AssumeHttp2) -> std::pair<bool, std::string> { - HttpClientSettings TestClientSettings{.LogCategory = "httpbuildsclient", - .ConnectTimeout = std::chrono::milliseconds{1000}, - .Timeout = std::chrono::milliseconds{2000}, - .AssumeHttp2 = AssumeHttp2, - .AllowResume = true, - .RetryCount = 0}; - - HttpClient TestHttpClient(BaseUrl, TestClientSettings); - HttpClient::Response TestResponse = TestHttpClient.Get("/health/live"); - if (TestResponse.IsSuccess()) - { - return {true, ""}; - } - return {false, TestResponse.ErrorMessage("")}; - }; + JupiterServerDiscovery Response = DiscoverJupiterEndpoints(m_Host, ClientSettings); if (m_OverrideHost.empty()) { - CbArrayView ServerEndpointsArray = ResponseObjectView["serverEndpoints"sv].AsArrayView(); - std::uint64_t ServerCount = ServerEndpointsArray.Num(); - if (ServerCount == 0) + if (Response.ServerEndPoints.empty()) { throw std::runtime_error(fmt::format("Failed to find any builds hosts at {}", m_Host)); } - for (CbFieldView ServerEndpointView : ServerEndpointsArray) + for (const JupiterServerDiscovery::EndPoint& ServerEndpoint : Response.ServerEndPoints) { - CbObjectView ServerEndpointObject = ServerEndpointView.AsObjectView(); - - std::string_view BaseUrl = ServerEndpointObject["baseUrl"sv].AsString(); - if (!BaseUrl.empty()) + if (!ServerEndpoint.BaseUrl.empty()) { - const bool AssumeHttp2 = ServerEndpointObject["assumeHttp2"sv].AsBool(false); - std::string_view Name = ServerEndpointObject["name"sv].AsString(); - if (auto TestResult = TestHostEndpoint(BaseUrl, AssumeHttp2); TestResult.first) + if (JupiterEndpointTestResult TestResult = + TestJupiterEndpoint(ServerEndpoint.BaseUrl, ServerEndpoint.AssumeHttp2); + TestResult.Success) { - CloudHost = BaseUrl; - m_AssumeHttp2 = AssumeHttp2; - BuildStorageName = Name; + CloudHost = ServerEndpoint.BaseUrl; + m_AssumeHttp2 = ServerEndpoint.AssumeHttp2; + BuildStorageName = ServerEndpoint.Name; break; } else { - ZEN_DEBUG("Unable to reach host {}. Reason: {}", BaseUrl, TestResult.second); + ZEN_DEBUG("Unable to reach host {}. Reason: {}", ServerEndpoint.BaseUrl, TestResult.FailureReason); } } } if (CloudHost.empty()) { - throw std::runtime_error( - fmt::format("Failed to find any usable builds hosts out of {} using {}", ServerCount, m_Host)); + throw std::runtime_error(fmt::format("Failed to find any usable builds hosts out of {} using {}", + Response.ServerEndPoints.size(), + m_Host)); } } - else if (auto TestResult = TestHostEndpoint(m_OverrideHost, m_AssumeHttp2); TestResult.first) + else if (JupiterEndpointTestResult TestResult = TestJupiterEndpoint(m_OverrideHost, m_AssumeHttp2); TestResult.Success) { CloudHost = m_OverrideHost; } else { - throw std::runtime_error(fmt::format("Host {} could not be reached. Reason: {}", m_OverrideHost, TestResult.second)); + throw std::runtime_error( + fmt::format("Host {} could not be reached. Reason: {}", m_OverrideHost, TestResult.FailureReason)); } if (m_ZenCacheHost.empty()) { - CbArrayView CacheEndpointsArray = ResponseObjectView["cacheEndpoints"sv].AsArrayView(); - std::uint64_t CacheCount = CacheEndpointsArray.Num(); - for (CbFieldView CacheEndpointView : CacheEndpointsArray) + for (const JupiterServerDiscovery::EndPoint& CacheEndpoint : Response.CacheEndPoints) { - CbObjectView CacheEndpointObject = CacheEndpointView.AsObjectView(); - - std::string_view BaseUrl = CacheEndpointObject["baseUrl"sv].AsString(); - if (!BaseUrl.empty()) + if (!CacheEndpoint.BaseUrl.empty()) { - const bool AssumeHttp2 = CacheEndpointObject["assumeHttp2"sv].AsBool(false); - std::string_view Name = CacheEndpointObject["name"sv].AsString(); - - if (auto TestResult = TestCacheEndpoint(BaseUrl, AssumeHttp2); TestResult.first) + if (ZenCacheEndpointTestResult TestResult = + TestZenCacheEndpoint(CacheEndpoint.BaseUrl, CacheEndpoint.AssumeHttp2); + TestResult.Success) { - m_ZenCacheHost = BaseUrl; - CacheAssumeHttp2 = AssumeHttp2; - BuildCacheName = Name; + m_ZenCacheHost = CacheEndpoint.BaseUrl; + CacheAssumeHttp2 = CacheEndpoint.AssumeHttp2; + BuildCacheName = CacheEndpoint.Name; break; } } @@ -10897,7 +10851,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { std::string ZenServerLocalHostUrl = fmt::format("http://127.0.0.1:{}", Entry.EffectiveListenPort.load()); - if (auto TestResult = TestCacheEndpoint(ZenServerLocalHostUrl, false); TestResult.first) + if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(ZenServerLocalHostUrl, false); + TestResult.Success) { m_ZenCacheHost = ZenServerLocalHostUrl; CacheAssumeHttp2 = false; @@ -10908,11 +10863,13 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } if (m_ZenCacheHost.empty() && !IsQuiet) { - ZEN_CONSOLE_WARN("Failed to find any usable cache hosts out of {} using {}", CacheCount, m_Host); + ZEN_CONSOLE_WARN("Failed to find any usable cache hosts out of {} using {}", + Response.CacheEndPoints.size(), + m_Host); } } } - else if (auto TestResult = TestCacheEndpoint(m_ZenCacheHost, false); TestResult.first) + else if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(m_ZenCacheHost, false); TestResult.Success) { std::string::size_type HostnameStart = 0; std::string::size_type HostnameLength = std::string::npos; @@ -10928,9 +10885,14 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else { - ZEN_CONSOLE_WARN("Unable to reach cache host {}. Reason: {}", m_ZenCacheHost, TestResult.second); + ZEN_CONSOLE_WARN("Unable to reach cache host {}. Reason: {}", m_ZenCacheHost, TestResult.FailureReason); + m_ZenCacheHost = ""; } } + else if (!m_OverrideHost.empty()) + { + CloudHost = m_OverrideHost; + } } else { @@ -10967,33 +10929,41 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } if (!m_ZenCacheHost.empty()) { - Result.CacheHttp = std::make_unique<HttpClient>(m_ZenCacheHost, - HttpClientSettings{.LogCategory = "httpcacheclient", - .ConnectTimeout = std::chrono::milliseconds{3000}, - .Timeout = std::chrono::milliseconds{30000}, - .AssumeHttp2 = CacheAssumeHttp2, - .AllowResume = true, - .RetryCount = 0}); - Result.BuildCacheStorage = CreateZenBuildStorageCache(*Result.CacheHttp, - StorageCacheStats, - m_Namespace, - m_Bucket, - TempPath / "zencache", - m_PrimeCacheOnly); - CacheDescription = fmt::format("Zen {}{}. SessionId: '{}'", - BuildCacheName.empty() ? "" : fmt::format("{}, ", BuildCacheName), - m_ZenCacheHost, - Result.CacheHttp->GetSessionId()); - - if (!m_Namespace.empty()) - { - CacheDescription += fmt::format(". Namespace '{}'", m_Namespace); + if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(m_ZenCacheHost, false); TestResult.Success) + { + Result.CacheHttp = std::make_unique<HttpClient>(m_ZenCacheHost, + HttpClientSettings{.LogCategory = "httpcacheclient", + .ConnectTimeout = std::chrono::milliseconds{3000}, + .Timeout = std::chrono::milliseconds{30000}, + .AssumeHttp2 = CacheAssumeHttp2, + .AllowResume = true, + .RetryCount = 0}); + Result.BuildCacheStorage = CreateZenBuildStorageCache(*Result.CacheHttp, + StorageCacheStats, + m_Namespace, + m_Bucket, + TempPath / "zencache", + m_PrimeCacheOnly); + CacheDescription = fmt::format("Zen {}{}. SessionId: '{}'", + BuildCacheName.empty() ? "" : fmt::format("{}, ", BuildCacheName), + m_ZenCacheHost, + Result.CacheHttp->GetSessionId()); + + if (!m_Namespace.empty()) + { + CacheDescription += fmt::format(". Namespace '{}'", m_Namespace); + } + if (!m_Bucket.empty()) + { + CacheDescription += fmt::format(" Bucket '{}'", m_Bucket); + } + Result.CacheName = BuildCacheName.empty() ? m_ZenCacheHost : BuildCacheName; } - if (!m_Bucket.empty()) + else { - CacheDescription += fmt::format(" Bucket '{}'", m_Bucket); + ZEN_CONSOLE_WARN("Unable to reach cache host {}. Reason: {}", m_ZenCacheHost, TestResult.FailureReason); + m_ZenCacheHost = ""; } - Result.CacheName = BuildCacheName.empty() ? m_ZenCacheHost : BuildCacheName; } if (!IsQuiet) { @@ -11433,6 +11403,20 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } } + auto ParseAllowPartialBlockRequests = [&]() -> EPartialBlockRequestMode { + if (m_PrimeCacheOnly) + { + return EPartialBlockRequestMode::Off; + } + EPartialBlockRequestMode Mode = PartialBlockRequestModeFromString(m_AllowPartialBlockRequests); + if (Mode == EPartialBlockRequestMode::Invalid) + { + throw OptionParseException(fmt::format("'--allow-partial-block-requests' ('{}') is invalid", m_AllowPartialBlockRequests), + SubOption->help()); + } + return Mode; + }; + if (SubOption == &m_DownloadOptions) { if (!IsQuiet) @@ -11477,7 +11461,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ZEN_CONSOLE_WARN("Ignoring '--force' option when '--cache-prime-only' is enabled"); } - if (m_AllowPartialBlockRequests && m_PrimeCacheOnly) + if (m_AllowPartialBlockRequests != "false" && m_PrimeCacheOnly) { ZEN_CONSOLE_WARN("Ignoring '--allow-partial-block-requests' option when '--cache-prime-only' is enabled"); } @@ -11485,6 +11469,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::vector<Oid> BuildPartIds = ParseBuildPartIds(); std::vector<std::string> BuildPartNames = ParseBuildPartNames(); + EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + DownloadFolder(Storage, BuildId, BuildPartIds, @@ -11493,7 +11479,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = m_ZenFolderPath, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests && !m_PrimeCacheOnly, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = m_Clean, .PostDownloadVerify = m_PostDownloadVerify, .PrimeCacheOnly = m_PrimeCacheOnly, @@ -11680,6 +11666,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath); + EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + BuildStorage::Statistics StorageStats; BuildStorageCache::Statistics StorageCacheStats; @@ -11705,7 +11693,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = m_ZenFolderPath, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = BuildIdString == m_BuildIds.front(), .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -11791,6 +11779,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } }); + EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + BuildStorage::Statistics StorageStats; BuildStorageCache::Statistics StorageCacheStats; @@ -11877,7 +11867,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = true, .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -11898,7 +11888,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -12011,7 +12001,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -12063,7 +12053,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -12083,7 +12073,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -12103,7 +12093,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .PrimeCacheOnly = false, @@ -12123,7 +12113,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadOptions{.SystemRootDir = m_SystemRootDir, .ZenFolderPath = DownloadPath2 / ZenFolderName, .AllowMultiparts = m_AllowMultiparts, - .AllowPartialBlockRequests = m_AllowPartialBlockRequests, + .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .PrimeCacheOnly = false, diff --git a/src/zen/cmds/builds_cmd.h b/src/zen/cmds/builds_cmd.h index 26f804fa5..27a178c6f 100644 --- a/src/zen/cmds/builds_cmd.h +++ b/src/zen/cmds/builds_cmd.h @@ -2,6 +2,7 @@ #pragma once +#include "../authutils.h" #include "../zen.h" #include <zenhttp/auth/authmgr.h> @@ -64,30 +65,10 @@ private: bool m_Force = false; uint8_t m_BlockReuseMinPercentLimit = 85; bool m_AllowMultiparts = true; - bool m_AllowPartialBlockRequests = true; + std::string m_AllowPartialBlockRequests = "mixed"; std::string m_ManifestPath; // Not a std::filesystem::path since it can be relative to m_Path - // Direct access token (may expire) - std::string m_AccessToken; - std::string m_AccessTokenEnv; - std::filesystem::path m_AccessTokenPath; - - // Auth manager token encryption - std::string m_EncryptionKey; // 256 bit AES encryption key - std::string m_EncryptionIV; // 128 bit AES initialization vector - - // OpenId acccess token - std::string m_OpenIdProviderName; - std::string m_OpenIdProviderUrl; - std::string m_OpenIdClientId; - std::string m_OpenIdRefreshToken; - - // OAuth acccess token - std::string m_OAuthUrl; - std::string m_OAuthClientId; - std::string m_OAuthClientSecret; - - std::string m_OidcTokenAuthExecutablePath; + AuthCommandLineOptions m_AuthOptions; std::string m_Verb; // list, upload, download diff --git a/src/zen/cmds/print_cmd.cpp b/src/zen/cmds/print_cmd.cpp index c3c11a0ea..557808ba7 100644 --- a/src/zen/cmds/print_cmd.cpp +++ b/src/zen/cmds/print_cmd.cpp @@ -19,7 +19,10 @@ PrintCbObject(CbObject Object, bool AddTypeComment) { ExtendableStringBuilder<1024> ObjStr; CompactBinaryToJson(Object, ObjStr, AddTypeComment); - ZEN_CONSOLE("{}", ObjStr); + ForEachStrTok(ObjStr.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); } static void @@ -27,7 +30,10 @@ PrintCompactBinary(IoBuffer Data, bool AddTypeComment) { ExtendableStringBuilder<1024> StreamString; CompactBinaryToJson(Data.GetView(), StreamString, AddTypeComment); - ZEN_CONSOLE("{}", StreamString); + ForEachStrTok(StreamString.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); } static CbValidateError diff --git a/src/zen/cmds/projectstore_cmd.cpp b/src/zen/cmds/projectstore_cmd.cpp index 8ed52c764..b738d16aa 100644 --- a/src/zen/cmds/projectstore_cmd.cpp +++ b/src/zen/cmds/projectstore_cmd.cpp @@ -4,24 +4,33 @@ #include <zencore/basicfile.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compress.h> +#include <zencore/config.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> +#include <zencore/process.h> #include <zencore/scopeguard.h> #include <zencore/stream.h> #include <zencore/timer.h> #include <zencore/workthreadpool.h> +#include <zenhttp/auth/authmgr.h> #include <zenhttp/formatters.h> #include <zenhttp/httpclient.h> +#include <zenhttp/httpclientauth.h> #include <zenhttp/httpcommon.h> +#include <zenutil/jupiter/jupiterbuildstorage.h> +#include <zenutil/jupiter/jupiterhost.h> + ZEN_THIRD_PARTY_INCLUDES_START #include <cpr/cpr.h> #include <json11.hpp> ZEN_THIRD_PARTY_INCLUDES_END #include <signal.h> +#include <iostream> namespace zen { @@ -29,64 +38,7 @@ namespace { using namespace std::literals; - const std::string DefaultJupiterAccessTokenEnvVariableName( -#if ZEN_PLATFORM_WINDOWS - "UE-CloudDataCacheAccessToken"sv -#endif -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - "UE_CloudDataCacheAccessToken"sv -#endif - - ); - - std::string ReadJupiterAccessTokenFromFile(const std::filesystem::path& Path) - { - if (!IsFile(Path)) - { - throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); - } - IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); - std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()); - std::string JsonError; - json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); - if (!JsonError.empty()) - { - throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); - } - const std::string AuthToken = TokenInfo["Token"].string_value(); - if (AuthToken.empty()) - { - throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); - } - return AuthToken; - } - - std::filesystem::path FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath) - { - if (OidcTokenAuthExecutablePath.empty()) - { - const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; - std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - OidcTokenPath = (std::filesystem::current_path() / OidcExecutableName).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - else - { - std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - return {}; - }; +#define ZEN_CLOUD_STORAGE "Cloud Storage" void WriteAuthOptions(CbObjectWriter& Writer, std::string_view JupiterOpenIdProvider, @@ -105,7 +57,7 @@ namespace { } if (!JupiterAccessTokenPath.empty()) { - std::string ResolvedCloudAccessToken = ReadJupiterAccessTokenFromFile(JupiterAccessTokenPath); + std::string ResolvedCloudAccessToken = ReadAccessTokenFromJsonFile(JupiterAccessTokenPath); if (!ResolvedCloudAccessToken.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessToken); @@ -910,7 +862,7 @@ ExportOplogCommand::ExportOplogCommand() "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", - cxxopts::value(m_JupiterAccessTokenEnv)->default_value(DefaultJupiterAccessTokenEnvVariableName), + cxxopts::value(m_JupiterAccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), "<envvariable>"); m_Options.add_option("", "", @@ -1387,7 +1339,7 @@ ImportOplogCommand::ImportOplogCommand() "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", - cxxopts::value(m_JupiterAccessTokenEnv)->default_value(DefaultJupiterAccessTokenEnvVariableName), + cxxopts::value(m_JupiterAccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), "<envvariable>"); m_Options.add_option("", "", @@ -2236,4 +2188,327 @@ OplogValidateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a } } +//////////////////////////// + +OplogDownloadCommand::OplogDownloadCommand() +{ + m_Options.add_options()("h,help", "Print help"); + + m_Options.add_option("", "", "system-dir", "Specify system root", cxxopts::value(m_SystemRootDir), "<systemdir>"); + + m_Options.add_option("output", "", "quiet", "Suppress non-essential output", cxxopts::value(m_Quiet), "<quiet>"); + m_Options.add_option("", "y", "yes", "Don't query for confirmation", cxxopts::value(m_Yes), "<yes>"); + + auto AddCloudOptions = [this](cxxopts::Options& Ops) { + m_AuthOptions.AddOptions(Ops); + + Ops.add_option("cloud build", "", "override-host", "Cloud Builds URL", cxxopts::value(m_OverrideHost), "<override-host>"); + Ops.add_option("cloud build", "", "cloud-url", "Cloud Artifact URL", cxxopts::value(m_Url), "<cloud-url>"); + Ops.add_option("cloud build", "", "host", "Cloud Builds host", cxxopts::value(m_Host), "<host>"); + Ops.add_option("cloud build", + "", + "assume-http2", + "Assume that the builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", + cxxopts::value(m_AssumeHttp2), + "<assumehttp2>"); + + Ops.add_option("cloud build", "", "namespace", "Builds Storage namespace", cxxopts::value(m_Namespace), "<namespace>"); + Ops.add_option("cloud build", "", "bucket", "Builds Storage bucket", cxxopts::value(m_Bucket), "<bucket>"); + }; + + AddCloudOptions(m_Options); + + m_Options.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), "<id>"); + m_Options.add_option("", + "", + "output-path", + "Path to oplog output, extension .json or .cb (compact binary). Default is output to console", + cxxopts::value(m_OutputPath), + "<path>"); + + m_Options.parse_positional({"cloud-url", "output-path"}); + m_Options.positional_help("[<cloud-url> <output-path>]"); +} + +OplogDownloadCommand::~OplogDownloadCommand() +{ +} + +void +OplogDownloadCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) +{ + ZEN_UNUSED(GlobalOptions); + + if (!ParseOptions(argc, argv)) + { + return; + } + + if (!m_Quiet) + { + ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); + } + + auto ParseSystemOptions = [&]() { + if (m_SystemRootDir.empty()) + { + m_SystemRootDir = PickDefaultSystemRootDirectory(); + } + MakeSafeAbsolutePathÍnPlace(m_SystemRootDir); + }; + ParseSystemOptions(); + + auto ParseStorageOptions = [&](bool RequireNamespace, bool RequireBucket) { + if (!m_Url.empty()) + { + if (!m_Host.empty()) + { + throw OptionParseException(fmt::format("'--host' ('{}') conflicts with '--url' ('{}')", m_Host, m_Url), m_Options.help()); + } + if (!m_Bucket.empty()) + { + throw OptionParseException(fmt::format("'--bucket' ('{}') conflicts with '--url' ('{}')", m_Bucket, m_Url), + m_Options.help()); + } + if (!m_BuildId.empty()) + { + throw OptionParseException(fmt::format("'--buildid' ('{}') conflicts with '--url' ('{}')", m_BuildId, m_Url), + m_Options.help()); + } + if (!ParseBuildStorageUrl(m_Url, m_Host, m_Namespace, m_Bucket, m_BuildId)) + { + throw OptionParseException("'--url' ('{}') is malformed, it does not match the Cloud Artifact URL format", + m_Options.help()); + } + } + + if (!m_OverrideHost.empty() || !m_Host.empty()) + { + if (RequireNamespace && m_Namespace.empty()) + { + throw OptionParseException("'--namespace' is required", m_Options.help()); + } + if (RequireBucket && m_Bucket.empty()) + { + throw OptionParseException("'--bucket' is required", m_Options.help()); + } + } + + if (m_OverrideHost.empty() && m_Host.empty()) + { + throw OptionParseException("'--host' or '--overridehost' is required", m_Options.help()); + } + }; + + std::unique_ptr<AuthMgr> Auth; + HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", + .AssumeHttp2 = m_AssumeHttp2, + .AllowResume = true, + .RetryCount = 2}; + + ParseStorageOptions(/*RequireNamespace*/ true, /*RequireBucket*/ true); + + m_AuthOptions.ParseOptions(m_Options, m_SystemRootDir, ClientSettings, m_OverrideHost.empty() ? m_Host : m_OverrideHost, Auth, m_Quiet); + + std::string BuildStorageName = ZEN_CLOUD_STORAGE; + + std::string CloudHost; + + auto TestHostEndpoint = [](std::string_view BaseUrl, const bool AssumeHttp2) -> std::pair<bool, std::string> { + HttpClientSettings TestClientSettings{.LogCategory = "httpbuildsclient", + .ConnectTimeout = std::chrono::milliseconds{1000}, + .Timeout = std::chrono::milliseconds{2000}, + .AssumeHttp2 = AssumeHttp2, + .AllowResume = true, + .RetryCount = 0}; + + HttpClient TestHttpClient(BaseUrl, TestClientSettings); + HttpClient::Response TestResponse = TestHttpClient.Get("/health/live"); + if (TestResponse.IsSuccess()) + { + return {true, ""}; + } + return {false, TestResponse.ErrorMessage("")}; + }; + + if (m_OverrideHost.empty()) + { + JupiterServerDiscovery Response = DiscoverJupiterEndpoints(m_Host, ClientSettings); + + if (Response.ServerEndPoints.empty()) + { + throw std::runtime_error(fmt::format("Failed to find any builds hosts at {}", m_Host)); + } + for (const JupiterServerDiscovery::EndPoint& ServerEndpoint : Response.ServerEndPoints) + { + if (!ServerEndpoint.BaseUrl.empty()) + { + if (JupiterEndpointTestResult TestResult = TestJupiterEndpoint(ServerEndpoint.BaseUrl, ServerEndpoint.AssumeHttp2); + TestResult.Success) + { + CloudHost = ServerEndpoint.BaseUrl; + m_AssumeHttp2 = ServerEndpoint.AssumeHttp2; + BuildStorageName = ServerEndpoint.Name; + break; + } + else + { + ZEN_DEBUG("Unable to reach host {}. Reason: {}", ServerEndpoint.BaseUrl, TestResult.FailureReason); + } + } + } + if (CloudHost.empty()) + { + throw std::runtime_error( + fmt::format("Failed to find any usable builds hosts out of {} using {}", Response.ServerEndPoints.size(), m_Host)); + } + } + else if (JupiterEndpointTestResult TestResult = TestJupiterEndpoint(m_OverrideHost, m_AssumeHttp2); TestResult.Success) + { + CloudHost = m_OverrideHost; + } + else + { + throw std::runtime_error(fmt::format("Host {} could not be reached. Reason: {}", m_OverrideHost, TestResult.FailureReason)); + } + + Oid BuildId = Oid::TryFromHexString(m_BuildId); + if (BuildId == Oid::Zero) + { + throw OptionParseException("'--buildid' is malformed", m_Options.help()); + } + + BuildStorage::Statistics StorageStats; + HttpClient BuildStorageHttp(CloudHost, ClientSettings); + + if (!m_Quiet) + { + std::string StorageDescription = fmt::format("Cloud {}{}. SessionId: '{}'. Namespace '{}', Bucket '{}'", + BuildStorageName.empty() ? "" : fmt::format("{}, ", BuildStorageName), + CloudHost, + BuildStorageHttp.GetSessionId(), + m_Namespace, + m_Bucket); + + ZEN_CONSOLE("Remote: {}", StorageDescription); + } + + std::filesystem::path StorageTempPath = std::filesystem::temp_directory_path() / ("zen_" + Oid::NewOid().ToString()); + + std::unique_ptr<BuildStorage> BuildStorage = + CreateJupiterBuildStorage(Log(), BuildStorageHttp, StorageStats, m_Namespace, m_Bucket, m_AllowRedirect, StorageTempPath); + + Stopwatch Timer; + CbObject BuildObject = BuildStorage->GetBuild(BuildId); + if (!m_Quiet) + { + ZEN_CONSOLE("Fetched {}/{}/{}/{} in {}", m_Url, m_Namespace, m_Bucket, BuildId, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + } + + Timer.Reset(); + + CbObjectView PartsObject = BuildObject["parts"sv].AsObjectView(); + if (!PartsObject) + { + throw std::runtime_error( + fmt::format("The build {}/{}/{}/{} payload does not contain a 'parts' object"sv, m_Url, m_Namespace, m_Bucket, m_BuildId)); + } + + static const std::string_view OplogContainerPartName = "oplogcontainer"sv; + + const Oid OplogBuildPartId = PartsObject[OplogContainerPartName].AsObjectId(); + if (OplogBuildPartId == Oid::Zero) + { + throw std::runtime_error(fmt::format("The build {}/{}/{}/{} payload 'parts' object does not contain a '{}' entry"sv, + m_Url, + m_Namespace, + m_Bucket, + m_BuildId, + OplogContainerPartName)); + } + + CbObject ContainerObject = BuildStorage->GetBuildPart(BuildId, OplogBuildPartId); + + MemoryView OpsSection = ContainerObject["ops"sv].AsBinaryView(); + IoBuffer OpsBuffer(IoBuffer::Wrap, OpsSection.GetData(), OpsSection.GetSize()); + IoBuffer SectionPayload = CompressedBuffer::FromCompressedNoValidate(std::move(OpsBuffer)).Decompress().AsIoBuffer(); + + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject SectionObject = ValidateAndReadCompactBinaryObject(std::move(SectionPayload), ValidateResult); + ValidateResult == CbValidateError::None && ContainerObject) + { + if (!m_Quiet) + { + ZEN_CONSOLE("Decompressed and validated oplog payload {} -> {} in {}", + NiceBytes(OpsSection.GetSize()), + NiceBytes(SectionObject.GetSize()), + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + } + + if (m_OutputPath.empty()) + { + if (!m_Yes) + { + if (OpsSection.GetSize() > 8u * 1024u * 1024u) + { + while (!m_Yes) + { + const std::string Prompt = fmt::format("Do you want to output an oplog of size {} to console? (yes/no) ", + NiceBytes(SectionObject.GetSize())); + printf("%s", Prompt.c_str()); + std::string Reponse; + std::getline(std::cin, Reponse); + Reponse = ToLower(Reponse); + if (Reponse == "y" || Reponse == "yes") + { + m_Yes = true; + } + else if (Reponse == "n" || Reponse == "no") + { + return; + } + } + } + } + ExtendableStringBuilder<1024> SB; + SectionObject.ToJson(SB); + ForEachStrTok(SB.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); + } + else + { + Timer.Reset(); + const std::string Extension = ToLower(m_OutputPath.extension().string()); + if (Extension == ".cb" || Extension == ".cbo") + { + WriteFile(m_OutputPath, IoBuffer(IoBuffer::Wrap, SectionObject.GetView().GetData(), SectionObject.GetSize())); + } + else if (Extension == ".json") + { + ExtendableStringBuilder<1024> SB; + SectionObject.ToJson(SB); + WriteFile(m_OutputPath, IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); + } + else + { + throw std::runtime_error(fmt::format("Unsupported output extension type '{}'", Extension)); + } + if (!m_Quiet) + { + ZEN_CONSOLE("Wrote {} to '{}' in {}", + NiceBytes(FileSizeFromPath(m_OutputPath)), + m_OutputPath, + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + } + } + } + else + { + throw std::runtime_error( + fmt::format("Failed to parse oplog container: '{}' ('{}')", "Section has unexpected data type", ToString(ValidateResult))); + } +} + } // namespace zen diff --git a/src/zen/cmds/projectstore_cmd.h b/src/zen/cmds/projectstore_cmd.h index 70b336650..136319aa3 100644 --- a/src/zen/cmds/projectstore_cmd.h +++ b/src/zen/cmds/projectstore_cmd.h @@ -4,6 +4,8 @@ #include "../zen.h" +#include "../authutils.h" + namespace zen { class ProjectStoreCommand : public ZenCmdBase @@ -162,6 +164,7 @@ private: std::string m_JupiterNamespace; std::string m_JupiterBucket; + std::string m_JupiterOpenIdProvider; std::string m_JupiterAccessToken; std::string m_JupiterAccessTokenEnv; @@ -268,4 +271,38 @@ private: std::string m_OplogName; }; +class OplogDownloadCommand : public ProjectStoreCommand +{ +public: + static constexpr char Name[] = "oplog-download"; + static constexpr char Description[] = "Download an cloud storage oplog"; + + OplogDownloadCommand(); + ~OplogDownloadCommand(); + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual cxxopts::Options& Options() override { return m_Options; } + +private: + cxxopts::Options m_Options{Name, Description}; + + std::filesystem::path m_SystemRootDir; + + bool m_Quiet = false; + bool m_Yes = false; + + AuthCommandLineOptions m_AuthOptions; + + // cloud builds + std::string m_OverrideHost; + std::string m_Host; + std::string m_Url; + bool m_AssumeHttp2 = false; + bool m_AllowRedirect = false; + std::string m_Namespace; + std::string m_Bucket; + std::string m_BuildId; + + std::filesystem::path m_OutputPath; +}; + } // namespace zen diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp index 200c8e4b4..0381dd15c 100644 --- a/src/zen/zen.cpp +++ b/src/zen/zen.cpp @@ -72,6 +72,88 @@ ZEN_THIRD_PARTY_INCLUDES_END namespace zen { +#if ZEN_PLATFORM_WINDOWS +static HANDLE +GetConsoleHandle() +{ + static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + return hStdOut; +} +#endif + +static bool +CheckStdoutTty() +{ +#if ZEN_PLATFORM_WINDOWS + HANDLE hStdOut = GetConsoleHandle(); + DWORD dwMode = 0; + static bool IsConsole = ::GetConsoleMode(hStdOut, &dwMode); + return IsConsole; +#else + return isatty(fileno(stdout)); +#endif +} + +static bool +IsStdoutTty() +{ + static bool StdoutIsTty = CheckStdoutTty(); + return StdoutIsTty; +} + +static void +OutputToConsoleRaw(const char* String, size_t Length) +{ +#if ZEN_PLATFORM_WINDOWS + HANDLE hStdOut = GetConsoleHandle(); +#endif + +#if ZEN_PLATFORM_WINDOWS + if (IsStdoutTty()) + { + WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0); + } + else + { + ::WriteFile(hStdOut, (LPCVOID)String, (DWORD)Length, 0, 0); + } +#else + fwrite(String, 1, Length, stdout); +#endif +} + +static void +OutputToConsoleRaw(const std::string& String) +{ + OutputToConsoleRaw(String.c_str(), String.length()); +} + +static void +OutputToConsoleRaw(const StringBuilderBase& SB) +{ + OutputToConsoleRaw(SB.c_str(), SB.Size()); +} + +static uint32_t +GetConsoleColumns(uint32_t Default) +{ +#if ZEN_PLATFORM_WINDOWS + HANDLE hStdOut = GetConsoleHandle(); + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (GetConsoleScreenBufferInfo(hStdOut, &csbi) == TRUE) + { + return (uint32_t)(csbi.srWindow.Right - csbi.srWindow.Left + 1); + } +#else + struct winsize w; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) + { + return (uint32_t)w.ws_col; + } +#endif + return Default; +} + enum class ReturnCode : std::int8_t { kSuccess = 0, @@ -122,6 +204,8 @@ ZenCmdBase::ParseOptions(int argc, char** argv) bool ZenCmdBase::ParseOptions(cxxopts::Options& CmdOptions, int argc, char** argv) { + CmdOptions.set_width(GetConsoleColumns(80)); + cxxopts::ParseResult Result; try @@ -308,88 +392,6 @@ ZenCmdBase::ResolveTargetHostSpec(const std::string& InHostSpec) return ResolveTargetHostSpec(InHostSpec, /* out */ Dummy); } -#if ZEN_PLATFORM_WINDOWS -static HANDLE -GetConsoleHandle() -{ - static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); - return hStdOut; -} -#endif - -static bool -CheckStdoutTty() -{ -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); - DWORD dwMode = 0; - static bool IsConsole = ::GetConsoleMode(hStdOut, &dwMode); - return IsConsole; -#else - return isatty(fileno(stdout)); -#endif -} - -static bool -IsStdoutTty() -{ - static bool StdoutIsTty = CheckStdoutTty(); - return StdoutIsTty; -} - -static void -OutputToConsoleRaw(const char* String, size_t Length) -{ -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); -#endif - -#if ZEN_PLATFORM_WINDOWS - if (IsStdoutTty()) - { - WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0); - } - else - { - ::WriteFile(hStdOut, (LPCVOID)String, (DWORD)Length, 0, 0); - } -#else - fwrite(String, 1, Length, stdout); -#endif -} - -static void -OutputToConsoleRaw(const std::string& String) -{ - OutputToConsoleRaw(String.c_str(), String.length()); -} - -static void -OutputToConsoleRaw(const StringBuilderBase& SB) -{ - OutputToConsoleRaw(SB.c_str(), SB.Size()); -} - -static uint32_t -GetConsoleColumns() -{ -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); - CONSOLE_SCREEN_BUFFER_INFO csbi; - if (GetConsoleScreenBufferInfo(hStdOut, &csbi) == TRUE) - { - return (uint32_t)(csbi.srWindow.Right - csbi.srWindow.Left + 1); - } -#else - struct winsize w; - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) - { - return (uint32_t)w.ws_col; - } -#endif - return 1024; -} - void ProgressBar::SetLogOperationName(Mode InMode, std::string_view Name) { @@ -504,7 +506,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak) uint64_t ETAMS = (NewState.Status == State::EStatus::Running) && (PercentDone > 5) ? (ETAElapsedMS * NewState.RemainingCount) / Completed : 0; - uint32_t ConsoleColumns = GetConsoleColumns(); + uint32_t ConsoleColumns = GetConsoleColumns(1024); const std::string PercentString = fmt::format("{:#3}%", PercentDone); @@ -708,6 +710,7 @@ main(int argc, char** argv) JobCommand JobCmd; OplogMirrorCommand OplogMirrorCmd; SnapshotOplogCommand SnapshotOplogCmd; + OplogDownloadCommand OplogDownload; OplogValidateCommand OplogValidateCmd; PrintCommand PrintCmd; PrintPackageCommand PrintPkgCmd; @@ -764,6 +767,7 @@ main(int argc, char** argv) {"oplog-import", &ImportOplogCmd, "Import project store oplog"}, {"oplog-mirror", &OplogMirrorCmd, "Mirror project store oplog to file system"}, {"oplog-snapshot", &SnapshotOplogCmd, "Snapshot project store oplog"}, + {OplogDownloadCommand::Name, &OplogDownload, OplogDownloadCommand::Description}, {"oplog-validate", &OplogValidateCmd, "Validate oplog for missing references"}, {"print", &PrintCmd, "Print compact binary object"}, {"printpackage", &PrintPkgCmd, "Print compact binary package"}, diff --git a/src/zencore/sentryintegration.cpp b/src/zencore/sentryintegration.cpp index 118c4158a..fef4cd8ed 100644 --- a/src/zencore/sentryintegration.cpp +++ b/src/zencore/sentryintegration.cpp @@ -32,7 +32,7 @@ ZEN_THIRD_PARTY_INCLUDES_END namespace sentry { namespace { - static const std::string DefaultDsn("https://[email protected]/5919284"); + static const std::string DefaultDsn("https://[email protected]/5919284"); } struct SentryAssertImpl : zen::AssertImpl diff --git a/src/zenhttp/httpclientauth.cpp b/src/zenhttp/httpclientauth.cpp index 62e1b77bc..4bbc6405b 100644 --- a/src/zenhttp/httpclientauth.cpp +++ b/src/zenhttp/httpclientauth.cpp @@ -176,9 +176,10 @@ namespace zen { namespace httpclientauth { std::optional<std::function<HttpClientAccessToken()>> CreateFromOidcTokenExecutable(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, - bool Quiet) + bool Quiet, + bool Unattended) { - HttpClientAccessToken InitialToken = GetOidcTokenFromExe(OidcExecutablePath, CloudHost, /* Unattended */ false, Quiet); + HttpClientAccessToken InitialToken = GetOidcTokenFromExe(OidcExecutablePath, CloudHost, Unattended, Quiet); if (InitialToken.IsValid()) { return [OidcExecutablePath = std::filesystem::path(OidcExecutablePath), diff --git a/src/zenhttp/include/zenhttp/httpclientauth.h b/src/zenhttp/include/zenhttp/httpclientauth.h index 32d00f87f..f44cb9b0d 100644 --- a/src/zenhttp/include/zenhttp/httpclientauth.h +++ b/src/zenhttp/include/zenhttp/httpclientauth.h @@ -28,7 +28,8 @@ namespace httpclientauth { std::optional<std::function<HttpClientAccessToken()>> CreateFromOidcTokenExecutable(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, - bool Quiet); + bool Quiet, + bool Unattended); } // namespace httpclientauth } // namespace zen diff --git a/src/zenserver/projectstore/buildsremoteprojectstore.cpp b/src/zenserver/projectstore/buildsremoteprojectstore.cpp index a9dd48510..0b0d8ccd0 100644 --- a/src/zenserver/projectstore/buildsremoteprojectstore.cpp +++ b/src/zenserver/projectstore/buildsremoteprojectstore.cpp @@ -470,7 +470,10 @@ private: }; std::shared_ptr<RemoteProjectStore> -CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, bool Quiet) +CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, + const std::filesystem::path& TempFilePath, + bool Quiet, + bool Unattended) { std::string Url = Options.Url; if (Url.find("://"sv) == std::string::npos) @@ -495,7 +498,8 @@ CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, const st } else if (!Options.OidcExePath.empty()) { - if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet); TokenProviderMaybe) + if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet, Unattended); + TokenProviderMaybe) { TokenProvider = TokenProviderMaybe.value(); } diff --git a/src/zenserver/projectstore/buildsremoteprojectstore.h b/src/zenserver/projectstore/buildsremoteprojectstore.h index 60b6caef7..24ab9678d 100644 --- a/src/zenserver/projectstore/buildsremoteprojectstore.h +++ b/src/zenserver/projectstore/buildsremoteprojectstore.h @@ -26,6 +26,7 @@ struct BuildsRemoteStoreOptions : RemoteStoreOptions std::shared_ptr<RemoteProjectStore> CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, - bool Quiet); + bool Quiet, + bool Unattended); } // namespace zen diff --git a/src/zenserver/projectstore/jupiterremoteprojectstore.cpp b/src/zenserver/projectstore/jupiterremoteprojectstore.cpp index a8d486e2a..da3f92353 100644 --- a/src/zenserver/projectstore/jupiterremoteprojectstore.cpp +++ b/src/zenserver/projectstore/jupiterremoteprojectstore.cpp @@ -343,7 +343,7 @@ private: }; std::shared_ptr<RemoteProjectStore> -CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, bool Quiet) +CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, bool Quiet, bool Unattended) { std::string Url = Options.Url; if (Url.find("://"sv) == std::string::npos) @@ -374,7 +374,8 @@ CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::fi } else if (!Options.OidcExePath.empty()) { - if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet); TokenProviderMaybe) + if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet, Unattended); + TokenProviderMaybe) { TokenProvider = TokenProviderMaybe.value(); } diff --git a/src/zenserver/projectstore/jupiterremoteprojectstore.h b/src/zenserver/projectstore/jupiterremoteprojectstore.h index ac2d25b47..d7a22700a 100644 --- a/src/zenserver/projectstore/jupiterremoteprojectstore.h +++ b/src/zenserver/projectstore/jupiterremoteprojectstore.h @@ -26,6 +26,7 @@ struct JupiterRemoteStoreOptions : RemoteStoreOptions std::shared_ptr<RemoteProjectStore> CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, - bool Quiet); + bool Quiet, + bool Unattended); } // namespace zen diff --git a/src/zenserver/projectstore/projectstore.cpp b/src/zenserver/projectstore/projectstore.cpp index 97175da23..78e014346 100644 --- a/src/zenserver/projectstore/projectstore.cpp +++ b/src/zenserver/projectstore/projectstore.cpp @@ -268,7 +268,7 @@ namespace { ForceDisableBlocks, ForceDisableTempBlocks, AssumeHttp2}; - RemoteStore = CreateJupiterRemoteStore(Options, TempFilePath, /*Quiet*/ false); + RemoteStore = CreateJupiterRemoteStore(Options, TempFilePath, /*Quiet*/ false, /*Unattended*/ false); } if (CbObjectView Zen = Params["zen"sv].AsObjectView(); Zen) @@ -365,7 +365,7 @@ namespace { ForceDisableTempBlocks, AssumeHttp2, MetaData}; - RemoteStore = CreateJupiterBuildsRemoteStore(Options, TempFilePath, /*Quiet*/ false); + RemoteStore = CreateJupiterBuildsRemoteStore(Options, TempFilePath, /*Quiet*/ false, /*Unattended*/ false); } if (!RemoteStore) @@ -4812,11 +4812,11 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { if (WantsRawSizeField) { - RawSizes.resize(Hashes.size(), 0u); + RawSizes.resize(Hashes.size(), (uint64_t)-1); } if (WantsSizeField) { - Sizes.resize(Hashes.size(), 0u); + Sizes.resize(Hashes.size(), (uint64_t)-1); } WorkerThreadPool& WorkerPool = GetSmallWorkerPool(EWorkloadType::Burst); // GetSyncWorkerPool(); @@ -4832,11 +4832,13 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { if (WantsRawSizeField) { + ZEN_ASSERT_SLOW(RawSizes[Index] == (uint64_t)-1); IoHash _; if (CompressedBuffer::ValidateCompressedHeader(Payload, _, RawSizes[Index])) { if (WantsSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); Sizes[Index] = Payload.GetSize(); } } @@ -4850,6 +4852,7 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, } else if (WantsSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); Sizes[Index] = Payload.GetSize(); } } @@ -4857,10 +4860,12 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { if (WantsSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); Sizes[Index] = Payload.GetSize(); } if (WantsRawSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); RawSizes[Index] = Payload.GetSize(); } } diff --git a/src/zenserver/projectstore/remoteprojectstore.cpp b/src/zenserver/projectstore/remoteprojectstore.cpp index 911628386..e61ccd917 100644 --- a/src/zenserver/projectstore/remoteprojectstore.cpp +++ b/src/zenserver/projectstore/remoteprojectstore.cpp @@ -2743,6 +2743,12 @@ ParseOplogContainer(const CbObject& ContainerObject, NiceBytes(Chunked.RawSize), Chunked.ChunkHashes.size()); } + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK), + .ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0, + .Reason = "Operation cancelled"}; + } } size_t NeedBlockCount = 0; @@ -2787,6 +2793,12 @@ ParseOplogContainer(const CbObject& ContainerObject, NeedBlockCount++; } } + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK), + .ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0, + .Reason = "Operation cancelled"}; + } } remotestore_impl::ReportMessage(OptionalContext, fmt::format("Requesting {} of {} attachment blocks", NeedBlockCount, BlocksArray.Num())); @@ -2801,6 +2813,12 @@ ParseOplogContainer(const CbObject& ContainerObject, OnNeedAttachment(AttachmentHash); NeedAttachmentCount++; } + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK), + .ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0, + .Reason = "Operation cancelled"}; + } }; remotestore_impl::ReportMessage(OptionalContext, fmt::format("Requesting {} of {} large attachments", NeedAttachmentCount, LargeChunksArray.Num())); diff --git a/src/zenstore/blockstore.cpp b/src/zenstore/blockstore.cpp index c50f2bb13..b8fa03305 100644 --- a/src/zenstore/blockstore.cpp +++ b/src/zenstore/blockstore.cpp @@ -121,6 +121,12 @@ BlockStoreFile::FileSize() const { return 0; } + uint64_t Expected = 0; + if (!m_CachedFileSize.compare_exchange_strong(Expected, Size)) + { + // Force a new check next time file size is fetched + m_CachedFileSize.store(0); + } return Size; } return m_CachedFileSize; @@ -153,13 +159,8 @@ void BlockStoreFile::Write(const void* Data, uint64_t Size, uint64_t FileOffset) { ZEN_TRACE_CPU("BlockStoreFile::Write"); -#if ZEN_BUILD_DEBUG - if (uint64_t CachedFileSize = m_CachedFileSize.load(); CachedFileSize > 0) - { - ZEN_ASSERT(FileOffset + Size <= CachedFileSize); - } -#endif // ZEN_BUILD_DEBUG m_File.Write(Data, Size, FileOffset); + m_CachedFileSize.store(0); } void diff --git a/src/zenstore/gc.cpp b/src/zenstore/gc.cpp index 1ddb25364..185bc2118 100644 --- a/src/zenstore/gc.cpp +++ b/src/zenstore/gc.cpp @@ -2858,7 +2858,7 @@ GcScheduler::CollectGarbage(const GcClock::TimePoint& CacheExpireTime, { ZEN_WARN("writing gc scheduler state ran out of disk space: '{}'", SystemError.what()); } - else if (RetryCount == 0) + else if (RetryCount == 2) { ZEN_ERROR("writing gc scheduler state failed with system error exception: '{}' ({})", SystemError.what(), @@ -2877,7 +2877,7 @@ GcScheduler::CollectGarbage(const GcClock::TimePoint& CacheExpireTime, } catch (const std::exception& Ex) { - if (RetryCount == 0) + if (RetryCount == 2) { ZEN_ERROR("writing gc scheduler state failed with: '{}'", Ex.what()); } diff --git a/src/zenstore/include/zenstore/blockstore.h b/src/zenstore/include/zenstore/blockstore.h index fce05766f..4006f4275 100644 --- a/src/zenstore/include/zenstore/blockstore.h +++ b/src/zenstore/include/zenstore/blockstore.h @@ -102,12 +102,12 @@ struct BlockStoreFile : public RefCounted IoBuffer GetMetaData() const; private: - std::filesystem::path GetMetaPath() const; - void RemoveMeta(); - const std::filesystem::path m_Path; - IoBuffer m_IoBuffer; - BasicFile m_File; - std::atomic<uint64_t> m_CachedFileSize = 0; + std::filesystem::path GetMetaPath() const; + void RemoveMeta(); + const std::filesystem::path m_Path; + IoBuffer m_IoBuffer; + BasicFile m_File; + mutable std::atomic<uint64_t> m_CachedFileSize = 0; }; class BlockStoreCompactState; diff --git a/src/zenutil/buildstoragecache.cpp b/src/zenutil/buildstoragecache.cpp index e5e8db8d2..376d967d1 100644 --- a/src/zenutil/buildstoragecache.cpp +++ b/src/zenutil/buildstoragecache.cpp @@ -413,4 +413,22 @@ CreateZenBuildStorageCache(HttpClient& HttpClient, return std::make_unique<ZenBuildStorageCache>(HttpClient, Stats, Namespace, Bucket, TempFolderPath, BoostBackgroundThreadCount); } +ZenCacheEndpointTestResult +TestZenCacheEndpoint(std::string_view BaseUrl, const bool AssumeHttp2) +{ + HttpClientSettings TestClientSettings{.LogCategory = "httpcacheclient", + .ConnectTimeout = std::chrono::milliseconds{1000}, + .Timeout = std::chrono::milliseconds{2000}, + .AssumeHttp2 = AssumeHttp2, + .AllowResume = true, + .RetryCount = 0}; + HttpClient TestHttpClient(BaseUrl, TestClientSettings); + HttpClient::Response TestResponse = TestHttpClient.Get("/status/builds"); + if (TestResponse.IsSuccess()) + { + return {.Success = true}; + } + return {.Success = false, .FailureReason = TestResponse.ErrorMessage("")}; +}; + } // namespace zen diff --git a/src/zenutil/include/zenutil/buildstoragecache.h b/src/zenutil/include/zenutil/buildstoragecache.h index a0690a16a..e6ca2c5e4 100644 --- a/src/zenutil/include/zenutil/buildstoragecache.h +++ b/src/zenutil/include/zenutil/buildstoragecache.h @@ -57,4 +57,13 @@ std::unique_ptr<BuildStorageCache> CreateZenBuildStorageCache(HttpClient& H std::string_view Bucket, const std::filesystem::path& TempFolderPath, bool BoostBackgroundThreadCount); + +struct ZenCacheEndpointTestResult +{ + bool Success = false; + std::string FailureReason; +}; + +ZenCacheEndpointTestResult TestZenCacheEndpoint(std::string_view BaseUrl, const bool AssumeHttp2); + } // namespace zen diff --git a/src/zenutil/include/zenutil/jupiter/jupiterbuildstorage.h b/src/zenutil/include/zenutil/jupiter/jupiterbuildstorage.h index bbf070993..f25d8933b 100644 --- a/src/zenutil/include/zenutil/jupiter/jupiterbuildstorage.h +++ b/src/zenutil/include/zenutil/jupiter/jupiterbuildstorage.h @@ -15,4 +15,11 @@ std::unique_ptr<BuildStorage> CreateJupiterBuildStorage(LoggerRef InLog, std::string_view Bucket, bool AllowRedirect, const std::filesystem::path& TempFolderPath); + +bool ParseBuildStorageUrl(std::string_view InUrl, + std::string& OutHost, + std::string& OutNamespace, + std::string& OutBucket, + std::string& OutBuildId); + } // namespace zen diff --git a/src/zenutil/include/zenutil/jupiter/jupiterhost.h b/src/zenutil/include/zenutil/jupiter/jupiterhost.h new file mode 100644 index 000000000..3bbc700b8 --- /dev/null +++ b/src/zenutil/include/zenutil/jupiter/jupiterhost.h @@ -0,0 +1,35 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <string> +#include <string_view> +#include <vector> + +namespace zen { + +struct HttpClientSettings; + +struct JupiterServerDiscovery +{ + struct EndPoint + { + std::string Name; + std::string BaseUrl; + bool AssumeHttp2 = false; + }; + std::vector<EndPoint> ServerEndPoints; + std::vector<EndPoint> CacheEndPoints; +}; + +JupiterServerDiscovery DiscoverJupiterEndpoints(std::string_view Host, const HttpClientSettings& ClientSettings); + +struct JupiterEndpointTestResult +{ + bool Success = false; + std::string FailureReason; +}; + +JupiterEndpointTestResult TestJupiterEndpoint(std::string_view BaseUrl, const bool AssumeHttp2); + +} // namespace zen diff --git a/src/zenutil/jupiter/jupiterbuildstorage.cpp b/src/zenutil/jupiter/jupiterbuildstorage.cpp index 386a91cb3..6eb3489dc 100644 --- a/src/zenutil/jupiter/jupiterbuildstorage.cpp +++ b/src/zenutil/jupiter/jupiterbuildstorage.cpp @@ -14,6 +14,8 @@ ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_map.h> ZEN_THIRD_PARTY_INCLUDES_END +#include <regex> + namespace zen { using namespace std::literals; @@ -511,4 +513,49 @@ CreateJupiterBuildStorage(LoggerRef InLog, return std::make_unique<JupiterBuildStorage>(InLog, InHttpClient, Stats, Namespace, Bucket, AllowRedirect, TempFolderPath); } +bool +ParseBuildStorageUrl(std::string_view InUrl, + std::string& OutHost, + std::string& OutNamespace, + std::string& OutBucket, + std::string& OutBuildId) +{ + std::string Url(InUrl); + const std::string_view ExtendedApiString = "api/v2/builds/"; + if (auto ApiString = ToLower(Url).find(ExtendedApiString); ApiString != std::string::npos) + { + Url.erase(ApiString, ExtendedApiString.length()); + } + + const std::string ArtifactURLRegExString = R"((http[s]?:\/\/.*?)\/(.*?)\/(.*?)\/(.*))"; + const std::regex ArtifactURLRegEx(ArtifactURLRegExString, std::regex::ECMAScript | std::regex::icase); + std::match_results<std::string_view::const_iterator> MatchResults; + std::string_view UrlToParse(Url); + if (regex_match(begin(UrlToParse), end(UrlToParse), MatchResults, ArtifactURLRegEx) && MatchResults.size() == 5) + { + auto GetMatch = [&MatchResults](uint32_t Index) -> std::string_view { + ZEN_ASSERT(Index < MatchResults.size()); + + const auto& Match = MatchResults[Index]; + + return std::string_view(&*Match.first, Match.second - Match.first); + }; + + const std::string_view Host = GetMatch(1); + const std::string_view Namespace = GetMatch(2); + const std::string_view Bucket = GetMatch(3); + const std::string_view BuildId = GetMatch(4); + + OutHost = Host; + OutNamespace = Namespace; + OutBucket = Bucket; + OutBuildId = BuildId; + return true; + } + else + { + return false; + } +} + } // namespace zen diff --git a/src/zenutil/jupiter/jupiterhost.cpp b/src/zenutil/jupiter/jupiterhost.cpp new file mode 100644 index 000000000..d06229cbf --- /dev/null +++ b/src/zenutil/jupiter/jupiterhost.cpp @@ -0,0 +1,66 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenutil/jupiter/jupiterhost.h> + +#include <zencore/compactbinary.h> +#include <zencore/fmtutils.h> +#include <zenhttp/httpclient.h> + +namespace zen { + +JupiterServerDiscovery +DiscoverJupiterEndpoints(std::string_view Host, const HttpClientSettings& ClientSettings) +{ + JupiterServerDiscovery Result; + + HttpClient DiscoveryHttpClient(Host, ClientSettings); + HttpClient::Response ServerInfoResponse = DiscoveryHttpClient.Get("/api/v1/status/servers", HttpClient::Accept(HttpContentType::kJSON)); + if (!ServerInfoResponse.IsSuccess()) + { + ServerInfoResponse.ThrowError(fmt::format("Failed to get list of servers from discovery url '{}'", Host)); + } + std::string_view JsonResponse = ServerInfoResponse.AsText(); + CbObject CbPayload = LoadCompactBinaryFromJson(JsonResponse).AsObject(); + CbArrayView ServerEndpoints = CbPayload["serverEndpoints"].AsArrayView(); + Result.ServerEndPoints.reserve(ServerEndpoints.Num()); + + auto ParseEndPoints = [](CbArrayView ServerEndpoints) -> std::vector<JupiterServerDiscovery::EndPoint> { + std::vector<JupiterServerDiscovery::EndPoint> Result; + + Result.reserve(ServerEndpoints.Num()); + for (CbFieldView ServerEndpointView : ServerEndpoints) + { + CbObjectView ServerEndPoint = ServerEndpointView.AsObjectView(); + Result.push_back(JupiterServerDiscovery::EndPoint{.Name = std::string(ServerEndPoint["name"].AsString()), + .BaseUrl = std::string(ServerEndPoint["baseUrl"].AsString()), + .AssumeHttp2 = ServerEndPoint["baseUrl"].AsBool(false)}); + } + return Result; + }; + + Result.ServerEndPoints = ParseEndPoints(CbPayload["serverEndpoints"].AsArrayView()); + Result.CacheEndPoints = ParseEndPoints(CbPayload["cacheEndpoints"].AsArrayView()); + + return Result; +} + +JupiterEndpointTestResult +TestJupiterEndpoint(std::string_view BaseUrl, const bool AssumeHttp2) +{ + HttpClientSettings TestClientSettings{.LogCategory = "httpbuildsclient", + .ConnectTimeout = std::chrono::milliseconds{1000}, + .Timeout = std::chrono::milliseconds{2000}, + .AssumeHttp2 = AssumeHttp2, + .AllowResume = true, + .RetryCount = 0}; + + HttpClient TestHttpClient(BaseUrl, TestClientSettings); + HttpClient::Response TestResponse = TestHttpClient.Get("/health/live"); + if (TestResponse.IsSuccess()) + { + return {.Success = true}; + } + return {.Success = false, .FailureReason = TestResponse.ErrorMessage("")}; +} + +} // namespace zen |