// Copyright Epic Games, Inc. All Rights Reserved. #include "projectstore_cmd.h" #include "zenserviceclient.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 #include #include #include #include #include #include "consoleprogress.h" ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END #include #include namespace zen { namespace projectstore_impl { using namespace std::literals; void WriteAuthOptions(CbObjectWriter& Writer, std::string_view JupiterOpenIdProvider, std::string_view JupiterAccessToken, std::string_view JupiterAccessTokenEnv, std::string_view JupiterAccessTokenPath, std::string_view OidcTokenAuthExecutablePath, std::string_view HelpText) { if (!JupiterOpenIdProvider.empty()) { Writer.AddString("openid-provider"sv, JupiterOpenIdProvider); } if (!JupiterAccessToken.empty()) { Writer.AddString("access-token"sv, JupiterAccessToken); } if (!JupiterAccessTokenPath.empty()) { std::string ResolvedCloudAccessToken = ReadAccessTokenFromJsonFile(JupiterAccessTokenPath); if (!ResolvedCloudAccessToken.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessToken); } } if (!JupiterAccessTokenEnv.empty()) { std::string ResolvedCloudAccessTokenEnv = GetEnvVariable(JupiterAccessTokenEnv).value_or(""); if (!ResolvedCloudAccessTokenEnv.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessTokenEnv); } else { Writer.AddString("access-token-env"sv, JupiterAccessTokenEnv); } } if (std::filesystem::path OidcTokenExePath = FindOidcTokenExePath(OidcTokenAuthExecutablePath); !OidcTokenExePath.empty()) { Writer.AddString("oidc-exe-path"sv, OidcTokenExePath.generic_string()); } else if (!OidcTokenAuthExecutablePath.empty()) { throw OptionParseException(fmt::format("'--oidctoken-exe-path' ('{}') does not exist", OidcTokenAuthExecutablePath), std::string(HelpText)); } } IoBuffer MakeCbObjectPayload(std::function WriteCB) { CbObjectWriter Writer; WriteCB(Writer); IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer(); Payload.SetContentType(ZenContentType::kCbObject); return Payload; }; static std::atomic_uint32_t SignalCounter[NSIG] = {0}; static void SignalCallbackHandler(int SigNum) { if (SigNum >= 0 && SigNum < NSIG) { SignalCounter[SigNum].fetch_add(1); } } // `OplogMirrorCommand::Run` uses a latching boolean flag rather than the // SignalCounter above, because it drives a worker pool that aborts on any // interrupt. Kept separate from SignalCallbackHandler so neither interferes // with the other when both are installed in the same process. static std::atomic MirrorAbortFlag{false}; static void MirrorSignalCallbackHandler(int SigNum) { if (SigNum == SIGINT) { MirrorAbortFlag.store(true); } #if ZEN_PLATFORM_WINDOWS if (SigNum == SIGBREAK) { MirrorAbortFlag.store(true); } #endif } void ExecuteAsyncOperation(HttpClient& Http, std::string_view Url, IoBuffer&& Payload, bool PlainProgress) { signal(SIGINT, SignalCallbackHandler); #if ZEN_PLATFORM_WINDOWS signal(SIGBREAK, SignalCallbackHandler); #endif // ZEN_PLATFORM_WINDOWS if (HttpClient::Response Result = Http.Post(Url, Payload)) { if (Result.StatusCode == HttpResponseCode::Accepted) { bool Cancelled = false; std::string_view JobIdText = Result.AsText(); std::optional JobIdMaybe = ParseInt(JobIdText); if (!JobIdMaybe) { throw std::runtime_error(fmt::format("invalid job id returned, received '{}'", JobIdText)); } std::unique_ptr ProgressOwner( CreateConsoleProgress(PlainProgress ? ConsoleProgressMode::Plain : ConsoleProgressMode::Pretty)); std::unique_ptr Bar = ProgressOwner->CreateProgressBar(""sv); std::string ActiveTask; auto OutputMessages = [&](CbObjectView StatusObject) { CbArrayView Messages = StatusObject["Messages"sv].AsArrayView(); if (Messages.Num() > 0) { Bar->ForceLinebreak(); for (auto M : Messages) { std::string_view Message = M.AsString(); ZEN_CONSOLE("{}", Message); } } }; uint64_t JobId = JobIdMaybe.value(); while (true) { HttpClient::Response StatusResult = Http.Get(fmt::format("/admin/jobs/{}", JobId), HttpClient::Accept(ZenContentType::kCbObject)); if (!StatusResult) { StatusResult.ThrowError("failed to create project"sv); } CbObject StatusObject = StatusResult.AsObject(); std::string_view Status = StatusObject["Status"sv].AsString(); bool MessagesDone = false; if (Status == "Running") { std::string_view CurrentOp = StatusObject["Op"sv].AsString(); std::string_view CurrentOpDetails = StatusObject["Details"sv].AsString(); uint64_t TotalCount = StatusObject["TotalCount"sv].AsUInt64(); uint64_t RemainingCount = StatusObject["RemainingCount"sv].AsUInt64(); uint64_t ProgressElapsedTimeMs = StatusObject["ProgressElapsedTimeMs"sv].AsUInt64((uint64_t)-1); if (ActiveTask != CurrentOp) { Bar->Finish(); ActiveTask = ""; } if (ActiveTask.empty()) { OutputMessages(StatusObject); MessagesDone = true; ActiveTask = std::string(CurrentOp); } Bar->UpdateState({.Task = std::string(CurrentOp), .Details = std::string(CurrentOpDetails), .TotalCount = TotalCount, .RemainingCount = RemainingCount, .OptionalElapsedTime = ProgressElapsedTimeMs}, false); } if ((Status == "Complete") || (Status == "Aborted")) { Bar->Finish(); ActiveTask = ""; } if (!MessagesDone) { OutputMessages(StatusObject); } if (Status == "Complete") { if (Cancelled) { ZEN_CONSOLE("Cancelled"); } else { double QueueTimeS = StatusObject["QueueTimeS"].AsDouble(); double RuntimeS = StatusObject["RunTimeS"].AsDouble(); ZEN_CONSOLE("Completed: QueueTime: {}, RunTime: {}", NiceTimeSpanMs(static_cast(QueueTimeS * 1000.0)), NiceTimeSpanMs(static_cast(RuntimeS * 1000.0))); } break; } if (Status == "Aborted") { std::string_view AbortReason = StatusObject["AbortReason"].AsString(); int ReturnCode = StatusObject["ReturnCode"].AsInt32(-1); if (!AbortReason.empty()) { throw ErrorWithReturnCode(std::string(AbortReason), ReturnCode); } else { throw ErrorWithReturnCode("Aborted", ReturnCode); } break; } if (Status == "Queued") { double QueueTimeS = StatusObject["QueueTimeS"].AsDouble(); ZEN_CONSOLE("Queued, waited {}...", NiceTimeSpanMs(static_cast(QueueTimeS * 1000.0))); } uint32_t InterruptCounter = SignalCounter[SIGINT].load(); uint32_t BreakCounter = 0; #if ZEN_PLATFORM_WINDOWS BreakCounter = SignalCounter[SIGBREAK].load(); #endif // ZEN_PLATFORM_WINDOWS if (InterruptCounter > 0 || BreakCounter > 0) { SignalCounter[SIGINT].fetch_sub(InterruptCounter); #if ZEN_PLATFORM_WINDOWS SignalCounter[SIGBREAK].fetch_sub(BreakCounter); #endif // ZEN_PLATFORM_WINDOWS if (HttpClient::Response DeleteResult = Http.Delete(fmt::format("/admin/jobs/{}", JobId))) { Bar->ForceLinebreak(); ZEN_CONSOLE("Requested cancel..."); Cancelled = true; } else { Bar->ForceLinebreak(); ZEN_CONSOLE("Failed cancelling job {}", DeleteResult); } continue; } if (PlainProgress) { Sleep(5000); } else { Sleep(500); } } } else { ZEN_CONSOLE("{}", Result); } } else { Result.ThrowError("failed to start operation"sv); } } std::vector GetProjectIds(HttpClient& Http) { std::vector AvailableProjects; if (HttpClient::Response Result = Http.Get("/prj")) { CbObject CompactBinary = Result.AsObject(); for (CbFieldView Field : CompactBinary.AsFieldView().AsArrayView()) { if (auto Id = Field.AsObjectView()["Id"].AsString(); !Id.empty()) { AvailableProjects.push_back(std::string(Id)); } } } else { Result.ThrowError(fmt::format("failed to fetch available projects from '{}'", Http.GetBaseUri())); } return AvailableProjects; } std::vector GetOlogIds(HttpClient& Http, std::string_view ProjectId) { std::vector AvailableOplogs; if (HttpClient::Response Result = Http.Get(fmt::format("/prj/{}", ProjectId))) { CbObject CompactBinary = Result.AsObject(); for (CbFieldView Field : CompactBinary["oplogs"].AsArrayView()) { if (auto Id = Field.AsObjectView()["id"].AsString(); !Id.empty()) { AvailableOplogs.push_back(std::string(Id)); } } } else { Result.ThrowError(fmt::format("failed to fetch available oplogs from '{}' for project '{}'", Http.GetBaseUri(), ProjectId)); } return AvailableOplogs; } std::vector MatchId(const std::vector& Ids, std::string_view FindId) { bool FindHasDotDelimiter = FindId.find('.') != std::string_view::npos; std::vector PossibleIds; for (const std::string& Id : Ids) { if (Id == FindId) { return std::vector{Id}; } else if (Id.starts_with(FindId)) { if (FindHasDotDelimiter || Id[FindId.length()] == '.') { PossibleIds.push_back(Id); } } } return PossibleIds; } std::string FmtArray(const std::vector& Values, std::string_view Separator = ", "sv, std::string_view Quotes = "'"sv) { ExtendableStringBuilder<512> SB; for (const std::string& Value : Values) { if (SB.Size() > 0 && !Separator.empty()) { SB.Append(Separator); } if (!Quotes.empty()) { SB.Append(Quotes); } SB.Append(Value); if (!Quotes.empty()) { SB.Append(Quotes); } } return SB.ToString(); } std::string FmtProjectIdArray(const std::vector& ProjectIds, std::string_view Separator = ", "sv, std::string_view Quotes = "'"sv) { std::vector PrettyProjectIds; PrettyProjectIds.reserve(ProjectIds.size()); for (const std::string& ProjectId : ProjectIds) { auto PrettyProjectId = [Quotes, &ProjectIds](const std::string& ProjectId) -> std::string { if (std::string::size_type DotPos = ProjectId.find('.'); DotPos != std::string::npos) { const std::string ConflictProjectId = ProjectId.substr(0, DotPos + 1); auto It = std::find_if(ProjectIds.begin(), ProjectIds.end(), [&ProjectId, &ConflictProjectId](const std::string& CheckForConflictProjectId) { return (ProjectId != CheckForConflictProjectId) && CheckForConflictProjectId.starts_with(ConflictProjectId); }); if (It == ProjectIds.end()) { return fmt::format("{}{}{} ({})", Quotes, ProjectId.substr(0, DotPos), Quotes, ProjectId); } } return fmt::format("{}{}{}", Quotes, ProjectId, Quotes); }; PrettyProjectIds.push_back(PrettyProjectId(ProjectId)); } return FmtArray(PrettyProjectIds, Separator, ""); } std::string ResolveProject(HttpClient& Http, std::string_view OptionalProjectName) { std::vector AvailableProjects = GetProjectIds(Http); if (AvailableProjects.empty()) { if (OptionalProjectName.empty()) { ZEN_CONSOLE("No projects found at {}", Http.GetBaseUri()); } else { ZEN_CONSOLE_WARN("Unable to resolve project name '{}', no projects found at {}", OptionalProjectName, Http.GetBaseUri()); } return {}; } if (OptionalProjectName.empty()) { if (AvailableProjects.size() == 1) { return AvailableProjects.front(); } else { ZEN_CONSOLE("Available projects at {}: {}", Http.GetBaseUri(), FmtProjectIdArray(AvailableProjects)); return {}; } } std::vector MatchingProjectIds = MatchId(AvailableProjects, OptionalProjectName); if (MatchingProjectIds.empty()) { ZEN_CONSOLE_WARN("Unable to match project name '{}' at {}, available projects: {}", OptionalProjectName, Http.GetBaseUri(), FmtProjectIdArray(AvailableProjects)); return {}; } if (MatchingProjectIds.size() == 1) { return MatchingProjectIds.front(); } ZEN_CONSOLE_WARN("Project name is ambigous '{}' at {}: possible matches: {}", OptionalProjectName, Http.GetBaseUri(), FmtProjectIdArray(MatchingProjectIds)); return {}; } std::string ResolveOplog(HttpClient& Http, std::string_view ProjectName, std::string_view OptionalOplogName) { std::vector AvailableOplogs = GetOlogIds(Http, ProjectName); if (AvailableOplogs.empty()) { if (OptionalOplogName.empty()) { ZEN_CONSOLE("No oplogs for project '{}' found at {}", ProjectName, Http.GetBaseUri()); } else { ZEN_CONSOLE_WARN("Unable to resolve oplog name '{}' for project '{}', no oplogs found at {}", OptionalOplogName, ProjectName, Http.GetBaseUri()); } return {}; } if (OptionalOplogName.empty()) { if (AvailableOplogs.size() == 1) { return AvailableOplogs.front(); } else { ZEN_CONSOLE("Available oplogs for project '{}' at {}: {}", ProjectName, Http.GetBaseUri(), FmtArray(AvailableOplogs)); return {}; } } std::vector MatchingOplogIds = MatchId(AvailableOplogs, OptionalOplogName); if (MatchingOplogIds.empty()) { ZEN_CONSOLE_WARN("Unable to match oplog name '{}' for project '{}', available oplogs at {}: {}", OptionalOplogName, ProjectName, Http.GetBaseUri(), FmtArray(AvailableOplogs)); return {}; } if (MatchingOplogIds.size() == 1) { return MatchingOplogIds.front(); } ZEN_CONSOLE_WARN("Oplog name '{}' for project '{}' at {}, is ambigous, possible matches: {}", OptionalOplogName, ProjectName, Http.GetBaseUri(), FmtArray(MatchingOplogIds)); return {}; } } // namespace projectstore_impl //////////////////////////////////////////////////////////////////////////////// // OplogCommand OplogCommand::OplogCommand() { m_Options.add_options()("h,help", "Print help"); AddSubCommand(m_CreateSubCmd); AddSubCommand(m_DownloadSubCmd); AddSubCommand(m_ExportSubCmd); AddSubCommand(m_ImportSubCmd); AddSubCommand(m_MirrorSubCmd); AddSubCommand(m_SnapshotSubCmd); AddSubCommand(m_ValidateSubCmd); } OplogCommand::~OplogCommand() = default; //////////////////////////////////////////////////////////////////////////////// // OplogSubCmdBase OplogSubCmdBase::OplogSubCmdBase(std::string_view Name, std::string_view Description) : ZenSubCmdBase(Name, Description) { m_SubOptions.add_option("", "u", "hosturl", ZenCmdBase::kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); } //////////////////////////////////////////////////////////////////////////////// // Legacy shim dispatcher namespace oplog_legacy_shim { void RunAs(const char* SubCommandName, const ZenCliOptions& GlobalOptions, int argc, char** argv) { // cxxopts treats argv as writable char**; stage the injected subcommand // token in writable string storage so we never hand out a pointer to a // string literal. std::string SubCmdStorage(SubCommandName); std::vector NewArgv; NewArgv.reserve(static_cast(argc) + 1); NewArgv.push_back(argv[0]); NewArgv.push_back(SubCmdStorage.data()); for (int i = 1; i < argc; ++i) { NewArgv.push_back(argv[i]); } OplogCommand Impl; Impl.Run(GlobalOptions, static_cast(NewArgv.size()), NewArgv.data()); } } // namespace oplog_legacy_shim /////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // ProjectCommand ProjectCommand::ProjectCommand() { m_Options.add_options()("h,help", "Print help"); AddSubCommand(m_CreateSubCmd); AddSubCommand(m_DropSubCmd); AddSubCommand(m_InfoSubCmd); AddSubCommand(m_OpDetailsSubCmd); AddSubCommand(m_StatsSubCmd); } ProjectCommand::~ProjectCommand() = default; //////////////////////////////////////////////////////////////////////////////// // ProjectSubCmdBase ProjectSubCmdBase::ProjectSubCmdBase(std::string_view Name, std::string_view Description) : ZenSubCmdBase(Name, Description) { m_SubOptions.add_option("", "u", "hosturl", ZenCmdBase::kHostUrlHelp, cxxopts::value(m_HostName)->default_value(""), ""); } //////////////////////////////////////////////////////////////////////////////// // Legacy shim dispatcher namespace project_legacy_shim { void RunAs(const char* SubCommandName, const ZenCliOptions& GlobalOptions, int argc, char** argv) { // cxxopts treats argv as writable char** in the style of C main(argv). // Stage the injected token in writable std::string storage so we never // hand out pointers to string literals. std::string Token(SubCommandName); std::vector NewArgv; NewArgv.reserve(static_cast(argc) + 1); NewArgv.push_back(argv[0]); NewArgv.push_back(Token.data()); for (int i = 1; i < argc; ++i) { NewArgv.push_back(argv[i]); } ProjectCommand Impl; Impl.Run(GlobalOptions, static_cast(NewArgv.size()), NewArgv.data()); } } // namespace project_legacy_shim //////////////////////////////////////////////////////////////////////////////// // ProjectDropSubCmd ProjectDropSubCmd::ProjectDropSubCmd() : ProjectSubCmdBase("drop", "Drop project or project oplog") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_SubOptions.add_option("", "", "dryrun", "Dry run - resolve arguments but do not drop", cxxopts::value(m_DryRun), ""); m_SubOptions.parse_positional({"project", "oplog"}); m_SubOptions.positional_help("[ []]"); } void ProjectDropSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "drop"}); HttpClient& Http = Service.Http(); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error(fmt::format("Can't find project '{}'", m_ProjectName)); } if (m_OplogName.empty()) { if (m_DryRun) { ZEN_CONSOLE("Would drop project '{}' from '{}'. Use --dryrun=false to execute the drop operation.", m_ProjectName, Service.HostSpec()); } else { ZEN_CONSOLE("Dropping project '{}' from '{}'", m_ProjectName, Service.HostSpec()); if (HttpClient::Response Result = Http.Delete(fmt::format("/prj/{}", m_ProjectName))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("delete project failed"sv); } } } else { m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw zen::runtime_error("Can't find oplog '{}' in project '{}'", m_OplogName, m_ProjectName); } if (m_DryRun) { ZEN_CONSOLE("Would drop oplog '{}/{}' from '{}'. Add --dryrun=false to execute the drop operation.", m_ProjectName, m_OplogName, Service.HostSpec()); } else { ZEN_CONSOLE("Dropping oplog '{}/{}' from '{}'", m_ProjectName, m_OplogName, Service.HostSpec()); if (HttpClient::Response Result = Http.Delete(fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("delete oplog failed"sv); } } } } //////////////////////////////////////////////////////////////////////////////// // ProjectInfoSubCmd ProjectInfoSubCmd::ProjectInfoSubCmd() : ProjectSubCmdBase("info", "Info on project or project oplog") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_SubOptions.parse_positional({"project", "oplog"}); m_SubOptions.positional_help("[ []]"); } void ProjectInfoSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; if (!m_OplogName.empty() && m_ProjectName.empty()) { throw OptionParseException("'--project' is required", m_SubOptions.help()); } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "info"}); HttpClient& Http = Service.Http(); std::string Url; if (m_ProjectName.empty()) { Url = "/prj"; ZEN_CONSOLE("Info from '{}'", Url); } else if (m_OplogName.empty()) { m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Unable to resolve project"); } Url = fmt::format("/prj/{}", m_ProjectName); ZEN_CONSOLE("Info on project '{}' from '{}{}'", m_ProjectName, Service.HostSpec(), Url); } else { m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Unable to resolve project"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw std::runtime_error("Unable to resolve oplog"); } Url = fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName); ZEN_CONSOLE("Info on oplog '{}/{}' from '{}{}'", m_ProjectName, m_OplogName, Service.HostSpec(), Url); } if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed to fetch info"sv); } } //////////////////////////////////////////////////////////////////////////////// // ProjectCreateSubCmd ProjectCreateSubCmd::ProjectCreateSubCmd() : ProjectSubCmdBase("create", "Create a project") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectId), ""); m_SubOptions.add_option("", "", "rootdir", "Absolute path to root directory", cxxopts::value(m_RootDir), ""); m_SubOptions.add_option("", "", "enginedir", "Absolute path to engine root directory", cxxopts::value(m_EngineRootDir), ""); m_SubOptions.add_option("", "", "projectdir", "Absolute path to project directory", cxxopts::value(m_ProjectRootDir), ""); m_SubOptions.add_option("", "", "projectfile", "Absolute path to .uproject file", cxxopts::value(m_ProjectFile), ""); m_SubOptions.add_option("", "f", "force-update", "Force update of existing project", cxxopts::value(m_ForceUpdate), ""); m_SubOptions.parse_positional({"project", "rootdir", "enginedir", "projectdir", "projectfile"}); } void ProjectCreateSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; using namespace std::literals; if (m_ProjectId.empty()) { throw OptionParseException("'--project' is required", m_SubOptions.help()); } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "create"}); HttpClient& Http = Service.Http(); std::string Url = fmt::format("/prj/{}", m_ProjectId); if (!m_ForceUpdate) { if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { throw std::runtime_error(fmt::format("Project already exists.\n{}", Result.ToText())); } } IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("id"sv, m_ProjectId); Writer.AddString("root"sv, m_RootDir); Writer.AddString("engine"sv, m_EngineRootDir); Writer.AddString("project"sv, m_ProjectRootDir); Writer.AddString("projectfile"sv, m_ProjectFile); }); if (HttpClient::Response Result = m_ForceUpdate ? Http.Put(Url, Payload, HttpClient::Accept(ZenContentType::kText)) : Http.Post(Url, Payload, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("failed to create project"sv); } } /////////////////////////////////////// OplogCreateSubCmd::OplogCreateSubCmd() : OplogSubCmdBase("create", "Create a project oplog") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectId), ""); m_SubOptions.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogId), ""); m_SubOptions.add_option("", "", "gcpath", "Absolute path to oplog lifetime marker file", cxxopts::value(m_GcPath), ""); m_SubOptions.add_option("", "f", "force-update", "Force update of existing oplog", cxxopts::value(m_ForceUpdate), ""); m_SubOptions.parse_positional({"project", "oplog", "gcpath"}); } void OplogCreateSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; using namespace std::literals; if (m_ProjectId.empty()) { throw OptionParseException("'--project' is required", m_SubOptions.help()); } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "oplog.create"}); HttpClient& Http = Service.Http(); m_ProjectId = ResolveProject(Http, m_ProjectId); if (m_ProjectId.empty()) { throw std::runtime_error("Project can not be found"); } if (m_OplogId.empty()) { throw OptionParseException("'--oplog' is required", m_SubOptions.help()); } std::string Url = fmt::format("/prj/{}/oplog/{}", m_ProjectId, m_OplogId); if (!m_ForceUpdate) { if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { throw std::runtime_error(fmt::format("Oplog already exists.\n{}", Result.ToText())); } } IoBuffer OplogPayload; OplogPayload.SetContentType(ZenContentType::kCbObject); if (!m_GcPath.empty()) { OplogPayload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("gcpath"sv, m_GcPath); }); } if (HttpClient::Response Result = m_ForceUpdate ? Http.Put(Url, OplogPayload, HttpClient::Accept(ZenContentType::kText)) : Http.Post(Url, OplogPayload, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("failed to create oplog"sv); } } /////////////////////////////////////// OplogExportSubCmd::OplogExportSubCmd() : OplogSubCmdBase("export", "Export project store oplog") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_SubOptions.add_option("", "", "maxblocksize", "Max size for bundled attachments", cxxopts::value(m_MaxBlockSize), ""); m_SubOptions.add_option("", "", "maxchunksperblock", "Max number of chunks in one block", cxxopts::value(m_MaxChunksPerBlock), ""); m_SubOptions.add_option("", "", "maxchunkembedsize", "Max size for attachment to be bundled", cxxopts::value(m_MaxChunkEmbedSize), ""); m_SubOptions.add_option("", "", "embedloosefiles", "Export additional files referenced by path as attachments", cxxopts::value(m_EmbedLooseFiles), ""); m_SubOptions.add_option("", "f", "force", "Force export of all attachments", cxxopts::value(m_Force), ""); m_SubOptions.add_option("", "", "ignore-missing-attachments", "Continue exporting oplog even if attachments are missing", cxxopts::value(m_IgnoreMissingAttachments), ""); m_SubOptions.add_option("", "", "disableblocks", "Disable block creation and save all attachments individually (applies to file and cloud target)", cxxopts::value(m_DisableBlocks), ""); m_SubOptions.add_option("", "a", "async", "Trigger export but don't wait for completion", cxxopts::value(m_Async), ""); m_SubOptions.add_option("", "", "namespace", "Cloud/Builds Storage namespace", cxxopts::value(m_JupiterNamespace), ""); m_SubOptions.add_option("", "", "bucket", "Cloud/Builds Storage bucket", cxxopts::value(m_JupiterBucket), ""); m_SubOptions.add_option("", "", "openid-provider", "Cloud/Builds Storage openid provider", cxxopts::value(m_JupiterOpenIdProvider), ""); m_SubOptions .add_option("", "", "access-token", "Cloud/Builds Storage access token", cxxopts::value(m_JupiterAccessToken), ""); m_SubOptions.add_option("", "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), ""); m_SubOptions.add_option("", "", "access-token-path", "Path to json file that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenPath), ""); m_SubOptions.add_option("", "", "oidctoken-exe-path", "Path to OidcToken executable", cxxopts::value(m_OidcTokenAuthExecutablePath)->default_value(""), ""); m_SubOptions.add_option("", "", "assume-http2", "Assume that the cloud/builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", cxxopts::value(m_JupiterAssumeHttp2), ""); m_SubOptions.add_option( "", "", "disabletempblocks", "Disable temp block creation and upload blocks without waiting for oplog container to be uploaded for cloud/builds storage", cxxopts::value(m_JupiterDisableTempBlocks), ""); m_SubOptions.add_option("", "", "cloud", "Cloud Storage URL", cxxopts::value(m_CloudUrl), ""); m_SubOptions.add_option("cloud", "", "key", "Cloud Storage key", cxxopts::value(m_CloudKey), ""); m_SubOptions.add_option("cloud", "", "basekey", "Optional Base Cloud Storage key for incremental export", cxxopts::value(m_BaseCloudKey), ""); m_SubOptions.add_option("", "", "builds", "Builds Storage API URL", cxxopts::value(m_BuildsUrl), ""); m_SubOptions.add_option("builds", "", "builds-id", "Builds Id", cxxopts::value(m_BuildsId), ""); m_SubOptions.add_option("builds", "", "builds-metadata-path", "Path to json file that holds the metadata for the build", cxxopts::value(m_BuildsMetadataPath), ""); m_SubOptions.add_option("builds", "", "builds-metadata", "Key-value pairs separated by ';' with build meta data. (key1=value1;key2=value2)", cxxopts::value(m_BuildsMetadata), ""); m_SubOptions.add_option("", "", "zen", "Zen service upload address", cxxopts::value(m_ZenUrl), ""); m_SubOptions.add_option("zen", "", "target-project", "Zen target project name", cxxopts::value(m_ZenProjectName), ""); m_SubOptions.add_option("zen", "", "target-oplog", "Zen target oplog name", cxxopts::value(m_ZenOplogName), ""); m_SubOptions.add_option("zen", "", "clean", "Delete existing target Zen oplog", cxxopts::value(m_ZenClean), ""); m_SubOptions.add_option("", "", "file", "Local folder path", cxxopts::value(m_FileDirectoryPath), ""); m_SubOptions.add_option("file", "", "name", "Local file name", cxxopts::value(m_FileName), ""); m_SubOptions.add_option("file", "", "basename", "Local base file name for incremental oplog export", cxxopts::value(m_BaseFileName), ""); m_SubOptions.add_option("file", "", "forcetempblocks", "Force creation of temp attachment blocks", cxxopts::value(m_FileForceEnableTempBlocks), ""); m_SubOptions .add_option("", "", "plainprogress", "Use (legacy) plain progress update", cxxopts::value(m_PlainProgress), ""); m_SubOptions.add_option("", "", "boost-worker-count", "Increase the number of worker threads - may cause computer to be less responsive", cxxopts::value(m_BoostWorkerCount), ""); m_SubOptions.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(m_BoostWorkerMemory), ""); m_SubOptions.add_option("", "", "boost-workers", "Enables both 'boost-worker-count' and 'boost-worker-memory' - may cause computer to be less responsive", cxxopts::value(m_BoostWorkers), ""); m_SubOptions.parse_positional({"project", "oplog"}); m_SubOptions.positional_help("[ ]"); } void OplogExportSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; using namespace std::literals; if (m_BoostWorkers) { m_BoostWorkerCount = true; m_BoostWorkerMemory = true; } if (m_ProjectName.empty()) { throw OptionParseException("'--project' is required", m_SubOptions.help()); } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "oplog.export"}); HttpClient& Http = Service.Http(); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw std::runtime_error("Oplog can not be found"); } size_t TargetCount = 0; TargetCount += m_CloudUrl.empty() ? 0 : 1; TargetCount += m_BuildsUrl.empty() ? 0 : 1; TargetCount += m_ZenUrl.empty() ? 0 : 1; TargetCount += m_FileDirectoryPath.empty() ? 0 : 1; if (TargetCount != 1) { if (TargetCount == 0) { throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' is required", m_SubOptions.help()); } else { throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' are conflicting", m_SubOptions.help()); } } if (!m_CloudUrl.empty()) { if (m_JupiterNamespace.empty()) { throw OptionParseException("'--namespace' is required", m_SubOptions.help()); } if (m_JupiterBucket.empty()) { throw OptionParseException("'--bucket' is required", m_SubOptions.help()); } if (m_CloudKey.empty()) { std::string KeyString = fmt::format("{}/{}/{}/{}", m_ProjectName, m_OplogName, m_JupiterNamespace, m_JupiterBucket); IoHash Key = IoHash::HashBuffer(KeyString.data(), KeyString.size()); m_CloudKey = Key.ToHexString(); ZEN_WARN("Using auto generated cloud key '{}'", m_CloudKey); } } if (!m_BuildsUrl.empty()) { if (m_JupiterNamespace.empty()) { throw OptionParseException("'--namespace' is required", m_SubOptions.help()); } if (m_JupiterNamespace.empty() || m_JupiterBucket.empty()) { throw OptionParseException("'--bucket' is required", m_SubOptions.help()); } if (m_BuildsMetadataPath.empty() && m_BuildsMetadata.empty()) { throw OptionParseException("'--metadata' or --'metadata-path' is required", m_SubOptions.help()); } if (!m_BuildsMetadataPath.empty() && !m_BuildsMetadata.empty()) { throw OptionParseException( fmt::format("'--metadata' ('{}') conflicts with --'metadata-path' ('{}')", m_BuildsMetadata, m_BuildsMetadataPath), m_SubOptions.help()); } if (m_BuildsId.empty()) { m_BuildsId = Oid::NewOid().ToString(); ZEN_CONSOLE("Using generated builds id: {}", m_BuildsId); } } if (!m_ZenUrl.empty()) { if (m_ZenProjectName.empty()) { m_ZenProjectName = m_ProjectName; ZEN_CONSOLE_WARN("Using default zen target project id '{}'", m_ZenProjectName); } if (m_ZenOplogName.empty()) { m_ZenOplogName = m_OplogName; ZEN_CONSOLE_WARN("Using default zen target oplog id '{}'", m_ZenOplogName); } std::string TargetUrlBase = m_ZenUrl; if (TargetUrlBase.find("://") == std::string::npos) { // Assume http URL TargetUrlBase = fmt::format("http://{}", TargetUrlBase); } HttpClient TargetHttp(TargetUrlBase); std::string Url = fmt::format("/prj/{}/oplog/{}", m_ZenProjectName, m_ZenOplogName); bool CreateOplog = false; if (HttpClient::Response Result = TargetHttp.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { if (m_ZenClean) { ZEN_CONSOLE_WARN("Deleting zen remote oplog '{}/{}'", m_ZenProjectName, m_ZenOplogName); Result = TargetHttp.Delete(Url, HttpClient::Accept(ZenContentType::kJSON)); if (!Result) { Result.ThrowError("failed deleting existing zen remote oplog"sv); } CreateOplog = true; } } else if (Result.StatusCode == HttpResponseCode::NotFound) { CreateOplog = true; } else { Result.ThrowError("failed checking zen remote oplog"sv); } if (CreateOplog) { ZEN_CONSOLE_WARN("Creating zen remote oplog '{}/{}'", m_ZenProjectName, m_ZenOplogName); if (HttpClient::Response Result = TargetHttp.Post(Url, IoBuffer(), ZenContentType::kCbObject); !Result) { Result.ThrowError("failed creating zen remote oplog"sv); } } } if (!m_FileDirectoryPath.empty()) { if (m_FileName.empty()) { m_FileName = m_OplogName; ZEN_CONSOLE_WARN("Using default file name '{}'", m_FileName); } } std::string TargetDescription; IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "export"sv); Writer.BeginObject("params"sv); { if (m_MaxBlockSize != 0) { Writer.AddInteger("maxblocksize"sv, m_MaxBlockSize); } if (m_MaxChunksPerBlock != 0) { Writer.AddInteger("maxchunksperblock"sv, m_MaxChunksPerBlock); } if (m_MaxChunkEmbedSize != 0) { Writer.AddInteger("maxchunkembedsize"sv, m_MaxChunkEmbedSize); } if (m_EmbedLooseFiles) { Writer.AddBool("embedloosefiles"sv, true); } if (m_Force) { Writer.AddBool("force"sv, true); } if (m_IgnoreMissingAttachments) { Writer.AddBool("ignoremissingattachments"sv, true); } if (m_BoostWorkerCount) { Writer.AddBool("boostworkercount"sv, true); } if (m_BoostWorkerMemory) { Writer.AddBool("boostworkermemory"sv, true); } Writer.AddBool("async"sv, true); if (!m_FileDirectoryPath.empty()) { Writer.BeginObject("file"sv); { Writer.AddString("path"sv, m_FileDirectoryPath); Writer.AddString("name"sv, m_FileName); if (!m_BaseFileName.empty()) { Writer.AddString("basename"sv, m_BaseFileName); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_FileForceEnableTempBlocks) { Writer.AddBool("enabletempblocks"sv, true); } } Writer.EndObject(); // "file" TargetDescription = fmt::format("[file] {}/{}{}{}", m_FileDirectoryPath, m_FileName, m_BaseFileName.empty() ? "" : " Base: ", m_BaseFileName); } if (!m_CloudUrl.empty()) { Writer.BeginObject("cloud"sv); { Writer.AddString("url"sv, m_CloudUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("key"sv, m_CloudKey); if (!m_BaseCloudKey.empty()) { Writer.AddString("basekey"sv, m_BaseCloudKey); } WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath, m_SubOptions.help()); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_JupiterDisableTempBlocks) { Writer.AddBool("disabletempblocks"sv, true); } } Writer.EndObject(); // "cloud" TargetDescription = fmt::format("[cloud] {}/{}/{}/{}{}{}", m_CloudUrl, m_JupiterNamespace, m_JupiterBucket, m_CloudKey, m_BaseCloudKey.empty() ? "" : " Base: ", m_BaseCloudKey); } if (!m_BuildsUrl.empty()) { Writer.BeginObject("builds"sv); { Writer.AddString("url"sv, m_BuildsUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("buildsid"sv, m_BuildsId); WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath, m_SubOptions.help()); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } if (m_DisableBlocks) { Writer.AddBool("disableblocks"sv, true); } if (m_JupiterDisableTempBlocks) { Writer.AddBool("disabletempblocks"sv, true); } if (!m_BuildsMetadataPath.empty()) { std::filesystem::path MetadataPath(m_BuildsMetadataPath); IoBuffer MetaDataJson = ReadFile(MetadataPath).Flatten(); std::string_view Json(reinterpret_cast(MetaDataJson.GetData()), MetaDataJson.GetSize()); std::string JsonError; CbFieldIterator MetaData = LoadCompactBinaryFromJson(Json, JsonError); if (!JsonError.empty()) { throw zen::runtime_error("builds metadata file '{}' is malformed. Reason: '{}'", MetadataPath.string(), JsonError); } Writer.AddBinary("metadata"sv, MetaData.GetBuffer()); } if (!m_BuildsMetadata.empty()) { CbObjectWriter MetaDataWriter(m_BuildsMetadata.length()); ForEachStrTok(m_BuildsMetadata, ';', [&](std::string_view Pair) { size_t SplitPos = Pair.find('='); if (SplitPos == std::string::npos || SplitPos == 0) { throw zen::runtime_error("builds metadata key-value pair '{}' is malformed", Pair); } MetaDataWriter.AddString(Pair.substr(0, SplitPos), Pair.substr(SplitPos + 1)); return true; }); CbObject MetaData = MetaDataWriter.Save(); Writer.AddBinary("metadata"sv, MetaData.GetBuffer()); } } Writer.EndObject(); // "builds" TargetDescription = fmt::format("[builds] {}/{}/{}/{}", m_BuildsUrl, m_JupiterNamespace, m_JupiterBucket, m_BuildsId); } if (!m_ZenUrl.empty()) { Writer.BeginObject("zen"sv); { Writer.AddString("url"sv, m_ZenUrl); Writer.AddString("project"sv, m_ZenProjectName); Writer.AddString("oplog"sv, m_ZenOplogName); } Writer.EndObject(); // "zen" TargetDescription = fmt::format("[zen] {}/{}/{}", m_ZenUrl, m_ZenProjectName, m_ZenOplogName); } } Writer.EndObject(); // "params" }); ZEN_CONSOLE("Saving oplog '{}/{}' from '{}' to {}", m_ProjectName, m_OplogName, m_HostName, TargetDescription); if (m_Async) { if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), HttpClient::Accept(ZenContentType::kJSON)); Result) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed requesting loading oplog export"sv); } } else { ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), m_PlainProgress); } } //////////////////////////// OplogImportSubCmd::OplogImportSubCmd() : OplogSubCmdBase("import", "Import project store oplog") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_SubOptions.add_option("", "", "gcpath", "Absolute path to oplog lifetime marker file if we create the oplog", cxxopts::value(m_GcPath), ""); m_SubOptions.add_option("", "f", "force", "Force import of all attachments", cxxopts::value(m_Force), ""); m_SubOptions.add_option("", "a", "async", "Trigger import but don't wait for completion", cxxopts::value(m_Async), ""); m_SubOptions.add_option("", "", "clean", "Delete existing target oplog", cxxopts::value(m_Clean), ""); m_SubOptions.add_option("", "", "ignore-missing-attachments", "Continue importing oplog even if attachments are missing", cxxopts::value(m_IgnoreMissingAttachments), ""); m_SubOptions.add_option("", "", "namespace", "Cloud/Builds Storage namespace", cxxopts::value(m_JupiterNamespace), ""); m_SubOptions.add_option("", "", "bucket", "Cloud/Builds Storage bucket", cxxopts::value(m_JupiterBucket), ""); m_SubOptions.add_option("", "", "openid-provider", "Cloud/Builds Storage openid provider", cxxopts::value(m_JupiterOpenIdProvider), ""); m_SubOptions .add_option("", "", "access-token", "Cloud/Builds Storage access token", cxxopts::value(m_JupiterAccessToken), ""); m_SubOptions.add_option("", "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), ""); m_SubOptions.add_option("", "", "access-token-path", "Path to json file that holds the cloud/builds Storage access token", cxxopts::value(m_JupiterAccessTokenPath), ""); m_SubOptions.add_option("", "", "oidctoken-exe-path", "Path to OidcToken executable", cxxopts::value(m_OidcTokenAuthExecutablePath)->default_value(""), ""); m_SubOptions.add_option("", "", "assume-http2", "Assume that the cloud/builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", cxxopts::value(m_JupiterAssumeHttp2), ""); m_SubOptions.add_option("", "", "cloud", "Cloud Storage URL", cxxopts::value(m_CloudUrl), ""); m_SubOptions.add_option("cloud", "", "key", "Cloud Storage key", cxxopts::value(m_CloudKey), ""); m_SubOptions.add_option("", "", "builds", "Builds Storage URL", cxxopts::value(m_BuildsHost), ""); m_SubOptions.add_option("", "", "builds-override-host", "Builds Storage override API host", cxxopts::value(m_BuildsOverrideHost), ""); m_SubOptions .add_option("builds", "", "zen-cache-host", "Host ip and port for zen builds cache", cxxopts::value(m_ZenCacheHost), ""); m_SubOptions.add_option("builds", "", "zen-cache-upload", "Upload data downloaded from remote host to zen cache", cxxopts::value(m_UploadToZenCache), ""); m_SubOptions.add_option("builds", "", "builds-id", "Builds Id", cxxopts::value(m_BuildsId), ""); m_SubOptions.add_option("", "", "zen", "Zen service upload address", cxxopts::value(m_ZenUrl), ""); m_SubOptions.add_option("zen", "", "source-project", "Zen source project name", cxxopts::value(m_ZenProjectName), ""); m_SubOptions.add_option("zen", "", "source-oplog", "Zen source oplog name", cxxopts::value(m_ZenOplogName), ""); m_SubOptions.add_option("", "", "file", "Local folder path", cxxopts::value(m_FileDirectoryPath), ""); m_SubOptions.add_option("file", "", "name", "Local file name", cxxopts::value(m_FileName), ""); m_SubOptions .add_option("", "", "plainprogress", "Use (legacy) plain progress update", cxxopts::value(m_PlainProgress), ""); m_SubOptions.add_option("", "", "boost-worker-count", "Increase the number of worker threads - may cause computer to be less responsive", cxxopts::value(m_BoostWorkerCount), ""); m_SubOptions.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(m_BoostWorkerMemory), ""); m_SubOptions.add_option("", "", "boost-workers", "Enables both 'boost-worker-count' and 'boost-worker-memory' - may cause computer to be less responsive", cxxopts::value(m_BoostWorkers), ""); m_SubOptions.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), ""); m_SubOptions.parse_positional({"project", "oplog", "gcpath"}); m_SubOptions.positional_help("[ []]"); } void OplogImportSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; using namespace std::literals; if (m_BoostWorkers) { m_BoostWorkerCount = true; m_BoostWorkerMemory = true; } if (m_ProjectName.empty()) { throw OptionParseException("'--project' is required", m_SubOptions.help()); } if (m_OplogName.empty()) { throw OptionParseException("'--oplog' is required", m_SubOptions.help()); } EPartialBlockRequestMode Mode = PartialBlockRequestModeFromString(m_AllowPartialBlockRequests); if (Mode == EPartialBlockRequestMode::Invalid) { throw OptionParseException(fmt::format("'--allow-partial-block-requests' ('{}') is invalid", m_AllowPartialBlockRequests), m_SubOptions.help()); } ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "oplog.import"}); HttpClient& Http = Service.Http(); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Project can not be found"); } size_t TargetCount = 0; TargetCount += m_CloudUrl.empty() ? 0 : 1; TargetCount += (m_BuildsHost.empty() && m_BuildsOverrideHost.empty()) ? 0 : 1; TargetCount += m_ZenUrl.empty() ? 0 : 1; TargetCount += m_FileDirectoryPath.empty() ? 0 : 1; if (TargetCount == 0) { throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' is required", m_SubOptions.help()); } else if (TargetCount > 1) { throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' are conflicting", m_SubOptions.help()); } if (!m_CloudUrl.empty()) { if (m_JupiterNamespace.empty()) { throw OptionParseException("--'namespace' is required", m_SubOptions.help()); } if (m_JupiterBucket.empty()) { throw OptionParseException("--'bucket' is required", m_SubOptions.help()); } if (m_CloudKey.empty()) { std::string KeyString = fmt::format("{}/{}/{}/{}", m_ProjectName, m_OplogName, m_JupiterNamespace, m_JupiterBucket); IoHash Key = IoHash::HashBuffer(KeyString.data(), KeyString.size()); m_CloudKey = Key.ToHexString(); ZEN_CONSOLE_WARN("Using auto generated cloud key '{}'", m_CloudKey); } } if (!m_BuildsHost.empty() || !m_BuildsOverrideHost.empty()) { if (m_JupiterNamespace.empty()) { throw OptionParseException("'--namespace' is required", m_SubOptions.help()); } if (m_JupiterBucket.empty()) { throw OptionParseException("'--bucket' is required", m_SubOptions.help()); } if (m_BuildsId.empty()) { throw OptionParseException("'--build-id' is required", m_SubOptions.help()); } } if (!m_ZenUrl.empty()) { if (m_ZenProjectName.empty()) { m_ZenProjectName = m_ProjectName; ZEN_CONSOLE_WARN("Using default zen target project id '{}'", m_ZenProjectName); } if (m_ZenOplogName.empty()) { m_ZenOplogName = m_OplogName; ZEN_CONSOLE_WARN("Using default zen target oplog id '{}'", m_ZenOplogName); } } if (!m_FileDirectoryPath.empty()) { if (m_FileName.empty()) { m_FileName = m_OplogName; ZEN_CONSOLE_WARN("Using auto generated file name '{}'", m_FileName); } } std::string Url = fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName); bool CreateOplog = false; if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON)); Result.StatusCode == HttpResponseCode::NotFound) { CreateOplog = true; } else if (!IsHttpSuccessCode(Result.StatusCode)) { Result.ThrowError("failed checking oplog"sv); } if (CreateOplog) { IoBuffer OplogPayload; OplogPayload.SetContentType(ZenContentType::kCbObject); if (!m_GcPath.empty()) { OplogPayload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("gcpath"sv, m_GcPath); }); } ZEN_CONSOLE("Creating oplog '{}/{}'", m_ProjectName, m_OplogName); if (HttpClient::Response Result = Http.Post(Url, OplogPayload); !Result) { Result.ThrowError("failed creating oplog"sv); } } std::string SourceDescription; IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "import"sv); Writer.BeginObject("params"sv); { if (m_Force) { Writer.AddBool("force"sv, true); } if (m_IgnoreMissingAttachments) { Writer.AddBool("ignoremissingattachments"sv, true); } if (m_Clean) { Writer.AddBool("clean"sv, true); } if (m_Force) { Writer.AddBool("force"sv, true); } if (m_BoostWorkerCount) { Writer.AddBool("boostworkercount"sv, true); } if (m_BoostWorkerMemory) { Writer.AddBool("boostworkermemory"sv, true); } Writer.AddString("partialblockrequestmode", m_AllowPartialBlockRequests); if (!m_FileDirectoryPath.empty()) { Writer.BeginObject("file"sv); { Writer.AddString("path"sv, m_FileDirectoryPath); Writer.AddString("name"sv, m_FileName); } Writer.EndObject(); // "file" SourceDescription = fmt::format("[file] {}/{}", m_FileDirectoryPath, m_FileName); } if (!m_CloudUrl.empty()) { Writer.BeginObject("cloud"sv); { Writer.AddString("url"sv, m_CloudUrl); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("key"sv, m_CloudKey); WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath, m_SubOptions.help()); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } } Writer.EndObject(); // "cloud" SourceDescription = fmt::format("[cloud] {}/{}/{}/{}", m_CloudUrl, m_JupiterNamespace, m_JupiterBucket, m_CloudKey); } if (!m_BuildsHost.empty() || !m_BuildsOverrideHost.empty()) { Writer.BeginObject("builds"sv); { Writer.AddString("url"sv, m_BuildsHost); Writer.AddString("override-host"sv, m_BuildsOverrideHost); Writer.AddString("zencachehost"sv, m_ZenCacheHost); Writer.AddString("namespace"sv, m_JupiterNamespace); Writer.AddString("bucket"sv, m_JupiterBucket); Writer.AddString("buildsid"sv, m_BuildsId); Writer.AddBool("populateCache"sv, m_UploadToZenCache); WriteAuthOptions(Writer, m_JupiterOpenIdProvider, m_JupiterAccessToken, m_JupiterAccessTokenEnv, m_JupiterAccessTokenPath, m_OidcTokenAuthExecutablePath, m_SubOptions.help()); if (m_JupiterAssumeHttp2) { Writer.AddBool("assumehttp2"sv, true); } } Writer.EndObject(); // "builds" SourceDescription = fmt::format("[builds] {}/{}/{}/{}", m_BuildsHost.empty() ? m_BuildsOverrideHost : m_BuildsHost, m_JupiterNamespace, m_JupiterBucket, m_BuildsId); } if (!m_ZenUrl.empty()) { Writer.BeginObject("zen"sv); { Writer.AddString("url"sv, m_ZenUrl); Writer.AddString("project"sv, m_ZenProjectName); Writer.AddString("oplog"sv, m_ZenOplogName); } Writer.EndObject(); // "zen" SourceDescription = fmt::format("[zen] {}", m_ZenUrl); } } Writer.EndObject(); // "params" }); ZEN_CONSOLE("Loading oplog '{}/{}' from '{}' to {}", m_ProjectName, m_OplogName, SourceDescription, m_HostName); if (m_Async) { if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), HttpClient::Accept(ZenContentType::kJSON)); Result) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed requesting loading oplog import"sv); } } else { ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), m_PlainProgress); } } //////////////////////////// OplogSnapshotSubCmd::OplogSnapshotSubCmd() : OplogSubCmdBase("snapshot", "Copy oplog's loose files on disk into zenserver") { m_SubOptions.add_option("", "p", "project", "Project name", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "o", "oplog", "Oplog name", cxxopts::value(m_OplogName), ""); m_SubOptions.parse_positional({"project", "oplog"}); } void OplogSnapshotSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; using namespace std::literals; ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "oplog.snapshot"}); HttpClient& Http = Service.Http(); if (m_ProjectName.empty()) { throw OptionParseException("'--project' is required", m_SubOptions.help()); } m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw std::runtime_error("Oplog can not be found"); } IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); ZEN_CONSOLE("Snapshotting oplog '{}/{}' to {}", m_ProjectName, m_OplogName, m_HostName); if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), Payload, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result); } else { Result.ThrowError("failed to create project"sv); } } //////////////////////////// //////////////////////////////////////////////////////////////////////////////// // ProjectStatsSubCmd ProjectStatsSubCmd::ProjectStatsSubCmd() : ProjectSubCmdBase("stats", "Stats on project store") { } void ProjectStatsSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "stats"}); HttpClient& Http = Service.Http(); if (HttpClient::Response Result = Http.Get("/stats/prj", HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed to get project stats"sv); } } //////////////////////////////////////////////////////////////////////////////// // ProjectOpDetailsSubCmd ProjectOpDetailsSubCmd::ProjectOpDetailsSubCmd() : ProjectSubCmdBase("op-details", "Detail info on ops inside a project store oplog") { m_SubOptions.add_option("", "c", "csv", "Output in CSV format (default is JSon)", cxxopts::value(m_CSV), ""); m_SubOptions.add_option("", "d", "details", "Detailed info on oplog", cxxopts::value(m_Details), "
"); m_SubOptions.add_option("", "o", "opdetails", "Details info on oplog body", cxxopts::value(m_OpDetails), ""); m_SubOptions.add_option("", "p", "project", "Project name to get info from", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "l", "oplog", "Oplog name to get info from", cxxopts::value(m_OplogName), ""); m_SubOptions.add_option("", "i", "opid", "Oid of a specific op info for", cxxopts::value(m_OpId), ""); m_SubOptions.add_option("", "a", "attachmentdetails", "Get detailed information about attachments", cxxopts::value(m_AttachmentDetails), ""); } void ProjectOpDetailsSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "op-details"}); HttpClient& Http = Service.Http(); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw std::runtime_error("Oplog can not be found"); } ExtendableStringBuilder<128> Url; Url.Append("/prj/details$"); if (!m_ProjectName.empty()) { Url.Append("/"); Url.Append(m_ProjectName); } if (!m_OplogName.empty()) { Url.Append("/"); Url.Append(m_OplogName); } if (!m_OpId.empty()) { Url.Append("/"); Url.Append(m_OpId); } if (HttpClient::Response Result = Http.Get(Url, m_CSV ? HttpClient::Accept(ZenContentType::kText) : HttpClient::Accept(ZenContentType::kJSON), {{"opdetails", m_OpDetails ? "true" : "false"}, {"details", m_Details ? "true" : "false"}, {"attachmentdetails", m_AttachmentDetails ? "true" : "false"}, {"csv", m_CSV ? "true" : "false"}})) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed to get project details"sv); } } //////////////////////////// OplogMirrorSubCmd::OplogMirrorSubCmd() : OplogSubCmdBase("mirror", "Mirror project store oplog to file system") { m_SubOptions.add_option("", "p", "project", "Project name to get info from", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "l", "oplog", "Oplog name to get info from", cxxopts::value(m_OplogName), ""); m_SubOptions.add_option("", "t", "target", "Target directory for mirror", cxxopts::value(m_MirrorRootPath), ""); m_SubOptions.add_option("", "k", "key", "Oplog key string to limit output (substring match), defaults to no filtering", cxxopts::value(m_KeyFilter), ""); m_SubOptions.add_option("", "f", "file", "Oplog file entry path string to limit output (substring match), defaults to no filtering", cxxopts::value(m_FilenameFilter), ""); m_SubOptions.add_option("", "c", "chunk", "Oplog file entry chunk id to limit output, defaults to no filtering", cxxopts::value(m_ChunkIdFilter), ""); m_SubOptions.add_option("", "d", "decompress", "Decompress data when applicable. Default = false", cxxopts::value(m_Decompress), ""); m_SubOptions.add_option( "", "", "trim", "Restricts the mirrored ops to only include the ops in the ReferencedSet. The default (`--trim=true`) is ignored if the oplog's " "ReferencedSet is invalid.", cxxopts::value(m_TrimToReferencedSet), ""); m_SubOptions.parse_positional({"project", "oplog", "target"}); m_SubOptions.positional_help("[ ]"); } void OplogMirrorSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "oplog.mirror"}); HttpClient& Http = Service.Http(); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw std::runtime_error("Oplog can not be found"); } if (m_MirrorRootPath.empty()) { throw OptionParseException("'--target' is required", m_SubOptions.help()); } Oid ChunkIdFilter = Oid::Zero; if (!m_ChunkIdFilter.empty()) { ChunkIdFilter = Oid::TryFromHexString(m_ChunkIdFilter); if (ChunkIdFilter == Oid::Zero) { throw OptionParseException(fmt::format("'--chunk' ('{}') is malformed", m_ChunkIdFilter), m_SubOptions.help()); } } if (!m_FilenameFilter.empty()) { std::replace(m_FilenameFilter.begin(), m_FilenameFilter.end(), '\\', '/'); m_FilenameFilter = ToLower(m_FilenameFilter); } if (!m_KeyFilter.empty()) { std::replace(m_KeyFilter.begin(), m_KeyFilter.end(), '\\', '/'); m_KeyFilter = ToLower(m_KeyFilter); } ZEN_CONSOLE("Emitting file data from oplog '{}/{}' to '{}'", m_ProjectName, m_OplogName, m_MirrorRootPath); // Emit file data to target directory std::filesystem::path RootPath{m_MirrorRootPath}; CreateDirectories(RootPath); std::filesystem::path TmpPath = RootPath / ".tmp"; CreateDirectories(TmpPath); auto _ = MakeGuard([&TmpPath]() { DeleteDirectories(TmpPath); }); std::atomic_int64_t FileCount = 0; int OplogEntryCount = 0; size_t WorkerCount = Min(GetHardwareConcurrency(), 16u); WorkerThreadPool WorkerPool(gsl::narrow(WorkerCount)); Latch WorkRemaining(1); size_t EmitCount = 0; std::unordered_set FileNames; std::atomic WrittenByteCount = 0; // Install Ctrl-C handler so SIGINT aborts the worker pool rather than killing // the process. Without this the local AbortFlag would shadow whatever global // handler is installed elsewhere and interrupts would be dropped. RAII so // the previous handler is restored when the function returns or throws. MirrorAbortFlag.store(false); ScopedSignalHandler SigIntGuard(SIGINT, MirrorSignalCallbackHandler); #if ZEN_PLATFORM_WINDOWS ScopedSignalHandler SigBreakGuard(SIGBREAK, MirrorSignalCallbackHandler); #endif std::atomic& AbortFlag = MirrorAbortFlag; Stopwatch WriteStopWatch; // Filenames come from the remote oplog, which may be compromised or untrusted. // Reject anything that could escape the mirror root via an absolute path, drive // letter / UNC / device path prefix, or '..' component before it is joined to // RootPath. Returns nullptr when the filename is safe. auto UnsafeFileNameReason = [](const std::filesystem::path& FileName) -> const char* { if (FileName.empty()) { return "filename is empty"; } if (FileName.has_root_name()) { return "filename has a root name (drive letter, UNC share, or device path)"; } if (FileName.has_root_directory()) { return "filename is absolute"; } for (const std::filesystem::path& Component : FileName) { const std::u8string C = Component.u8string(); if (C.empty() || C == u8"..") { return "filename contains a '..' or empty component"; } } return nullptr; }; auto EmitFilesForDataArray = [&](CbArrayView DataArray) { for (auto DataIter : DataArray) { if (CbObjectView Data = DataIter.AsObjectView()) { std::filesystem::path FileName(Data["filename"sv].AsU8String()); if (const char* Reason = UnsafeFileNameReason(FileName)) { ZEN_CONSOLE_ERROR("Rejecting unsafe filename '{}' from remote oplog: {}", FileName.string(), Reason); AbortFlag.store(true); break; } if (!m_FilenameFilter.empty()) { std::string FileNameLowerCase = ToLower(FileName.string()); if (FileNameLowerCase.find(m_FilenameFilter) == std::string::npos) { continue; } } Oid ChunkId = Data["id"sv].AsObjectId(); if (ChunkIdFilter != Oid::Zero && ChunkIdFilter != ChunkId) { continue; } if (!FileNames.insert(FileName.u8string()).second) { continue; } EmitCount++; WorkRemaining.AddCount(1); WorkerPool.ScheduleWork( [this, &RootPath, &AbortFlag, FileName, &FileCount, ChunkId, &Http, TmpPath, &WorkRemaining, &WrittenByteCount]() { auto _ = MakeGuard([&WorkRemaining]() { WorkRemaining.CountDown(); }); if (!AbortFlag) { std::filesystem::path TargetPath = RootPath / FileName; try { if (HttpClient::Response ChunkResponse = Http.Download(fmt::format("/prj/{}/oplog/{}/{}"sv, m_ProjectName, m_OplogName, ChunkId), TmpPath)) { auto TryDecompress = [](const IoBuffer& Buffer) -> IoBuffer { IoHash RawHash; uint64_t RawSize; if (CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Buffer), RawHash, RawSize)) { return Compressed.Decompress().AsIoBuffer(); }; return Buffer; }; IoBuffer ChunkData = m_Decompress ? TryDecompress(ChunkResponse.ResponsePayload) : ChunkResponse.ResponsePayload; if (std::error_code MoveEc = MoveToFile(TargetPath, ChunkData); MoveEc) { WriteFile(TargetPath, ChunkData); } WrittenByteCount.fetch_add(ChunkData.GetSize()); ++FileCount; } else { throw std::runtime_error(fmt::format("Unable to fetch '{}' (chunk {}). Reason: '{}'", FileName, ChunkId, ChunkResponse.ErrorMessage(""sv))); } } catch (const std::exception& Ex) { AbortFlag.store(true); ZEN_CONSOLE_ERROR("Failed writing file to '{}'. Reason: '{}'", TargetPath, Ex.what()); } } }, WorkerThreadPool::EMode::EnableBacklog); } } }; HttpClient::KeyValueMap Parameters{{"trim_by_referencedset", m_TrimToReferencedSet ? "true" : "false"}}; if (HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries"sv, m_ProjectName, m_OplogName), HttpClient::KeyValueMap(), Parameters)) { ZEN_CONSOLE("Fetched oplog in {}", NiceTimeSpanMs(uint64_t(Response.ElapsedSeconds * 1000.0))); if (CbObject ResponseObject = Response.AsObject()) { std::unique_ptr ProgressOwner2(CreateConsoleProgress(ConsoleProgressMode::Pretty)); std::unique_ptr EmitProgressBar; { std::unique_ptr ParseProgressBar = ProgressOwner2->CreateProgressBar(""); CbArrayView Entries = ResponseObject["entries"sv].AsArrayView(); uint64_t Remaining = Entries.Num(); for (auto EntryIter : Entries) { if (!AbortFlag) { CbObjectView Entry = EntryIter.AsObjectView(); ParseProgressBar->UpdateState( {.Task = "Parsing oplog", .Details = "", .TotalCount = Entries.Num(), .RemainingCount = Remaining}, false); Remaining--; if (!m_KeyFilter.empty()) { if (Entry["key"].AsString().find(m_KeyFilter) == std::string_view::npos) { continue; } } if (!EmitProgressBar) { EmitProgressBar = ProgressOwner2->CreateProgressBar(""sv); WriteStopWatch.Reset(); } EmitFilesForDataArray(Entry["packagedata"sv].AsArrayView()); EmitFilesForDataArray(Entry["bulkdata"sv].AsArrayView()); ++OplogEntryCount; } } ParseProgressBar->Finish(); } WorkRemaining.CountDown(); if (EmitProgressBar) { while (!WorkRemaining.Wait(200)) { uint64_t EmitRemaining = gsl::narrow(WorkRemaining.Remaining()); uint64_t WrittenBytes = WrittenByteCount.load(); uint64_t ElapsedTimeMS = WriteStopWatch.GetElapsedTimeMs(); uint64_t BytesPerSecond = ElapsedTimeMS > 0 ? (1000 * WrittenBytes) / ElapsedTimeMS : WrittenBytes; EmitProgressBar->UpdateState({.Task = "Writing files", .Details = fmt::format("{}/{} files, {}/sec ({})", EmitCount - EmitRemaining, EmitCount, NiceBytes(BytesPerSecond), NiceBytes(WrittenByteCount.load())), .TotalCount = EmitCount, .RemainingCount = EmitRemaining}, false); } EmitProgressBar->Finish(); } else { WorkRemaining.Wait(); } if (AbortFlag) { throw std::runtime_error("Failed to mirror oplog"); } } else { throw std::runtime_error("Unknown format response to oplog entries request"); } } else { Response.ThrowError("oplog entries fetch failed"); } ZEN_CONSOLE("mirrored {} files from {} oplog entries successfully", FileCount.load(), OplogEntryCount); } //////////////////////////// OplogValidateSubCmd::OplogValidateSubCmd() : OplogSubCmdBase("validate", "Validate oplog for missing references") { m_SubOptions.add_option("", "p", "project", "Project name to get info from", cxxopts::value(m_ProjectName), ""); m_SubOptions.add_option("", "l", "oplog", "Oplog name to get info from", cxxopts::value(m_OplogName), ""); m_SubOptions.parse_positional({"project", "oplog"}); m_SubOptions.positional_help("[ ]"); } void OplogValidateSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; using namespace std::literals; ZenServiceClient Service({.HostSpec = m_HostName, .CommandName = "oplog.validate"}); HttpClient& Http = Service.Http(); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { throw std::runtime_error("Oplog can not be found"); } std::string Url = fmt::format("/prj/{}/oplog/{}/validate", m_ProjectName, m_OplogName); if (HttpClient::Response Result = Http.Post(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); } else { Result.ThrowError("failed to get validate project oplog"sv); } } //////////////////////////// OplogDownloadSubCmd::OplogDownloadSubCmd() : ZenSubCmdBase("download", "Download an cloud storage oplog") { m_SubOptions.add_option("", "", "system-dir", "Specify system root", cxxopts::value(m_SystemRootDir), ""); auto AddCloudOptions = [this](cxxopts::Options& Ops) { m_AuthOptions.AddOptions(Ops); Ops.add_option("cloud build", "", "override-host", "Cloud Builds URL", cxxopts::value(m_OverrideHost), ""); Ops.add_option("cloud build", "", "cloud-url", "Cloud Artifact URL", cxxopts::value(m_Url), ""); Ops.add_option("cloud build", "", "host", "Cloud Builds host", cxxopts::value(m_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), ""); Ops.add_option("cloud build", "", "namespace", "Builds Storage namespace", cxxopts::value(m_Namespace), ""); Ops.add_option("cloud build", "", "bucket", "Builds Storage bucket", cxxopts::value(m_Bucket), ""); }; auto AddCacheOptions = [this](cxxopts::Options& Ops) { Ops.add_option("cache", "", "zen-cache-host", "Host ip and port for zen builds cache", cxxopts::value(m_ZenCacheHost), ""); Ops.add_option("cache", "", "zen-cache-upload", "Upload data downloaded from remote host to zen cache", cxxopts::value(m_UploadToZenCache), ""); }; AddCloudOptions(m_SubOptions); AddCacheOptions(m_SubOptions); auto AddOutputOptions = [this](cxxopts::Options& Ops) { Ops.add_option("", "y", "yes", "Don't query for confirmation", cxxopts::value(m_Yes), ""); Ops.add_option("output", "", "plain-progress", "Show progress using plain output", cxxopts::value(m_PlainProgress), ""); Ops.add_option("output", "", "log-progress", "Write @progress style progress to output", cxxopts::value(m_LogProgress), ""); Ops.add_option("output", "", "verbose", "Enable verbose console output", cxxopts::value(m_Verbose), ""); Ops.add_option("output", "", "quiet", "Suppress non-essential output", cxxopts::value(m_Quiet), ""); }; AddOutputOptions(m_SubOptions); m_SubOptions.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), ""); m_SubOptions.add_option("", "", "force", "Force download and disregard local cache", cxxopts::value(m_ForceDownload), ""); m_SubOptions.add_option("", "", "boost-worker-count", "Increase the number of worker threads - may cause computer to be less responsive", cxxopts::value(m_BoostWorkerCount), ""); m_SubOptions.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(m_BoostWorkerMemory), ""); m_SubOptions.add_option("", "", "boost-workers", "Enables both 'boost-worker-count' and 'boost-worker-memory' - may cause computer to be less responsive", cxxopts::value(m_BoostWorkers), ""); m_SubOptions .add_option("", "", "decompress", "Decompress downloaded attachment", cxxopts::value(m_DecompressAttachments), ""); m_SubOptions.add_option("", "", "output-path", "Path to oplog output, extension .json or .cb (compact binary). Default is output to console", cxxopts::value(m_OplogOutputPath), ""); m_SubOptions.add_option("", "", "attachments", "Comma separated list of attachments in RawHash for to download", cxxopts::value(m_Attachments), ""); m_SubOptions .add_option("", "", "attachments-path", "Path to folder to write attachments to", cxxopts::value(m_AttachmentsPath), ""); m_SubOptions.parse_positional({"cloud-url", "output-path"}); m_SubOptions.positional_help("[ ]"); } void OplogDownloadSubCmd::Run(const ZenCliOptions& /*GlobalOptions*/) { using namespace projectstore_impl; if (!m_Quiet) { ZenCmdBase::LogExecutableVersionAndPid(); } auto ParseSystemOptions = [&]() { if (m_SystemRootDir.empty()) { m_SystemRootDir = PickDefaultSystemRootDirectory(); } MakeSafeAbsolutePathInPlace(m_SystemRootDir); }; ParseSystemOptions(); ConsoleProgressMode ProgressMode = ConsoleProgressMode::Pretty; auto ParseOutputOptions = [&]() { if (m_Verbose && m_Quiet) { throw OptionParseException("'--verbose' conflicts with '--quiet'", m_SubOptions.help()); } if (m_LogProgress && m_PlainProgress) { throw OptionParseException("'--plain-progress' conflicts with '--log-progress'", m_SubOptions.help()); } if (m_LogProgress && m_Quiet) { throw OptionParseException("'--quiet' conflicts with '--log-progress'", m_SubOptions.help()); } if (m_PlainProgress && m_Quiet) { throw OptionParseException("'--quiet' conflicts with '--plain-progress'", m_SubOptions.help()); } if (m_LogProgress) { ProgressMode = ConsoleProgressMode::Log; } else if (m_PlainProgress) { ProgressMode = ConsoleProgressMode::Plain; } else if (m_Quiet) { ProgressMode = ConsoleProgressMode::Quiet; } else { ProgressMode = ConsoleProgressMode::Pretty; } }; ParseOutputOptions(); 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_SubOptions.help()); } if (!m_Bucket.empty()) { throw OptionParseException(fmt::format("'--bucket' ('{}') conflicts with '--url' ('{}')", m_Bucket, m_Url), m_SubOptions.help()); } if (!m_BuildId.empty()) { throw OptionParseException(fmt::format("'--buildid' ('{}') conflicts with '--url' ('{}')", m_BuildId, m_Url), m_SubOptions.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_SubOptions.help()); } } if (!m_OverrideHost.empty() || !m_Host.empty()) { if (RequireNamespace && m_Namespace.empty()) { throw OptionParseException("'--namespace' is required", m_SubOptions.help()); } if (RequireBucket && m_Bucket.empty()) { throw OptionParseException("'--bucket' is required", m_SubOptions.help()); } } if (m_OverrideHost.empty() && m_Host.empty()) { throw OptionParseException("'--host' or '--overridehost' is required", m_SubOptions.help()); } }; ParseStorageOptions(/*RequireNamespace*/ true, /*RequireBucket*/ true); if (m_BoostWorkers) { m_BoostWorkerCount = true; m_BoostWorkerMemory = true; } std::unique_ptr Progress(CreateConsoleProgress(ProgressMode)); TransferThreadWorkers Workers(m_BoostWorkerCount, /*SingleThreaded*/ false); if (!m_Quiet) { ZEN_CONSOLE("{}", Workers.GetWorkersInfo()); } std::unique_ptr Auth; HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", .AssumeHttp2 = m_AssumeHttp2, .AllowResume = true, .RetryCount = 2}; Oid BuildId = Oid::TryFromHexString(m_BuildId); if (BuildId == Oid::Zero) { throw OptionParseException(fmt::format("'--build-id' ('{}') is malformed", m_BuildId), m_SubOptions.help()); } m_AuthOptions.ParseOptions(m_SubOptions, m_SystemRootDir, ClientSettings, m_OverrideHost.empty() ? m_Host : m_OverrideHost, Auth, m_Quiet, /*Hidden*/ false, m_Verbose); BuildStorageResolveResult ResolveRes = ResolveBuildStorage(ConsoleLog(), ClientSettings, m_Host, m_OverrideHost, m_ZenCacheHost, ZenCacheResolveMode::Discovery, m_Verbose); BuildStorageBase::Statistics StorageStats; StorageInstance Storage; ClientSettings.AssumeHttp2 = ResolveRes.Cloud.AssumeHttp2; ClientSettings.MaximumInMemoryDownloadSize = m_BoostWorkerMemory ? RemoteStoreOptions::DefaultMaxBlockSize : 1024u * 1024u; Storage.BuildStorageHttp = std::make_unique(ResolveRes.Cloud.Address, ClientSettings); Storage.BuildStorageHost = ResolveRes.Cloud; BuildStorageCache::Statistics StorageCacheStats; std::atomic AbortFlag(false); if (!ResolveRes.Cache.Address.empty()) { Storage.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, .MaximumInMemoryDownloadSize = m_BoostWorkerMemory ? RemoteStoreOptions::DefaultMaxBlockSize : 1024u * 1024u}, [&AbortFlag]() { return AbortFlag.load(); }); Storage.CacheHost = ResolveRes.Cache; Storage.SetupCacheSession(ResolveRes.Cache.Address, "oplog.download", GetSessionId()); } if (!m_Quiet) { std::string StorageDescription = fmt::format("Cloud {}{}. SessionId {}. Namespace '{}', Bucket '{}'", ResolveRes.Cloud.Name, (ResolveRes.Cloud.Address == ResolveRes.Cloud.Name) ? "" : fmt::format(" {}", ResolveRes.Cloud.Address), Storage.BuildStorageHttp->GetSessionId(), m_Namespace, m_Bucket); ZEN_CONSOLE("Remote: {}", StorageDescription); if (Storage.CacheHttp) { std::string CacheDescription = fmt::format("Zen {}{}. SessionId {}. Namespace '{}', Bucket '{}'", ResolveRes.Cache.Name, (ResolveRes.Cache.Address == ResolveRes.Cache.Name) ? "" : fmt::format(" {}", ResolveRes.Cache.Address), Storage.CacheHttp->GetSessionId(), m_Namespace, m_Bucket); ZEN_CONSOLE("Cache : {}", CacheDescription); } } std::string FullBuildKey = fmt::format("{}_{}_{}", m_Namespace, m_Bucket, m_BuildId); IoHash FullBuildKeyHash = IoHash::HashBuffer(FullBuildKey.data(), FullBuildKey.length()); std::filesystem::path StorageTempPath = std::filesystem::temp_directory_path() / ("zen_" + FullBuildKeyHash.ToHexString()); Storage.BuildStorage = CreateJupiterBuildStorage(Log(), *Storage.BuildStorageHttp, StorageStats, m_Namespace, m_Bucket, m_AllowRedirect, StorageTempPath); if (Storage.CacheHttp) { Storage.CacheStorage = CreateZenBuildStorageCache( *Storage.CacheHttp, StorageCacheStats, m_Namespace, m_Bucket, StorageTempPath / "zencache", false ? GetSmallWorkerPool(EWorkloadType::Background) : GetTinyWorkerPool(EWorkloadType::Background)); } ProjectStoreOperationOplogState State( ConsoleLog(), Storage, BuildId, {.IsQuiet = m_Quiet, .IsVerbose = m_Verbose, .ForceDownload = m_ForceDownload, .TempFolderPath = StorageTempPath}); if (!m_Attachments.empty()) { if (m_AttachmentsPath.empty()) { throw OptionParseException("'--attachments-path' is required when '--attachments' is given", m_SubOptions.help()); } std::filesystem::path AttachmentsPath = MakeSafeAbsolutePath(m_AttachmentsPath); CreateDirectories(AttachmentsPath); std::vector AttachmentHashes; AttachmentHashes.reserve(m_Attachments.size()); for (const std::string& Attachment : m_Attachments) { IoHash RawHash; if (!IoHash::TryParse(Attachment, RawHash)) { throw OptionParseException(fmt::format("'--attachments' ('{}') is malformed", Attachment), m_SubOptions.help()); } AttachmentHashes.push_back(RawHash); } std::atomic PauseFlag; ProjectStoreOperationDownloadAttachments Op(ConsoleLog(), *Progress, Storage, AbortFlag, PauseFlag, Workers.GetIOWorkerPool(), Workers.GetNetworkPool(), State, AttachmentHashes, {.IsQuiet = m_Quiet, .IsVerbose = m_Verbose, .ForceDownload = m_ForceDownload, .DecompressAttachments = m_DecompressAttachments, .TempFolderPath = StorageTempPath, .AttachmentOutputPath = m_AttachmentsPath, .PopulateCache = m_UploadToZenCache}); Op.Execute(); } else { CbObjectView OpsSectionObject = State.LoadOpsSectionObject(); if (m_OplogOutputPath.empty()) { if (!m_Yes) { if (OpsSectionObject.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(OpsSectionObject.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; OpsSectionObject.ToJson(SB); ForEachStrTok(SB.ToView(), '\n', [](std::string_view Row) { ZEN_CONSOLE("{}", Row); return true; }); } else { Stopwatch Timer; const std::string Extension = ToLower(m_OplogOutputPath.extension().string()); if (Extension == ".cb" || Extension == ".cbo") { WriteFile(m_OplogOutputPath, IoBuffer(IoBuffer::Wrap, OpsSectionObject.GetView().GetData(), OpsSectionObject.GetSize())); } else if (Extension == ".json") { ExtendableStringBuilder<1024> SB; OpsSectionObject.ToJson(SB); WriteFile(m_OplogOutputPath, 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_OplogOutputPath)), m_OplogOutputPath, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); } } } } } // namespace zen