// Copyright Epic Games, Inc. All Rights Reserved. #include "builds_cmd.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace zen { using namespace std::literals; namespace builds_impl { static std::atomic AbortFlag = false; static std::atomic PauseFlag = false; static void SignalCallbackHandler(int SigNum) { if (SigNum == SIGINT) { PauseFlag = false; AbortFlag = true; } #if ZEN_PLATFORM_WINDOWS if (SigNum == SIGBREAK) { PauseFlag = false; AbortFlag = true; } #endif // ZEN_PLATFORM_WINDOWS } struct ZenStateSharedData { static constexpr uint64_t kMagicV1 = 0x3176646d636e657a; // zencmdv1 uint64_t Magic = 0; // Implies the size and layout of this struct - changes to the data requires change to Magic constant std::atomic Pid; uint8_t SessionId[12]; uint8_t Padding1[4]; std::atomic Abort; std::atomic Pause; uint8_t Padding2[2]; void Initialize() { Magic = kMagicV1; Pid.store(0); memset(SessionId, 0, sizeof(SessionId)); Padding1[0] = Padding1[1] = Padding1[2] = Padding1[3] = 0; Abort.store(0); Pause.store(0); Padding2[0] = Padding2[1] = 0; } }; class ZenState { public: ZenState(const ZenState&) = delete; ZenState& operator=(const ZenState&) = delete; ZenState(); explicit ZenState(uint32_t Pid); ~ZenState(); const ZenStateSharedData& StateData() const { ZEN_ASSERT(m_Data); return *m_Data; } ZenStateSharedData& StateData() { ZEN_ASSERT(m_Data); return *m_Data; } private: static constexpr std::string_view MapBaseName = "UnrealEngineZenCmd_"sv; static constexpr size_t MapSize = sizeof(ZenStateSharedData); bool m_Created = false; std::unique_ptr m_MemMap; ZenStateSharedData* m_Data = nullptr; std::thread m_StateMonitor; Event m_ExitStateMonitorEvent; }; ZenState::ZenState(uint32_t Pid) { const std::string ZenStateMapName = fmt::format("{}{}", MapBaseName, Pid); if (!IsProcessRunning(Pid)) { throw std::runtime_error(fmt::format("The process {} is not running", Pid)); } std::unique_ptr MemMap = OpenSharedMemory(ZenStateMapName, MapSize, false); if (!MemMap) { throw std::runtime_error(fmt::format("The process {} is not a running zen process", Pid)); } ZenStateSharedData* data = (ZenStateSharedData*)MemMap->GetData(); if (uint64_t MemMagic = data->Magic; MemMagic != ZenStateSharedData::kMagicV1) { throw std::runtime_error(fmt::format("The mem map for process {} has an unsupported magic {:x}, expected {:x}", Pid, MemMagic, ZenStateSharedData::kMagicV1)); } if (uint32_t MemPid = data->Pid.load(); MemPid != Pid) { throw std::runtime_error( fmt::format("The mem map for process {} has an missmatching pid of {}, expected {}", Pid, MemPid, Pid)); } m_MemMap = std::move(MemMap); m_Data = data; } ZenState::ZenState() { int Pid = GetCurrentProcessId(); const std::string ZenStateMapName = fmt::format("{}{}", MapBaseName, Pid); std::unique_ptr MemMap = CreateSharedMemory(ZenStateMapName, MapSize, false); if (!MemMap) { throw std::runtime_error(fmt::format("The mem map for process {} could not be created", Pid)); } ZenStateSharedData* data = (ZenStateSharedData*)MemMap->GetData(); data->Initialize(); data->Pid.store(gsl::narrow(Pid)); const Oid SessionId = GetSessionId(); memcpy(data->SessionId, &SessionId, sizeof SessionId); m_MemMap = std::move(MemMap); m_Data = data; m_StateMonitor = std::thread([this]() { while (!m_ExitStateMonitorEvent.Wait(500)) { if (m_Data->Abort.load()) { AbortFlag.store(true); } PauseFlag.store(m_Data->Pause.load()); } }); } ZenState::~ZenState() { try { if (m_StateMonitor.joinable()) { m_ExitStateMonitorEvent.Set(); m_StateMonitor.join(); } m_MemMap.reset(); } catch (const std::exception& Ex) { ZEN_CONSOLE_ERROR("ZenState::~ZenState threw exception: {}", Ex.what()); } } // Debugging knobs for file build storage - always 0 in shipped builds const double DefaultLatency = 0; // .0010; const double DefaultDelayPerKBSec = 0; // 0.00005; void WriteResultObject(const std::filesystem::path& Path, const CbObject& Response) { const MemoryView ResponseView = Response.GetView(); if (ToLower(Path.extension().string()) == ".cbo") { WriteFile(Path, IoBuffer(IoBuffer::Wrap, ResponseView.GetData(), ResponseView.GetSize())); } else { ExtendableStringBuilder<1024> SB; CompactBinaryToJson(ResponseView, SB); WriteFile(Path, IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); } } } // namespace builds_impl ////////////////////////////////////////////////////////////////////////// void BuildsConfiguration::AddSystemOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "system-dir", "Specify system root", cxxopts::value(SystemRootDir), ""); Ops.add_option("", "", "use-sparse-files", "Enable use of sparse files when writing large files. Defaults to true.", cxxopts::value(UseSparseFiles), ""); } void BuildsConfiguration::AddCloudOptions(cxxopts::Options& Ops) { AuthOptions.AddOptions(Ops); Ops.add_option("cloud build", "", "override-host", "Cloud Builds URL", cxxopts::value(OverrideHost), ""); Ops.add_option("cloud build", "", "url", "Cloud Builds host url (legacy - use --override-host)", cxxopts::value(OverrideHost), ""); Ops.add_option("cloud build", "", "cloud-url", "Cloud Artifact URL", cxxopts::value(Url), ""); Ops.add_option("cloud build", "", "host", "Cloud Builds host", cxxopts::value(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(AssumeHttp2), ""); Ops.add_option("cloud build", "", "verbose-http", "Enable verbose option for http client", cxxopts::value(VerboseHttp), ""); Ops.add_option("cloud build", "", "namespace", "Builds Storage namespace", cxxopts::value(Namespace), ""); Ops.add_option("cloud build", "", "bucket", "Builds Storage bucket", cxxopts::value(Bucket), ""); Ops.add_option("cloud build", "", "allow-redirect", "Allow redirect of requests", cxxopts::value(AllowRedirect), ""); } void BuildsConfiguration::AddFileOptions(cxxopts::Options& Ops) { Ops.add_option("filestorage", "", "storage-path", "Builds Storage Path", cxxopts::value(StoragePath), ""); Ops.add_option("filestorage", "", "json-metadata", "Write build, part and block metadata as .json files in addition to .cb files", cxxopts::value(WriteMetadataAsJson), ""); } void BuildsConfiguration::AddCacheOptions(cxxopts::Options& Ops) { Ops.add_option("cache", "", "zen-cache-host", "Host ip and port for zen builds cache", cxxopts::value(ZenCacheHost), ""); } void BuildsConfiguration::AddOutputOptions(cxxopts::Options& Ops) { Ops.add_option("output", "", "plain-progress", "Show progress using plain output", cxxopts::value(PlainProgress), ""); Ops.add_option("output", "", "log-progress", "Write @progress style progress to output", cxxopts::value(LogProgress), ""); Ops.add_option("output", "", "verbose", "Enable verbose console output", cxxopts::value(Verbose), ""); Ops.add_option("output", "", "quiet", "Suppress non-essential output", cxxopts::value(Quiet), ""); } void BuildsConfiguration::AddWorkerOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "boost-worker-count", "Increase the number of worker threads - may cause computer to be less responsive", cxxopts::value(BoostWorkerCount), ""); Ops.add_option("", "", "boost-worker-memory", "Increase the limit where we write downloaded data to temporary storage to conserve space - may cause computer to " "be less responsive due to high memory usage", cxxopts::value(BoostWorkerMemory), ""); Ops.add_option("", "", "boost-workers", "Enables both 'boost-worker-count' and 'boost-worker-memory' - may cause computer to be less responsive", cxxopts::value(BoostWorkers), ""); } void BuildsConfiguration::AddZenFolderOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "zen-folder-path", "Path to the zen state and temp folder used by this command. Overrides the command-specific default.", cxxopts::value(ZenFolderPath), ""); } void BuildsConfiguration::AddChunkingCacheOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "chunking-cache-path", "Path to cache for chunking information of scanned files. Default is empty resulting in no caching", cxxopts::value(ChunkingCachePath), ""); } void BuildsConfiguration::AddWildcardOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "wildcard", "Windows style wildcard(s) (using * and ?) to match file paths to include, separated by ;", cxxopts::value(IncludeWildcard), ""); Ops.add_option("", "", "exclude-wildcard", "Windows style wildcard(s) (using * and ?) to match file paths to exclude, separated by ;. Applied after --wildcard " "include filter", cxxopts::value(ExcludeWildcard), ""); } void BuildsConfiguration::AddExcludeFolderOption(cxxopts::Options& Ops) { Ops.add_option("", "", "exclude-folders", "Names of folders to exclude, separated by ;", cxxopts::value(ExcludeFolders), ""); } void BuildsConfiguration::AddExcludeExtensionsOption(cxxopts::Options& Ops) { Ops.add_option("", "", "exclude-extensions", "Extensions to exclude, separated by ;" "include filter", cxxopts::value(ExcludeExtensions), ""); } void BuildsConfiguration::AddMultipartOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "allow-multipart", "Allow large attachments to be transfered using multipart protocol. Defaults to true.", cxxopts::value(AllowMultiparts), ""); } void BuildsConfiguration::AddPartialBlockRequestOptions(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(AllowPartialBlockRequests), ""); } void BuildsConfiguration::AddAppendNewContentOptions(cxxopts::Options& Ops) { Ops.add_option("", "", "append", "Decides if the remote data should replace or append to the current local data.\n" " false = the local content will be replaced by the remote content\n" " true = the remote data will be overlayed on top of local data\n" "Defaults to false.", cxxopts::value(AppendNewContent), ""); } BuildsCommand::BuildsCommand() : m_ListNamespacesSubCmd(m_Configuration) , m_ListSubCmd(m_Configuration) , m_ListBlocksSubCmd(m_Configuration) , m_UploadSubCmd(m_Configuration) , m_DownloadSubCmd(m_Configuration) , m_LsSubCmd(m_Configuration) , m_DiffSubCmd(m_Configuration) , m_FetchBlobSubCmd(m_Configuration) , m_PrimeCacheSubCmd(m_Configuration) , m_PauseSubCmd(m_Configuration) , m_ResumeSubCmd(m_Configuration) , m_AbortSubCmd(m_Configuration) , m_ValidatePartSubCmd(m_Configuration) , m_TestSubCmd(m_Configuration) , m_MultiTestDownloadSubCmd(m_Configuration) { m_Options.add_options()("h,help", "Print help"); m_Options.add_option("__hidden__", "", "subcommand", "", cxxopts::value(m_SubCommand)->default_value(""), ""); m_Options.parse_positional({"subcommand"}); AddSubCommand(m_ListNamespacesSubCmd); AddSubCommand(m_ListSubCmd); AddSubCommand(m_ListBlocksSubCmd); AddSubCommand(m_UploadSubCmd); AddSubCommand(m_DownloadSubCmd); AddSubCommand(m_LsSubCmd); AddSubCommand(m_DiffSubCmd); AddSubCommand(m_FetchBlobSubCmd); AddSubCommand(m_PrimeCacheSubCmd); AddSubCommand(m_PauseSubCmd); AddSubCommand(m_ResumeSubCmd); AddSubCommand(m_AbortSubCmd); AddSubCommand(m_ValidatePartSubCmd); AddSubCommand(m_TestSubCmd); AddSubCommand(m_MultiTestDownloadSubCmd); } BuildsCommand::~BuildsCommand() = default; bool BuildsCommand::OnParentOptionsParsed(const ZenCliOptions& /*GlobalOptions*/) { using namespace builds_impl; // Held in member variables so the handlers stay installed through the // subcommand's Run() and are restored when BuildsCommand is destroyed. m_SigIntGuard.emplace(SIGINT, SignalCallbackHandler); #if ZEN_PLATFORM_WINDOWS m_SigBreakGuard.emplace(SIGBREAK, SignalCallbackHandler); #endif // Validate output options if (m_Configuration.Verbose && m_Configuration.Quiet) { throw OptionParseException("'--verbose' conflicts with '--quiet'", {}); } if (m_Configuration.LogProgress && m_Configuration.PlainProgress) { throw OptionParseException("'--plain-progress' conflicts with '--log-progress'", {}); } if (m_Configuration.LogProgress && m_Configuration.Quiet) { throw OptionParseException("'--quiet' conflicts with '--log-progress'", {}); } if (m_Configuration.PlainProgress && m_Configuration.Quiet) { throw OptionParseException("'--quiet' conflicts with '--plain-progress'", {}); } if (m_Configuration.LogProgress) { m_Configuration.ProgressMode = ConsoleProgressMode::Log; } else if (m_Configuration.PlainProgress) { m_Configuration.ProgressMode = ConsoleProgressMode::Plain; } else if (m_Configuration.Quiet) { m_Configuration.ProgressMode = ConsoleProgressMode::Quiet; } else { m_Configuration.ProgressMode = ConsoleProgressMode::Pretty; } if (m_Configuration.BoostWorkers) { m_Configuration.BoostWorkerCount = true; m_Configuration.BoostWorkerMemory = true; } // Parse system options if (m_Configuration.SystemRootDir.empty()) { m_Configuration.SystemRootDir = PickDefaultSystemRootDirectory(); } MakeSafeAbsolutePathInPlace(m_Configuration.SystemRootDir); MakeSafeAbsolutePathInPlace(m_Configuration.ChunkingCachePath); return true; } std::atomic& BuildsSubCmdBase::AbortFlag() const { return builds_impl::AbortFlag; } std::atomic& BuildsSubCmdBase::PauseFlag() const { return builds_impl::PauseFlag; } std::unique_ptr BuildsSubCmdBase::CreateProgress() const { return std::unique_ptr(CreateConsoleProgress(m_Config.ProgressMode)); } ////////////////////////////////////////////////////////////////////////// void BuildsSubCmdBase::LogBanner() { if (!m_Config.Quiet) { ZenCmdBase::LogExecutableVersionAndPid(); } } void BuildsSubCmdBase::LogWorkersInfo(const TransferThreadWorkers& Workers) { if (!m_Config.Quiet) { ZEN_CONSOLE("{}", Workers.GetWorkersInfo()); } } void BuildsSubCmdBase::EnsureZenFolderExists() { m_CreatedZenFolder = CreateDirectories(GetZenFolderPath()); } void BuildsSubCmdBase::CleanZenFolder() { using namespace builds_impl; WorkerThreadPool& Pool = GetSmallWorkerPool(EWorkloadType::Burst); if (m_CreatedZenFolder) { CleanAndRemoveDirectory(Pool, AbortFlag(), PauseFlag(), GetZenFolderPath()); } else { CleanAndRemoveDirectory(Pool, AbortFlag(), PauseFlag(), ZenTempFolderPath(GetZenFolderPath())); } } ////////////////////////////////////////////////////////////////////////// BuildsSubCmdBase::ResolvedStorage BuildsSubCmdBase::ParseStorageOptions(std::string& BuildId, const CreateBuildStorageOptions& Options, cxxopts::Options& SubOpts, const std::filesystem::path& SystemRootDirOverride, const std::filesystem::path& StoragePathOverride) { ResolvedStorage Resolved{.SystemRootDir = SystemRootDirOverride.empty() ? m_Config.SystemRootDir : SystemRootDirOverride, .Host = m_Config.Host, .Namespace = m_Config.Namespace, .Bucket = m_Config.Bucket, .StoragePath = StoragePathOverride.empty() ? m_Config.StoragePath : StoragePathOverride}; if (!m_Config.Url.empty()) { if (!Resolved.Host.empty()) { throw OptionParseException(fmt::format("'--host' ('{}') conflicts with '--url' ('{}')", Resolved.Host, m_Config.Url), SubOpts.help()); } if (!Resolved.Bucket.empty()) { throw OptionParseException(fmt::format("'--bucket' ('{}') conflicts with '--url' ('{}')", Resolved.Bucket, m_Config.Url), SubOpts.help()); } if (!BuildId.empty()) { throw OptionParseException(fmt::format("'--buildid' ('{}') conflicts with '--url' ('{}')", BuildId, m_Config.Url), SubOpts.help()); } if (!ParseBuildStorageUrl(m_Config.Url, Resolved.Host, Resolved.Namespace, Resolved.Bucket, BuildId)) { throw OptionParseException( fmt::format("'--url' ('{}') is malformed, it does not match the Cloud Artifact URL format", m_Config.Url), SubOpts.help()); } } if (!m_Config.OverrideHost.empty() || !Resolved.Host.empty()) { if (!Resolved.StoragePath.empty()) { throw OptionParseException( fmt::format("'--storage-path' ('{}') conflicts with '--host'/'--url'/'--override-host' options", Resolved.StoragePath), SubOpts.help()); } if (Options.RequireNamespace && Resolved.Namespace.empty()) { throw OptionParseException("'--namespace' is required", SubOpts.help()); } if (Options.RequireBucket && Resolved.Bucket.empty()) { throw OptionParseException("'--bucket' is required", SubOpts.help()); } } else if (Resolved.StoragePath.empty()) { throw OptionParseException("'--host', '--url', '--override-host' or '--storage-path' is required", SubOpts.help()); } MakeSafeAbsolutePathInPlace(Resolved.StoragePath); return Resolved; } StorageInstance BuildsSubCmdBase::CreateBuildStorage(const std::filesystem::path& ZenFolder, const CreateBuildStorageOptions& Options, cxxopts::Options& SubOpts, BuildStorageBase::Statistics& StorageStats, BuildStorageCache::Statistics& StorageCacheStats, std::unique_ptr& Auth, const ResolvedStorage& Resolved) { using namespace builds_impl; // Empty ZenFolder => operate without a temp directory and keep all downloads in memory. const bool HasZenFolder = !ZenFolder.empty(); const std::filesystem::path StorageTempPath = HasZenFolder ? ZenTempFolderPath(ZenFolder) / "storage" : std::filesystem::path{}; const std::filesystem::path CacheTempPath = HasZenFolder ? ZenTempFolderPath(ZenFolder) / "zencache" : std::filesystem::path{}; HttpClientSettings ClientSettings{ .LogCategory = "httpbuildsclient", .AssumeHttp2 = m_Config.AssumeHttp2, .AllowResume = true, .RetryCount = 2, .Verbose = m_Config.VerboseHttp, .MaximumInMemoryDownloadSize = HasZenFolder ? GetMaxMemoryBufferSize(DefaultMaxChunkBlockSize, m_Config.BoostWorkerMemory) : std::numeric_limits::max()}; std::string StorageDescription; std::string CacheDescription; StorageInstance Result; if (!Resolved.Host.empty() || !m_Config.OverrideHost.empty()) { AuthCommandLineOptions AuthOpts = m_Config.AuthOptions; AuthOpts.ParseOptions(SubOpts, Resolved.SystemRootDir, ClientSettings, Resolved.Host.empty() ? m_Config.OverrideHost : Resolved.Host, Auth, m_Config.Quiet, /*Hidden*/ false, m_Config.Verbose); BuildStorageResolveResult ResolveRes = ResolveBuildStorage(ConsoleLog(), ClientSettings, Resolved.Host, m_Config.OverrideHost, m_Config.ZenCacheHost, ZenCacheResolveMode::All, m_Config.Verbose); if (!ResolveRes.Cloud.Address.empty()) { ClientSettings.AssumeHttp2 = ResolveRes.Cloud.AssumeHttp2; Result.BuildStorageHttp = std::make_unique(ResolveRes.Cloud.Address, ClientSettings, [this]() { return AbortFlag().load(); }); Result.BuildStorage = CreateJupiterBuildStorage(Log(), *Result.BuildStorageHttp, StorageStats, Resolved.Namespace, Resolved.Bucket, m_Config.AllowRedirect, StorageTempPath); Result.BuildStorageHost = ResolveRes.Cloud; uint64_t HostLatencyNs = ResolveRes.Cloud.LatencySec >= 0 ? uint64_t(ResolveRes.Cloud.LatencySec * 1000000000.0) : 0; StorageDescription = fmt::format("Cloud {}{}. SessionId: '{}'. Namespace '{}', Bucket '{}'. Latency: {}", ResolveRes.Cloud.Name, (ResolveRes.Cloud.Address == ResolveRes.Cloud.Name) ? "" : fmt::format(" {}", ResolveRes.Cloud.Address), Result.BuildStorageHttp->GetSessionId(), Resolved.Namespace, Resolved.Bucket, NiceLatencyNs(HostLatencyNs)); if (!ResolveRes.Cache.Address.empty()) { Result.CacheHttp = std::make_unique( ResolveRes.Cache.Address, HttpClientSettings{ .LogCategory = "httpcacheclient", .ConnectTimeout = std::chrono::milliseconds{3000}, .Timeout = std::chrono::milliseconds{30000}, .AssumeHttp2 = ResolveRes.Cache.AssumeHttp2, .AllowResume = true, .RetryCount = 0, .Verbose = m_Config.VerboseHttp, .MaximumInMemoryDownloadSize = GetMaxMemoryBufferSize(DefaultMaxChunkBlockSize, m_Config.BoostWorkerMemory)}, [this]() { return AbortFlag().load(); }); Result.CacheStorage = CreateZenBuildStorageCache(*Result.CacheHttp, StorageCacheStats, Resolved.Namespace, Resolved.Bucket, CacheTempPath, Options.BoostCacheBackgroundWorkers ? GetSmallWorkerPool(EWorkloadType::Background) : GetTinyWorkerPool(EWorkloadType::Background)); Result.CacheHost = ResolveRes.Cache; Result.SetupCacheSession(ResolveRes.Cache.Address, fmt::format("builds {}", m_SubOptions.program()), GetSessionId()); uint64_t CacheLatencyNs = ResolveRes.Cache.LatencySec >= 0 ? uint64_t(ResolveRes.Cache.LatencySec * 1000000000.0) : 0; CacheDescription = fmt::format("Zen {}{}. SessionId: '{}'. Latency: {}", ResolveRes.Cache.Name, (ResolveRes.Cache.Address == ResolveRes.Cache.Name) ? "" : fmt::format(" {}", ResolveRes.Cache.Address), Result.CacheHttp->GetSessionId(), NiceLatencyNs(CacheLatencyNs)); if (!Resolved.Namespace.empty()) { CacheDescription += fmt::format(". Namespace '{}'", Resolved.Namespace); } if (!Resolved.Bucket.empty()) { CacheDescription += fmt::format(" Bucket '{}'", Resolved.Bucket); } } } } else if (!Resolved.StoragePath.empty()) { StorageDescription = fmt::format("folder {}", Resolved.StoragePath); Result.BuildStorage = CreateFileBuildStorage(Resolved.StoragePath, StorageStats, false, DefaultLatency, DefaultDelayPerKBSec); Result.BuildStorageHost = BuildStorageResolveResult::Host{.Address = Resolved.StoragePath.generic_string(), .Name = "Disk", .LatencySec = 1.0 / 100000, // 1 us .Caps = {.MaxRangeCountPerRequest = 2048u}}; if (!m_Config.ZenCacheHost.empty()) { ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(m_Config.ZenCacheHost, m_Config.AssumeHttp2, m_Config.VerboseHttp); if (TestResult.Success) { Result.CacheHttp = std::make_unique( m_Config.ZenCacheHost, HttpClientSettings{ .LogCategory = "httpcacheclient", .ConnectTimeout = std::chrono::milliseconds{3000}, .Timeout = std::chrono::milliseconds{30000}, .AssumeHttp2 = m_Config.AssumeHttp2, .AllowResume = true, .RetryCount = 0, .Verbose = m_Config.VerboseHttp, .MaximumInMemoryDownloadSize = GetMaxMemoryBufferSize(DefaultMaxChunkBlockSize, m_Config.BoostWorkerMemory)}, [this]() { return AbortFlag().load(); }); Result.CacheStorage = CreateZenBuildStorageCache(*Result.CacheHttp, StorageCacheStats, Resolved.Namespace, Resolved.Bucket, CacheTempPath, Options.BoostCacheBackgroundWorkers ? GetSmallWorkerPool(EWorkloadType::Background) : GetTinyWorkerPool(EWorkloadType::Background)); Result.CacheHost = BuildStorageResolveResult::Host{.Address = m_Config.ZenCacheHost, .Name = m_Config.ZenCacheHost, .AssumeHttp2 = m_Config.AssumeHttp2, .LatencySec = TestResult.LatencySeconds, .Caps = {.MaxRangeCountPerRequest = TestResult.MaxRangeCountPerRequest}}; Result.SetupCacheSession(m_Config.ZenCacheHost, fmt::format("builds {}", m_SubOptions.program()), GetSessionId()); CacheDescription = fmt::format("Zen {}. SessionId: '{}'", Result.CacheHost.Name, Result.CacheHttp->GetSessionId()); if (!Resolved.Namespace.empty()) { CacheDescription += fmt::format(". Namespace '{}'", Resolved.Namespace); } if (!Resolved.Bucket.empty()) { CacheDescription += fmt::format(" Bucket '{}'", Resolved.Bucket); } } } } else { throw OptionParseException("'--host', '--url', '--override-host' or '--storage-path' is required", SubOpts.help()); } if (!m_Config.Quiet) { ZEN_CONSOLE("Remote: {}", StorageDescription); if (!Result.CacheHost.Name.empty()) { ZEN_CONSOLE("Cache : {}", CacheDescription); } } return Result; } Oid BuildsSubCmdBase::ParseBuildId(const std::string& BuildIdStr, cxxopts::Options& SubOpts) { const Oid BuildId = Oid::TryFromHexString(BuildIdStr); if (BuildId == Oid::Zero) { throw OptionParseException( fmt::format("'--build-id' ('{}') is malformed, expected {} hex characters", BuildIdStr, Oid::StringLength), SubOpts.help()); } return BuildId; } Oid BuildsSubCmdBase::ParseBuildPartId(const std::string& BuildPartIdStr, cxxopts::Options& SubOpts) { const Oid BuildPartId = Oid::TryFromHexString(BuildPartIdStr); if (BuildPartId == Oid::Zero) { throw OptionParseException( fmt::format("'--build-part-id' ('{}') is malformed, expected {} hex characters", BuildPartIdStr, Oid::StringLength), SubOpts.help()); } return BuildPartId; } std::vector BuildsSubCmdBase::ParseBuildPartIds(const std::vector& BuildPartIdStrs, cxxopts::Options& SubOpts) { std::vector BuildPartIds; for (const std::string& BuildPartId : BuildPartIdStrs) { BuildPartIds.push_back(Oid::TryFromHexString(RemoveQuotes(BuildPartId))); if (BuildPartIds.back() == Oid::Zero) { throw OptionParseException(fmt::format("'--build-part-id' ('{}') is malformed", BuildPartId), SubOpts.help()); } } return BuildPartIds; } std::vector BuildsSubCmdBase::ParseBuildPartNames(const std::vector& BuildPartNameStrs, cxxopts::Options& SubOpts) { std::vector BuildPartNames; for (const std::string& BuildPartName : BuildPartNameStrs) { BuildPartNames.push_back(std::string(RemoveQuotes(BuildPartName))); if (BuildPartNames.back().empty()) { throw OptionParseException(fmt::format("'--build-part-names' ('{}') is invalid", BuildPartName), SubOpts.help()); } } return BuildPartNames; } CbObject BuildsSubCmdBase::ParseBuildMetadata(bool CreateBuild, std::filesystem::path& BuildMetadataPath, const std::string& BuildMetadata, cxxopts::Options& SubOpts) { if (CreateBuild) { if (BuildMetadataPath.empty() && BuildMetadata.empty()) { throw OptionParseException("'--metadata-path' or '--metadata' is required", SubOpts.help()); } if (!BuildMetadataPath.empty() && !BuildMetadata.empty()) { throw OptionParseException( fmt::format("'--metadata-path' ('{}') conflicts with '--metadata' ('{}')", BuildMetadataPath.string(), BuildMetadata), SubOpts.help()); } if (!BuildMetadataPath.empty()) { MakeSafeAbsolutePathInPlace(BuildMetadataPath); IoBuffer MetaDataJson = ReadFile(BuildMetadataPath).Flatten(); std::string_view Json(reinterpret_cast(MetaDataJson.GetData()), MetaDataJson.GetSize()); std::string JsonError; CbObject MetaData = LoadCompactBinaryFromJson(Json, JsonError).AsObject(); if (!JsonError.empty()) { throw std::runtime_error( fmt::format("build metadata file '{}' is malformed. Reason: '{}'", BuildMetadataPath.string(), JsonError)); } return MetaData; } if (!BuildMetadata.empty()) { CbObjectWriter MetaDataWriter(1024); ForEachStrTok(BuildMetadata, ';', [&](std::string_view Pair) { size_t SplitPos = Pair.find('='); if (SplitPos == std::string::npos || SplitPos == 0) { throw std::runtime_error(fmt::format("build metadata key-value pair '{}' is malformed", Pair)); } MetaDataWriter.AddString(Pair.substr(0, SplitPos), Pair.substr(SplitPos + 1)); return true; }); return MetaDataWriter.Save(); } } else { if (!BuildMetadataPath.empty()) { throw OptionParseException("'--metadata-path' requires '--create-build'", SubOpts.help()); } if (!BuildMetadata.empty()) { throw OptionParseException("'--metadata' requires '--create-build'", SubOpts.help()); } } return {}; } void BuildsSubCmdBase::ParsePath(std::filesystem::path& Path, cxxopts::Options& SubOpts) { if (Path.empty()) { throw OptionParseException("'--local-path' is required", SubOpts.help()); } MakeSafeAbsolutePathInPlace(Path); } IoHash BuildsSubCmdBase::ParseBlobHash(const std::string& BlobHashStr, cxxopts::Options& SubOpts) { if (BlobHashStr.empty()) { throw OptionParseException("'--blob-hash' is required", SubOpts.help()); } if (BlobHashStr.length() != IoHash::StringLength) { throw OptionParseException( fmt::format("'--blob-hash' ('{}') is malformed, it must be {} characters long", BlobHashStr, IoHash::StringLength), SubOpts.help()); } IoHash BlobHash; if (!IoHash::TryParse(BlobHashStr, BlobHash)) { throw OptionParseException(fmt::format("'--blob-hash' ('{}') is malformed", BlobHashStr), SubOpts.help()); } return BlobHash; } void BuildsSubCmdBase::ParseFileFilters(std::vector& OutIncludeWildcards, std::vector& OutExcludeWildcards) { auto SplitAndAppendWildcard = [](const std::string_view Wildcard, std::vector& Output) { ForEachStrTok(Wildcard, ';', [&Output](std::string_view Wildcard) { if (!Wildcard.empty()) { std::string CleanWildcard(ToLower(Wildcard)); for (auto It = begin(CleanWildcard); It != end(CleanWildcard); It++) { if (*It == '\\') { *It = '/'; } } if (CleanWildcard.starts_with("./") || CleanWildcard.starts_with(".\\")) { CleanWildcard = CleanWildcard.substr(2); } Output.emplace_back(std::move(CleanWildcard)); } return true; }); }; SplitAndAppendWildcard(m_Config.IncludeWildcard, OutIncludeWildcards); SplitAndAppendWildcard(m_Config.ExcludeWildcard, OutExcludeWildcards); } void BuildsSubCmdBase::ParseExcludeFolderAndExtension(std::vector& OutExcludeFolders, std::vector& OutExcludeExtensions) { auto SplitAndAppendExclusion = [](const std::string_view Input, std::vector& Output) { ForEachStrTok(Input, ";,", [&Output](std::string_view Exclusion) { if (!Exclusion.empty()) { std::string CleanExclusion(ToLower(Exclusion)); if (CleanExclusion.length() > 2 && CleanExclusion.front() == '"' && CleanExclusion.back() == '"') { CleanExclusion = CleanExclusion.substr(1, CleanExclusion.length() - 2); } Output.emplace_back(std::move(CleanExclusion)); } return true; }); }; SplitAndAppendExclusion(m_Config.ExcludeFolders, OutExcludeFolders); SplitAndAppendExclusion(m_Config.ExcludeExtensions, OutExcludeExtensions); } void BuildsSubCmdBase::SetZenFolderPath(const std::filesystem::path& Fallback) { m_ResolvedZenFolderPath = m_Config.ZenFolderPath.empty() ? Fallback : m_Config.ZenFolderPath; MakeSafeAbsolutePathInPlace(m_ResolvedZenFolderPath); } void BuildsSubCmdBase::ResolveZenFolderPath(const std::filesystem::path& LocalPath) { using namespace builds_impl; if (!m_Config.ZenFolderPath.empty()) { m_ResolvedZenFolderPath = m_Config.ZenFolderPath; } else if (!LocalPath.empty()) { m_ResolvedZenFolderPath = LocalPath / ZenFolderName; } else { m_ResolvedZenFolderPath = std::filesystem::current_path() / ZenFolderName; } MakeSafeAbsolutePathInPlace(m_ResolvedZenFolderPath); } EPartialBlockRequestMode BuildsSubCmdBase::ParseAllowPartialBlockRequests(cxxopts::Options& SubOpts) { EPartialBlockRequestMode Mode = PartialBlockRequestModeFromString(m_Config.AllowPartialBlockRequests); if (Mode == EPartialBlockRequestMode::Invalid) { throw OptionParseException(fmt::format("'--allow-partial-block-requests' ('{}') is invalid", m_Config.AllowPartialBlockRequests), SubOpts.help()); } return Mode; } void BuildsSubCmdBase::ParseZenProcessId(int& ZenProcessId) { if (ZenProcessId == -1) { const std::filesystem::path RunningExecutablePath = GetRunningExecutablePath(); ProcessHandle RunningProcess; std::error_code Ec = FindProcess(RunningExecutablePath, RunningProcess, /*IncludeSelf*/ false); if (Ec) { throw std::runtime_error(fmt::format("Failed finding process running '{}', reason: '{}'", RunningExecutablePath, Ec.message())); } if (!RunningProcess.IsValid()) { throw std::runtime_error(fmt::format("Unable to find a running instance of the zen executable '{}'", RunningExecutablePath)); } ZenProcessId = RunningProcess.Pid(); } } ////////////////////////////////////////////////////////////////////////// BuildsListNamespacesSubCmd::BuildsListNamespacesSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "list-namespaces", "List all namespaces and optionally their buckets") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddZenFolderOptions(Opts); Opts.add_option("", "", "recursive", "Enable fetch of buckets within namespaces also", cxxopts::value(m_Recursive), ""); Opts.add_option("", "", "result-path", "Path to json (.json) or compactbinary (.cbo) to write output result to. Default is output to console", cxxopts::value(m_ResultPath), ""); Opts.parse_positional({"result-path"}); Opts.positional_help("result-path"); } void BuildsListNamespacesSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { if (!m_ResultPath.empty()) { LogBanner(); } cxxopts::Options& Opts = SubOptions(); std::string DummyBuildId; BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(DummyBuildId, {.RequireNamespace = false, .RequireBucket = false}, Opts); // Read-only listing: no local zen folder needed; downloads stay in memory. StorageInstance Storage = CreateBuildStorage({}, {}, Opts, StorageStats, CacheStats, Auth, Resolved); CbObject Response = Storage.BuildStorage->ListNamespaces(m_Recursive); ZEN_ASSERT(ValidateCompactBinary(Response.GetView(), CbValidateMode::Default) == CbValidateError::None); if (m_ResultPath.empty()) { ExtendableStringBuilder<1024> SB; CompactBinaryToJson(Response.GetView(), SB); ZEN_CONSOLE("{}", SB.ToView()); } else { builds_impl::WriteResultObject(MakeSafeAbsolutePath(m_ResultPath), Response); } } ////////////////////////////////////////////////////////////////////////// BuildsListSubCmd::BuildsListSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "list", "List builds matching a query") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddZenFolderOptions(Opts); Opts.add_option("", "", "query-path", "Path to json or compactbinary file containing list query", cxxopts::value(m_QueryPath), ""); Opts.add_option("", "", "result-path", "Path to json (.json) or compactbinary (.cbo) to write output result to. Default is output to console", cxxopts::value(m_ResultPath), ""); Opts.parse_positional({"query-path", "result-path"}); Opts.positional_help("query-path result-path"); } void BuildsListSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { if (!m_ResultPath.empty()) { LogBanner(); } cxxopts::Options& Opts = SubOptions(); MakeSafeAbsolutePathInPlace(m_QueryPath); MakeSafeAbsolutePathInPlace(m_ResultPath); std::string JsonQuery; if (m_QueryPath.empty()) { CbObjectWriter QueryWriter; QueryWriter.BeginObject("query"); QueryWriter.EndObject(); // query CbObject QueryObject = QueryWriter.Save(); ExtendableStringBuilder<64> SB; CompactBinaryToJson(QueryObject, SB); JsonQuery = SB.ToString(); } else { if (ToLower(m_QueryPath.extension().string()) == ".cbo") { CbObject QueryObject = LoadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_QueryPath)); ExtendableStringBuilder<64> SB; CompactBinaryToJson(QueryObject, SB); JsonQuery = SB.ToString(); } else { IoBuffer MetaDataJson = ReadFile(m_QueryPath).Flatten(); std::string_view Json(reinterpret_cast(MetaDataJson.GetData()), MetaDataJson.GetSize()); std::string JsonError; CbObject QueryObject = LoadCompactBinaryFromJson(Json, JsonError) .AsObject(); // We try to convert it so it is at least reaonably verified in format if (!JsonError.empty()) { throw std::runtime_error( fmt::format("build metadata file '{}' is malformed. Reason: '{}'", m_QueryPath.string(), JsonError)); } JsonQuery = std::string(Json); } } std::string DummyBuildId; BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(DummyBuildId, {.RequireBucket = false}, Opts); // Read-only listing: no local zen folder needed; downloads stay in memory. StorageInstance Storage = CreateBuildStorage({}, {}, Opts, StorageStats, CacheStats, Auth, Resolved); CbObject Response = Storage.BuildStorage->ListBuilds(JsonQuery); ZEN_ASSERT(ValidateCompactBinary(Response.GetView(), CbValidateMode::Default) == CbValidateError::None); if (m_ResultPath.empty()) { ExtendableStringBuilder<1024> SB; CompactBinaryToJson(Response.GetView(), SB); ZEN_CONSOLE("{}", SB.ToView()); } else { builds_impl::WriteResultObject(m_ResultPath, Response); } } ////////////////////////////////////////////////////////////////////////// BuildsListBlocksSubCmd::BuildsListBlocksSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "list-blocks", "List blocks for a build") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddOutputOptions(Opts); Config.AddZenFolderOptions(Opts); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "result-path", "Path to json (.json) or compactbinary (.cbo) to write output result to. Default is output to console", cxxopts::value(m_ResultPath), ""); Opts.add_option("", "", "max-count", "Maximum number of blocks to list", cxxopts::value(m_MaxCount), ""); Opts.parse_positional({"build-id"}); Opts.positional_help("build-id"); } void BuildsListBlocksSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { if (!m_ResultPath.empty()) { LogBanner(); } cxxopts::Options& Opts = SubOptions(); MakeSafeAbsolutePathInPlace(m_ResultPath); if (m_MaxCount == 0) { throw OptionParseException(fmt::format("'--max-count' ('{}') is invalid", m_MaxCount), Opts.help()); } BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); // Read-only listing: no local zen folder needed; downloads stay in memory. StorageInstance Storage = CreateBuildStorage({}, {}, Opts, StorageStats, CacheStats, Auth, Resolved); const Oid BuildId = ParseBuildId(m_BuildId, Opts); CbObject Response = Storage.BuildStorage->FindBlocks(BuildId, m_MaxCount); ZEN_ASSERT(ValidateCompactBinary(Response.GetView(), CbValidateMode::Default) == CbValidateError::None); std::vector BlockDescriptions = ParseChunkBlockDescriptionList(Response); if (!m_Config.Quiet) { ZEN_CONSOLE("Response contains {} block", BlockDescriptions.size()); } if (m_ResultPath.empty()) { ExtendableStringBuilder<1024> SB; CompactBinaryToJson(Response.GetView(), SB); ForEachStrTok(SB.ToView(), '\n', [](std::string_view Row) { ZEN_CONSOLE("{}", Row); return true; }); } else { builds_impl::WriteResultObject(m_ResultPath, Response); } } ////////////////////////////////////////////////////////////////////////// BuildsUploadSubCmd::BuildsUploadSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "upload", "Upload a folder to build storage") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddWorkerOptions(Opts); Config.AddZenFolderOptions(Opts); Config.AddExcludeFolderOption(Opts); Config.AddExcludeExtensionsOption(Opts); Config.AddChunkingCacheOptions(Opts); Opts.add_option("", "l", "local-path", "Root file system folder for build", cxxopts::value(m_Path), ""); Opts.add_option("", "", "create-build", "Set to true to create the containing build, if unset a builds-id must be given and the build already exist", cxxopts::value(m_CreateBuild), ""); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "build-part-id", "Build part Id, if not given it will be auto generated", cxxopts::value(m_BuildPartId), ""); Opts.add_option("", "", "build-part-name", "Name of the build part, if not given it will be be named after the directory name at end of local-path", cxxopts::value(m_BuildPartName), ""); Opts.add_option("", "", "metadata-path", "Path to json file that holds the metadata for the build. Requires the create-build option to be set", cxxopts::value(m_BuildMetadataPath), ""); Opts.add_option( "", "", "metadata", "Key-value pairs separated by ';' with build meta data. (key1=value1;key2=value2). Requires the create-build option to be set", cxxopts::value(m_BuildMetadata), ""); Opts.add_option("", "", "clean", "Ignore existing blocks", cxxopts::value(m_Clean), ""); Opts.add_option("", "", "block-min-reuse", "Percent of an existing block that must be relevant for it to be resused. Defaults to 85.", cxxopts::value(m_BlockReuseMinPercentLimit), ""); Opts.add_option("cache", "", "zen-cache-upload", "Upload data downloaded from remote host to zen cache", cxxopts::value(m_UploadToZenCache), ""); Config.AddMultipartOptions(Opts); Opts.add_option("", "", "manifest-path", "Path to a text file with one line of [TAB] per file to include or a " "structured .json file describing the parts", cxxopts::value(m_ManifestPath), ""); Opts.add_option("", "", "verify", "Enable post upload verify of all uploaded data", cxxopts::value(m_PostUploadVerify), ""); Opts.add_option("", "", "find-max-block-count", "The maximum number of blocks we search for in the build context", cxxopts::value(m_FindBlockMaxCount), ""); Opts.parse_positional({"local-path", "build-id"}); Opts.positional_help("local-path build-id"); } void BuildsUploadSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); ParsePath(m_Path, Opts); builds_impl::ZenState InstanceState; BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); ResolveZenFolderPath({}); StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {}, Opts, StorageStats, CacheStats, Auth, Resolved); EnsureZenFolderExists(); auto _ = MakeGuard([this]() { CleanZenFolder(); }); if (m_BuildPartName.empty() && m_ManifestPath.empty()) { m_BuildPartName = m_Path.filename().string(); } const Oid BuildId = m_BuildId.empty() ? Oid::NewOid() : ParseBuildId(m_BuildId, Opts); if (m_BuildId.empty()) { m_BuildId = BuildId.ToString(); } Oid BuildPartId; if (!m_BuildPartId.empty()) { BuildPartId = ParseBuildPartId(m_BuildPartId, Opts); } CbObject MetaData = ParseBuildMetadata(m_CreateBuild, m_BuildMetadataPath, m_BuildMetadata, Opts); const std::filesystem::path TempDir = ZenTempFolderPath(GetZenFolderPath()); std::vector ExcludeFolders = DefaultExcludeFolders; std::vector ExcludeExtensions = DefaultExcludeExtensions; ParseExcludeFolderAndExtension(ExcludeFolders, ExcludeExtensions); std::unique_ptr ChunkController = CreateStandardChunkingController(StandardChunkingControllerSettings{}); std::unique_ptr ChunkCache = m_Config.ChunkingCachePath.empty() ? CreateNullChunkingCache() : CreateDiskChunkingCache(m_Config.ChunkingCachePath, *ChunkController, 256u * 1024u); std::unique_ptr Progress = CreateProgress(); std::vector> UploadedParts = UploadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), BuildId, BuildPartId, m_BuildPartName, m_Path, m_ManifestPath, MetaData, *ChunkController, *ChunkCache, UploadFolderOptions{.TempDir = TempDir, .FindBlockMaxCount = m_FindBlockMaxCount, .BlockReuseMinPercentLimit = m_BlockReuseMinPercentLimit, .AllowMultiparts = m_Config.AllowMultiparts, .CreateBuild = m_CreateBuild, .IgnoreExistingBlocks = m_Clean, .UploadToZenCache = m_UploadToZenCache, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .ExcludeFolders = ExcludeFolders, .ExcludeExtensions = ExcludeExtensions}); if (!AbortFlag()) { if (m_PostUploadVerify) { for (const auto& Part : UploadedParts) { ValidateBuildPart(ConsoleLog(), *Progress, AbortFlag(), PauseFlag(), m_Config.Quiet, m_Config.Verbose, Workers, *Storage.BuildStorage, ZenTempFolderPath(GetZenFolderPath()) / "validate", BuildId, Part.first, Part.second); } } } if (!m_Config.Quiet) { const BuildStorageResolveResult::Host& Host = Storage.BuildStorageHost; const BuildStorageBase::Statistics& Stats = StorageStats; ZEN_CONSOLE( "{}:\n" "Read: {}\n" "Write: {}\n" "Requests: {}\n" "Avg Request Time: {}\n" "Avg I/O Time: {}", Host.Name, NiceBytes(Stats.TotalBytesRead.load()), NiceBytes(Stats.TotalBytesWritten.load()), Stats.TotalRequestCount.load(), Stats.TotalExecutionTimeUs.load() > 0 ? NiceTimeSpanMs(Stats.TotalExecutionTimeUs.load() / 1000 / Stats.TotalRequestCount.load()) : 0, Stats.TotalRequestCount.load() > 0 ? NiceTimeSpanMs(Stats.TotalRequestTimeUs.load() / 1000 / Stats.TotalRequestCount.load()) : 0); } if (AbortFlag()) { throw std::runtime_error("Upload aborted"); } } ////////////////////////////////////////////////////////////////////////// BuildsDownloadSubCmd::BuildsDownloadSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "download", "Download a build to a local folder") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddZenFolderOptions(Opts); Config.AddWorkerOptions(Opts); Config.AddWildcardOptions(Opts); Config.AddAppendNewContentOptions(Opts); Config.AddExcludeFolderOption(Opts); Opts.add_option("", "l", "local-path", "Root file system folder for build", cxxopts::value(m_Path), ""); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "build-part-id", "Build part Ids list separated by ','.", cxxopts::value(m_BuildPartIds), ""); Opts.add_option("", "", "build-part-name", "Build part names list separated by ','. If neither --build-part-id nor --build-part-name is given, " "the part named 'default' is selected. Use '*' (alone) to select all parts.", cxxopts::value(m_BuildPartNames), ""); Opts.add_option("", "", "clean", "Delete all data in target folder that is not part of the downloaded content", cxxopts::value(m_Clean), ""); Opts.add_option("", "", "force", "Force download of all content by ignoring any existing local content", cxxopts::value(m_Force), ""); Opts.add_option("cache", "", "zen-cache-upload", "Upload data downloaded from remote host to zen cache", cxxopts::value(m_UploadToZenCache), ""); Config.AddMultipartOptions(Opts); Config.AddPartialBlockRequestOptions(Opts); Opts.add_option( "", "", "download-spec-path", "Path to a text file with one line of per file to include or a structured .json file describing what to download.", cxxopts::value(m_DownloadSpecPath), ""); Opts.add_option("", "", "verify", "Enable post download verify of all tracked files", cxxopts::value(m_PostDownloadVerify), ""); Opts.add_option("", "", "enable-scavenge", "Enable scavenging of data from previouse download locations", cxxopts::value(m_EnableScavenging), ""); Opts.add_option("", "", "allow-file-clone", "Enable use of block reference counting when copying files", cxxopts::value(m_AllowFileClone), ""); Opts.parse_positional({"local-path", "build-id", "build-part-name"}); Opts.positional_help("local-path build-id build-part-name"); } void BuildsDownloadSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); ParsePath(m_Path, Opts); std::vector IncludeWildcards; std::vector ExcludeWildcards; ParseFileFilters(IncludeWildcards, ExcludeWildcards); builds_impl::ZenState InstanceState; BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); // Preserve zen folder on exit - /current_state.cbo tracks what has been downloaded into // this folder and is consulted by later download operations against the same target. ResolveZenFolderPath(m_Path); StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {}, Opts, StorageStats, CacheStats, Auth, Resolved); const Oid BuildId = ParseBuildId(m_BuildId, Opts); std::vector BuildPartIds = ParseBuildPartIds(m_BuildPartIds, Opts); std::vector BuildPartNames = ParseBuildPartNames(m_BuildPartNames, Opts); NormalizePartSelection(BuildPartIds, BuildPartNames, Opts.help()); EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(Opts); if (m_Config.AppendNewContent && m_Clean) { throw OptionParseException("'--append' conflicts with '--clean'", Opts.help()); } std::vector ExcludeFolders = DefaultExcludeFolders; std::vector ExcludeExtensions = DefaultExcludeExtensions; ParseExcludeFolderAndExtension(ExcludeFolders, ExcludeExtensions); std::unique_ptr Progress = CreateProgress(); DownloadFolder( ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), CacheStats, BuildId, BuildPartIds, BuildPartNames, m_DownloadSpecPath, m_Path, DownloadOptions{.SystemRootDir = m_Config.SystemRootDir, .ZenFolderPath = GetZenFolderPath(), .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = m_Clean, .PostDownloadVerify = m_PostDownloadVerify, .EnableOtherDownloadsScavenging = m_EnableScavenging && !m_Force, .EnableTargetFolderScavenging = !m_Force, .AllowFileClone = m_AllowFileClone, .IncludeWildcards = IncludeWildcards, .ExcludeWildcards = ExcludeWildcards, .MaximumInMemoryPayloadSize = GetMaxMemoryBufferSize(DefaultMaxChunkBlockSize, m_Config.BoostWorkerMemory), .PopulateCache = m_UploadToZenCache, .AppendNewContent = m_Config.AppendNewContent, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles, .ExcludeFolders = ExcludeFolders}); if (AbortFlag()) { throw std::runtime_error("Download aborted"); } } ////////////////////////////////////////////////////////////////////////// BuildsLsSubCmd::BuildsLsSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "ls", "List files in a build") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddZenFolderOptions(Opts); Config.AddWorkerOptions(Opts); Config.AddWildcardOptions(Opts); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "build-part-id", "Build part Ids list separated by ','.", cxxopts::value(m_BuildPartIds), ""); Opts.add_option("", "", "build-part-name", "Build part names list separated by ','. If neither --build-part-id nor --build-part-name is given, " "the part named 'default' is selected. Use '*' (alone) to select all parts.", cxxopts::value(m_BuildPartNames), ""); Opts.add_option("", "", "result-path", "Path to json (.json) or compactbinary (.cbo) to write output result to. Default is output to console", cxxopts::value(m_ResultPath), ""); Opts.add_option("", "o", "output-path", "Path to output, extension .json or .cb (compac binary). Default is output to console", cxxopts::value(m_ResultPath), ""); Opts.parse_positional({"build-id", "wildcard"}); Opts.positional_help("build-id wildcard"); } void BuildsLsSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { if (!m_ResultPath.empty()) { LogBanner(); } cxxopts::Options& Opts = SubOptions(); builds_impl::ZenState InstanceState; std::vector IncludeWildcards; std::vector ExcludeWildcards; ParseFileFilters(IncludeWildcards, ExcludeWildcards); BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); // Read-only listing: no local zen folder needed; downloads stay in memory. StorageInstance Storage = CreateBuildStorage({}, {}, Opts, StorageStats, CacheStats, Auth, Resolved); const Oid BuildId = ParseBuildId(m_BuildId, Opts); std::vector BuildPartIds = ParseBuildPartIds(m_BuildPartIds, Opts); std::vector BuildPartNames = ParseBuildPartNames(m_BuildPartNames, Opts); NormalizePartSelection(BuildPartIds, BuildPartNames, Opts.help()); std::unique_ptr StructuredOutput; if (!m_ResultPath.empty()) { MakeSafeAbsolutePathInPlace(m_ResultPath); StructuredOutput = std::make_unique(); } ListBuild(m_Config.Quiet, Storage, BuildId, BuildPartIds, BuildPartNames, IncludeWildcards, ExcludeWildcards, StructuredOutput.get()); if (StructuredOutput) { CbObject Response = StructuredOutput->Save(); builds_impl::WriteResultObject(m_ResultPath, Response); } if (AbortFlag()) { throw std::runtime_error("List build aborted"); } } ////////////////////////////////////////////////////////////////////////// BuildsDiffSubCmd::BuildsDiffSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "diff", "Diff two local folders") { cxxopts::Options& Opts = SubOptions(); Config.AddOutputOptions(Opts); Config.AddWorkerOptions(Opts); Config.AddExcludeFolderOption(Opts); Config.AddExcludeExtensionsOption(Opts); Config.AddChunkingCacheOptions(Opts); Opts.add_option("", "l", "local-path", "Root file system folder used as base", cxxopts::value(m_Path), ""); Opts.add_option("", "c", "compare-path", "Root file system folder used as diff", cxxopts::value(m_DiffPath), ""); Opts.add_option("", "", "only-chunked", "Skip files from diff summation that are not processed with chunking", cxxopts::value(m_OnlyChunked), ""); Opts.parse_positional({"local-path", "compare-path"}); Opts.positional_help("local-path compare-path"); } void BuildsDiffSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); ParsePath(m_Path, Opts); if (m_DiffPath.empty()) { throw OptionParseException("'--compare-path' is required", Opts.help()); } MakeSafeAbsolutePathInPlace(m_DiffPath); std::vector ExcludeFolders = DefaultExcludeFolders; std::vector ExcludeExtensions = DefaultExcludeExtensions; ParseExcludeFolderAndExtension(ExcludeFolders, ExcludeExtensions); StandardChunkingControllerSettings ChunkingSettings; std::unique_ptr ChunkController = CreateStandardChunkingController(ChunkingSettings); std::unique_ptr ChunkCache = m_Config.ChunkingCachePath.empty() ? CreateNullChunkingCache() : CreateDiskChunkingCache(m_Config.ChunkingCachePath, *ChunkController, 256u * 1024u); if (m_OnlyChunked) { ExcludeExtensions.insert(ExcludeExtensions.end(), ChunkingSettings.SplitOnlyExtensions.begin(), ChunkingSettings.SplitOnlyExtensions.end()); ExcludeExtensions.insert(ExcludeExtensions.end(), ChunkingSettings.SplitAndCompressExtensions.begin(), ChunkingSettings.SplitAndCompressExtensions.end()); } std::unique_ptr Progress = CreateProgress(); DiffFolders(*Progress, AbortFlag(), PauseFlag(), m_Config.Quiet, Workers, m_Path, m_DiffPath, *ChunkController, *ChunkCache, ExcludeFolders, ExcludeExtensions); if (AbortFlag()) { throw std::runtime_error("Diff folders aborted"); } } ////////////////////////////////////////////////////////////////////////// BuildsFetchBlobSubCmd::BuildsFetchBlobSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "fetch-blob", "Fetch and validate a specific blob") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddZenFolderOptions(Opts); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "blob-hash", "IoHash in hex form identifying the blob to download", cxxopts::value(m_BlobHash), ""); Opts.parse_positional({"build-id", "blob-hash"}); Opts.positional_help("build-id blob-hash"); } void BuildsFetchBlobSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); cxxopts::Options& Opts = SubOptions(); BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); ResolveZenFolderPath({}); StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {}, Opts, StorageStats, CacheStats, Auth, Resolved); EnsureZenFolderExists(); auto _ = MakeGuard([this]() { CleanZenFolder(); }); IoHash BlobHash = ParseBlobHash(m_BlobHash, Opts); const Oid BuildId = ParseBuildId(m_BuildId, Opts); uint64_t CompressedSize; uint64_t DecompressedSize; ValidateBlob(AbortFlag(), *Storage.BuildStorage, BuildId, BlobHash, CompressedSize, DecompressedSize); if (AbortFlag()) { throw std::runtime_error("Fetch blob aborted"); } if (!m_Config.Quiet) { ZEN_CONSOLE("Blob '{}' has a compressed size {} and a decompressed size of {} bytes", BlobHash, CompressedSize, DecompressedSize); } } ////////////////////////////////////////////////////////////////////////// BuildsPrimeCacheSubCmd::BuildsPrimeCacheSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "prime-cache", "Prime the zen cache with build data") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddWorkerOptions(Opts); Config.AddZenFolderOptions(Opts); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "build-part-id", "Build part Ids list separated by ','.", cxxopts::value(m_BuildPartIds), ""); Opts.add_option("", "", "build-part-name", "Build part names list separated by ','. If neither --build-part-id nor --build-part-name is given, " "the part named 'default' is selected. Use '*' (alone) to select all parts.", cxxopts::value(m_BuildPartNames), ""); Opts.add_option("", "", "force", "Force download of all blobs by ignoring any existing blobs in cache", cxxopts::value(m_Force), ""); Opts.parse_positional({"build-id"}); Opts.positional_help("build-id"); } void BuildsPrimeCacheSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); ResolveZenFolderPath({}); StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {.BoostCacheBackgroundWorkers = true}, Opts, StorageStats, CacheStats, Auth, Resolved); EnsureZenFolderExists(); auto _ = MakeGuard([this]() { CleanZenFolder(); }); const Oid BuildId = ParseBuildId(m_BuildId, Opts); std::vector BuildPartIds = ParseBuildPartIds(m_BuildPartIds, Opts); std::vector BuildPartNames = ParseBuildPartNames(m_BuildPartNames, Opts); NormalizePartSelection(BuildPartIds, BuildPartNames, Opts.help()); std::uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; CbObject BuildObject = GetBuild(*Storage.BuildStorage, BuildId, m_Config.Quiet); std::vector> AllBuildParts = ResolveBuildPartNames(BuildObject, BuildId, BuildPartIds, BuildPartNames, PreferredMultipartChunkSize); std::vector AllBuildPartIds; AllBuildPartIds.reserve(AllBuildParts.size()); for (const std::pair& BuildPart : AllBuildParts) { AllBuildPartIds.push_back(BuildPart.first); } std::unique_ptr Progress = CreateProgress(); Progress->SetLogOperationName("Prime Cache"); BuildsOperationPrimeCache PrimeOp(ConsoleLog(), *Progress, Storage, AbortFlag(), PauseFlag(), Workers.GetNetworkPool(), BuildId, AllBuildPartIds, BuildsOperationPrimeCache::Options{.IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .ZenFolderPath = GetZenFolderPath(), .LargeAttachmentSize = PreferredMultipartChunkSize * 4u, .PreferredMultipartChunkSize = PreferredMultipartChunkSize, .ForceUpload = m_Force}, CacheStats); PrimeOp.Execute(); if (!m_Config.Quiet && Storage.CacheStorage) { ZEN_CONSOLE("Uploaded {} ({}) blobs to {}", CacheStats.PutBlobCount.load(), NiceBytes(CacheStats.PutBlobByteCount), Storage.CacheHost.Name); } } ////////////////////////////////////////////////////////////////////////// namespace { void AddProcessIdOption(cxxopts::Options& Opts, int& ZenProcessId) { Opts.add_option("", "", "process-id", "Process id of running process", cxxopts::value(ZenProcessId), ""); Opts.parse_positional({"process-id"}); Opts.positional_help("process-id"); } } // namespace BuildsPauseSubCmd::BuildsPauseSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "pause", "Pause a running zen process") { AddProcessIdOption(SubOptions(), m_ZenProcessId); } void BuildsPauseSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { ParseZenProcessId(m_ZenProcessId); builds_impl::ZenState RunningState(m_ZenProcessId); RunningState.StateData().Pause.store(1); } ////////////////////////////////////////////////////////////////////////// BuildsResumeSubCmd::BuildsResumeSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "resume", "Resume a paused zen process") { AddProcessIdOption(SubOptions(), m_ZenProcessId); } void BuildsResumeSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { ParseZenProcessId(m_ZenProcessId); builds_impl::ZenState RunningState(m_ZenProcessId); RunningState.StateData().Pause.store(0); } ////////////////////////////////////////////////////////////////////////// BuildsAbortSubCmd::BuildsAbortSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "abort", "Abort a running zen process") { AddProcessIdOption(SubOptions(), m_ZenProcessId); } void BuildsAbortSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { ParseZenProcessId(m_ZenProcessId); builds_impl::ZenState RunningState(m_ZenProcessId); RunningState.StateData().Abort.store(1); } ////////////////////////////////////////////////////////////////////////// BuildsValidatePartSubCmd::BuildsValidatePartSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "validate-part", "Validate a build part") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddWorkerOptions(Opts); Config.AddZenFolderOptions(Opts); Opts.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); Opts.add_option("", "", "build-part-id", "Build part Id, if not given it will be auto generated", cxxopts::value(m_BuildPartId), ""); Opts.add_option("", "", "build-part-name", "Name of the build part, if not given it will be be named after the directory name at end of local-path", cxxopts::value(m_BuildPartName), ""); Opts.parse_positional({"build-id", "build-part-id"}); Opts.positional_help("build-id build-part-id"); } void BuildsValidatePartSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(m_BuildId, {}, Opts); ResolveZenFolderPath({}); StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {}, Opts, StorageStats, CacheStats, Auth, Resolved); EnsureZenFolderExists(); auto _ = MakeGuard([this]() { CleanZenFolder(); }); builds_impl::ZenState InstanceState; Oid BuildId = ParseBuildId(m_BuildId, Opts); if (!m_BuildPartName.empty() && !m_BuildPartId.empty()) { throw OptionParseException( fmt::format("'--build-part-id' ('{}') conflicts with '--build-part-name' ('{}')", m_BuildPartId, m_BuildPartName), Opts.help()); } const Oid BuildPartId = m_BuildPartName.empty() ? Oid::Zero : ParseBuildPartId(m_BuildPartId, Opts); std::unique_ptr Progress = CreateProgress(); ValidateBuildPart(ConsoleLog(), *Progress, AbortFlag(), PauseFlag(), m_Config.Quiet, m_Config.Verbose, Workers, *Storage.BuildStorage, ZenTempFolderPath(GetZenFolderPath()) / "validate", BuildId, BuildPartId, m_BuildPartName); if (AbortFlag()) { throw std::runtime_error("Validate build part failed"); } } ////////////////////////////////////////////////////////////////////////// BuildsTestSubCmd::BuildsTestSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "test", "Run an upload/download test cycle") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddWorkerOptions(Opts); Opts.add_option("", "l", "local-path", "Root file system folder used as base", cxxopts::value(m_Path), ""); Config.AddMultipartOptions(Opts); Config.AddPartialBlockRequestOptions(Opts); Config.AddWildcardOptions(Opts); Config.AddAppendNewContentOptions(Opts); Config.AddChunkingCacheOptions(Opts); Opts.add_option("", "", "enable-scavenge", "Enable scavenging of data from previouse download locations", cxxopts::value(m_EnableScavenging), ""); Opts.add_option("", "", "allow-file-clone", "Enable use of block reference counting when copying files", cxxopts::value(m_AllowFileClone), ""); Opts.parse_positional({"local-path"}); Opts.positional_help("local-path"); } void BuildsTestSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); m_TestSystemRootDir = (GetRunningExecutablePath().parent_path() / ".tmpzensystem").make_preferred(); CreateDirectories(m_TestSystemRootDir); CleanDirectory(m_TestSystemRootDir, /*ForceRemoveReadOnlyFiles*/ true); auto SystemGuard = MakeGuard([this]() { DeleteDirectories(m_TestSystemRootDir); }); ParsePath(m_Path, Opts); const bool CreatedTempStorage = m_Config.OverrideHost.empty() && m_Config.StoragePath.empty(); if (CreatedTempStorage) { m_TestStoragePath = (GetRunningExecutablePath().parent_path() / ".tmpstore").make_preferred(); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), m_TestStoragePath); CreateDirectories(m_TestStoragePath); m_TestStoragePath = m_TestStoragePath.generic_string(); } auto StorageGuard = MakeGuard([this, CreatedTempStorage]() { if (CreatedTempStorage) { DeleteDirectories(m_TestStoragePath); } }); EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(Opts); BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics StorageCacheStats; m_BuildPartName = m_Path.filename().string(); const std::filesystem::path DownloadPath = m_Path.parent_path() / (m_BuildPartName + "_test"); const std::filesystem::path DownloadPath2 = m_Path.parent_path() / (m_BuildPartName + "_test2"); const std::filesystem::path DownloadPath3 = m_Path.parent_path() / (m_BuildPartName + "_test3"); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath2); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath3); auto DownloadGuard = MakeGuard([this, &Workers, DownloadPath, DownloadPath2, DownloadPath3]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath2); CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath3); }); std::unique_ptr Auth; std::string TestBuildId; const ResolvedStorage Resolved = ParseStorageOptions(TestBuildId, {}, Opts, m_TestSystemRootDir, m_TestStoragePath); // Place scratch next to the folder under test so upload does not self-include, and so the // scratch area shares the volume of the source data. { const std::u8string PathStr = m_Path.generic_u8string(); const IoHash PathHash = IoHash::HashBuffer(PathStr.data(), PathStr.length()); SetZenFolderPath(m_Path.parent_path() / fmt::format("zen_{}", PathHash)); } StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {}, Opts, StorageStats, StorageCacheStats, Auth, Resolved); EnsureZenFolderExists(); auto ZenGuard = MakeGuard([this]() { CleanZenFolder(); }); const Oid BuildId = Oid::NewOid(); const Oid BuildPartId = Oid::NewOid(); m_BuildId = BuildId.ToString(); m_BuildPartId = BuildPartId.ToString(); m_CreateBuild = true; auto MakeMetaData = [](const Oid& BuildId) -> CbObject { CbObjectWriter BuildMetaDataWriter; { const uint32_t CL = BuildId.OidBits[2]; BuildMetaDataWriter.AddString("name", fmt::format("++Test+Main-CL-{}", CL)); BuildMetaDataWriter.AddString("branch", "ZenTestBuild"); BuildMetaDataWriter.AddString("baselineBranch", "ZenTestBuild"); BuildMetaDataWriter.AddString("platform", "Windows"); BuildMetaDataWriter.AddString("project", "Test"); BuildMetaDataWriter.AddInteger("changelist", CL); BuildMetaDataWriter.AddString("buildType", "test-folder"); } return BuildMetaDataWriter.Save(); }; CbObject MetaData = MakeMetaData(Oid::TryFromHexString(m_BuildId)); { ExtendableStringBuilder<256> SB; CompactBinaryToJson(MetaData, SB); ZEN_CONSOLE("Upload Build {}, Part {} ({}) from '{}'\n{}", m_BuildId, BuildPartId, m_BuildPartName, m_Path, SB.ToView()); } const std::filesystem::path UploadTempDir = ZenTempFolderPath(GetZenFolderPath()); std::unique_ptr ChunkController = CreateStandardChunkingController(StandardChunkingControllerSettings{}); std::unique_ptr ChunkCache = m_Config.ChunkingCachePath.empty() ? CreateNullChunkingCache() : CreateDiskChunkingCache(m_Config.ChunkingCachePath, *ChunkController, 256u * 1024u); std::unique_ptr Progress = CreateProgress(); UploadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), BuildId, BuildPartId, m_BuildPartName, m_Path, {}, MetaData, *ChunkController, *ChunkCache, UploadFolderOptions{.TempDir = UploadTempDir, .FindBlockMaxCount = m_FindBlockMaxCount, .BlockReuseMinPercentLimit = m_BlockReuseMinPercentLimit, .AllowMultiparts = m_Config.AllowMultiparts, .CreateBuild = true, .IgnoreExistingBlocks = false, .UploadToZenCache = m_UploadToZenCache, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Upload build)"); } { ZEN_CONSOLE("Upload Build {}, Part {} ({}) from '{}' with chunking cache", m_BuildId, BuildPartId, m_BuildPartName, m_Path); UploadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), Oid::NewOid(), Oid::NewOid(), m_BuildPartName, m_Path, {}, MetaData, *ChunkController, *ChunkCache, UploadFolderOptions{.TempDir = UploadTempDir, .FindBlockMaxCount = m_FindBlockMaxCount, .BlockReuseMinPercentLimit = m_BlockReuseMinPercentLimit, .AllowMultiparts = m_Config.AllowMultiparts, .CreateBuild = true, .IgnoreExistingBlocks = false, .UploadToZenCache = m_UploadToZenCache, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Upload again, chunking is cached)"); } } ValidateBuildPart(ConsoleLog(), *Progress, AbortFlag(), PauseFlag(), m_Config.Quiet, m_Config.Verbose, Workers, *Storage.BuildStorage, ZenTempFolderPath(GetZenFolderPath()) / "validate", BuildId, BuildPartId, m_BuildPartName); if (!m_Config.IncludeWildcard.empty() || !m_Config.ExcludeWildcard.empty()) { auto WcGuard = MakeGuard([&]() { CleanAndRemoveDirectory(Workers.GetIOWorkerPool(), AbortFlag(), PauseFlag(), DownloadPath); }); ZEN_CONSOLE("\nDownload Filtered Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); std::vector IncludeWildcards; std::vector ExcludeWildcards; ParseFileFilters(IncludeWildcards, ExcludeWildcards); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = true, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = false, .AllowFileClone = m_AllowFileClone, .IncludeWildcards = IncludeWildcards, .ExcludeWildcards = ExcludeWildcards, .AppendNewContent = false, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download build)"); } ZEN_CONSOLE("\nDownload Filtered Out Remaining of Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = true, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IncludeWildcards = ExcludeWildcards, .ExcludeWildcards = IncludeWildcards, .AppendNewContent = true, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download build)"); } ZEN_CONSOLE("\nDownload Full Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IncludeWildcards = {}, .ExcludeWildcards = {}, .AppendNewContent = false, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download build)"); } } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = true, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = false, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download build)"); } ZEN_CONSOLE("\nRe-download Build {}, Part {} ({}) to '{}' (identical target)", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Re-download identical target)"); } auto ScrambleDir = [&Workers, this](const std::filesystem::path& Path) { ZEN_CONSOLE("\nScrambling '{}'", Path); Stopwatch Timer; DirectoryContent DownloadContent; GetDirectoryContent( Path, DirectoryContentFlags::Recursive | DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::IncludeFileSizes, DownloadContent); auto IsAcceptedFolder = [ExcludeFolders = DefaultExcludeFolders, Path](const std::filesystem::path& AbsolutePath) -> bool { std::string RelativePath = std::filesystem::relative(AbsolutePath, Path).generic_string(); for (const std::string& ExcludeFolder : ExcludeFolders) { if (RelativePath.starts_with(ExcludeFolder)) { if (RelativePath.length() == ExcludeFolder.length()) { return false; } else if (RelativePath[ExcludeFolder.length()] == '/') { return false; } } } return true; }; ParallelWork Work(AbortFlag(), PauseFlag(), WorkerThreadPool::EMode::EnableBacklog); uint32_t Randomizer = 0; auto FileSizeIt = DownloadContent.FileSizes.begin(); for (const std::filesystem::path& FilePath : DownloadContent.Files) { if (IsAcceptedFolder(FilePath)) { uint32_t Case = (Randomizer++) % 7; switch (Case) { case 0: { uint64_t SourceSize = *FileSizeIt; if (SourceSize > 256) { Work.ScheduleWork( Workers.GetIOWorkerPool(), [this, SourceSize, FilePath = std::filesystem::path(FilePath)](std::atomic&) { if (!AbortFlag()) { bool WasReadOnly = SetFileReadOnly(FilePath, false); { BasicFile Source(FilePath, BasicFile::Mode::kWrite); uint64_t RangeSize = Min(SourceSize / 3, 512u * 1024u); IoBuffer TempBuffer1(RangeSize); IoBuffer TempBuffer2(RangeSize); IoBuffer TempBuffer3(RangeSize); Source.Read(TempBuffer1.GetMutableView().GetData(), RangeSize, 0); Source.Read(TempBuffer2.GetMutableView().GetData(), RangeSize, SourceSize / 2); Source.Read(TempBuffer3.GetMutableView().GetData(), RangeSize, SourceSize - RangeSize); Source.Write(TempBuffer1, SourceSize / 2); Source.Write(TempBuffer2, SourceSize - RangeSize); Source.Write(TempBuffer3, SourceSize - 0); } if (WasReadOnly) { SetFileReadOnly(FilePath, true); } } }); } } break; case 1: { (void)SetFileReadOnly(FilePath, false); std::error_code Ec; const bool Removed = RemoveFile(FilePath, Ec); if (!Removed || Ec) { throw zen::runtime_error("ScrambleDir: failed to delete '{}'. Reason: '{}'", FilePath, Ec ? Ec.message() : "file no longer present"); } } break; default: break; } } FileSizeIt++; } Work.Wait(5000, [&](bool IsAborted, bool IsPaused, std::ptrdiff_t PendingWork) { ZEN_UNUSED(IsAborted, IsPaused); ZEN_CONSOLE("Scrambling files, {} remaining", PendingWork); }); ZEN_ASSERT(!AbortFlag().load()); ZEN_CONSOLE("Scrambled files in {}", NiceTimeSpanMs(Timer.GetElapsedTimeMs())); }; ScrambleDir(DownloadPath); ZEN_CONSOLE("\nRe-download Build {}, Part {} ({}) to '{}' (scrambled target)", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Re-download scrambled target)"); } ScrambleDir(DownloadPath); Oid BuildId2 = Oid::NewOid(); Oid BuildPartId2 = Oid::NewOid(); CbObject MetaData2 = MakeMetaData(BuildId2); { ExtendableStringBuilder<256> SB; CompactBinaryToJson(MetaData, SB); ZEN_CONSOLE("\nUpload scrambled Build {}, Part {} ({})\n{}\n", BuildId2, BuildPartId2, m_BuildPartName, SB.ToView()); } UploadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), BuildId2, BuildPartId2, m_BuildPartName, DownloadPath, {}, MetaData2, *ChunkController, *ChunkCache, UploadFolderOptions{.TempDir = UploadTempDir, .FindBlockMaxCount = m_FindBlockMaxCount, .BlockReuseMinPercentLimit = m_BlockReuseMinPercentLimit, .AllowMultiparts = m_Config.AllowMultiparts, .CreateBuild = true, .IgnoreExistingBlocks = false, .UploadToZenCache = m_UploadToZenCache, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Upload scrambled)"); } ValidateBuildPart(ConsoleLog(), *Progress, AbortFlag(), PauseFlag(), m_Config.Quiet, m_Config.Verbose, Workers, *Storage.BuildStorage, ZenTempFolderPath(GetZenFolderPath()) / "validate", BuildId, BuildPartId, m_BuildPartName); ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (original)", BuildId, BuildPartId, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download original)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (scrambled)", BuildId2, BuildPartId2, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId2, {BuildPartId2}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download scrambled)"); } ZEN_CONSOLE("\nRe-download Build {}, Part {} ({}) to '{}' (scrambled)", BuildId2, BuildPartId2, m_BuildPartName, DownloadPath); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId2, {BuildPartId2}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Re-download scrambled)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (original)", BuildId, BuildPartId, m_BuildPartName, DownloadPath2); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath2, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath2 / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download original)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (original)", BuildId, BuildPartId, m_BuildPartName, DownloadPath3); DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), StorageCacheStats, BuildId, {BuildPartId}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, DownloadPath3, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = DownloadPath3 / ZenFolderName, .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = false, .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = true, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Test aborted. (Download original)"); } } ////////////////////////////////////////////////////////////////////////// BuildsMultiTestDownloadSubCmd::BuildsMultiTestDownloadSubCmd(BuildsConfiguration& Config) : BuildsSubCmdBase(Config, "multi-test-download", "Download multiple builds sequentially as a test") { cxxopts::Options& Opts = SubOptions(); Config.AddSystemOptions(Opts); Config.AddCloudOptions(Opts); Config.AddFileOptions(Opts); Config.AddOutputOptions(Opts); Config.AddCacheOptions(Opts); Config.AddWorkerOptions(Opts); Opts.add_option("", "l", "local-path", "Root file system folder used as base", cxxopts::value(m_Path), ""); Opts.add_option("", "", "build-ids", "Build Ids list separated by ','", cxxopts::value(m_BuildIds), ""); Opts.add_option("", "", "enable-scavenge", "Enable scavenging of data from previouse download locations", cxxopts::value(m_EnableScavenging), ""); Opts.add_option("", "", "allow-file-clone", "Enable use of block reference counting when copying files", cxxopts::value(m_AllowFileClone), ""); Opts.parse_positional({"local-path"}); Opts.positional_help("local-path"); } void BuildsMultiTestDownloadSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { LogBanner(); TransferThreadWorkers Workers(m_Config.BoostWorkerCount, false); LogWorkersInfo(Workers); cxxopts::Options& Opts = SubOptions(); m_TestSystemRootDir = (GetRunningExecutablePath().parent_path() / ".tmpzensystem").make_preferred(); CreateDirectories(m_TestSystemRootDir); CleanDirectory(m_TestSystemRootDir, /*ForceRemoveReadOnlyFiles*/ true); auto SystemGuard = MakeGuard([this]() { DeleteDirectories(m_TestSystemRootDir); }); ParsePath(m_Path, Opts); std::string DummyBuildId; BuildStorageBase::Statistics StorageStats; BuildStorageCache::Statistics CacheStats; std::unique_ptr Auth; const ResolvedStorage Resolved = ParseStorageOptions(DummyBuildId, {}, Opts, m_TestSystemRootDir); // Place scratch next to the download target so the scratch area shares its volume. { const std::u8string PathStr = m_Path.generic_u8string(); const IoHash PathHash = IoHash::HashBuffer(PathStr.data(), PathStr.length()); SetZenFolderPath(m_Path.parent_path() / fmt::format("zen_{}", PathHash)); } StorageInstance Storage = CreateBuildStorage(GetZenFolderPath(), {}, Opts, StorageStats, CacheStats, Auth, Resolved); EnsureZenFolderExists(); auto ZenGuard = MakeGuard([this]() { CleanZenFolder(); }); EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(Opts); std::unique_ptr Progress = CreateProgress(); Stopwatch Timer; for (const std::string& BuildIdString : m_BuildIds) { Oid BuildId = Oid::FromHexString(RemoveQuotes(BuildIdString)); if (BuildId == Oid::Zero) { throw OptionParseException(fmt::format("'--build-id' ('{}') is malformed", BuildIdString), Opts.help()); } DownloadFolder(ConsoleLog(), *Progress, Workers, Storage, AbortFlag(), PauseFlag(), CacheStats, BuildId, /*BuildPartIds,*/ {}, /*BuildPartNames*/ {}, /*ManifestPath*/ {}, m_Path, DownloadOptions{.SystemRootDir = m_TestSystemRootDir, .ZenFolderPath = GetZenFolderPath(), .AllowMultiparts = m_Config.AllowMultiparts, .PartialBlockRequestMode = PartialBlockRequestMode, .CleanTargetFolder = BuildIdString == m_BuildIds.front(), .PostDownloadVerify = true, .EnableOtherDownloadsScavenging = m_EnableScavenging, .EnableTargetFolderScavenging = false, .AllowFileClone = m_AllowFileClone, .IsQuiet = m_Config.Quiet, .IsVerbose = m_Config.Verbose, .UseSparseFiles = m_Config.UseSparseFiles}); if (AbortFlag()) { throw std::runtime_error("Multitest aborted"); } if (!m_Config.Quiet) { ZEN_CONSOLE("\n"); } } if (!m_Config.Quiet) { ZEN_CONSOLE("Completed in {}", NiceTimeSpanMs(Timer.GetElapsedTimeMs())); } } } // namespace zen