aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/storage/admin/admin.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2025-10-14 11:32:16 +0200
committerGitHub Enterprise <[email protected]>2025-10-14 11:32:16 +0200
commitca09abbeef5b1788f4a52b61eedd2f3dd07f81f2 (patch)
tree005a50adfddf6982bab3a06bb93d4c50da1a11fd /src/zenserver/storage/admin/admin.cpp
parentmake asiohttp work without IPv6 (#562) (diff)
downloadzen-ca09abbeef5b1788f4a52b61eedd2f3dd07f81f2.tar.xz
zen-ca09abbeef5b1788f4a52b61eedd2f3dd07f81f2.zip
move all storage-related services into storage tree (#571)
* move all storage-related services into storage tree * move config into config/ * also move admin service into storage since it mostly has storage related functionality * header consolidation
Diffstat (limited to 'src/zenserver/storage/admin/admin.cpp')
-rw-r--r--src/zenserver/storage/admin/admin.cpp804
1 files changed, 804 insertions, 0 deletions
diff --git a/src/zenserver/storage/admin/admin.cpp b/src/zenserver/storage/admin/admin.cpp
new file mode 100644
index 000000000..4803063d7
--- /dev/null
+++ b/src/zenserver/storage/admin/admin.cpp
@@ -0,0 +1,804 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "admin.h"
+
+#include <zencore/compactbinarybuilder.h>
+#include <zencore/filesystem.h>
+#include <zencore/fmtutils.h>
+#include <zencore/jobqueue.h>
+#include <zencore/logging.h>
+#include <zencore/string.h>
+#include <zenstore/cache/structuredcachestore.h>
+#include <zenstore/gc.h>
+#include <zenutil/workerpools.h>
+
+#if ZEN_WITH_TRACE
+# include <zencore/trace.h>
+#endif // ZEN_WITH_TRACE
+
+#if ZEN_USE_MIMALLOC
+# include <mimalloc.h>
+#endif
+
+#include "config/config.h"
+
+#include <chrono>
+
+namespace zen {
+
+struct DirStats
+{
+ uint64_t FileCount = 0;
+ uint64_t DirCount = 0;
+ uint64_t ByteCount = 0;
+};
+
+DirStats
+GetStatsForDirectory(std::filesystem::path Dir)
+{
+ if (!IsDir(Dir))
+ return {};
+
+ struct StatsTraversal : public GetDirectoryContentVisitor
+ {
+ virtual void AsyncVisitDirectory(const std::filesystem::path& RelativeRoot, DirectoryContent&& Content) override
+ {
+ ZEN_UNUSED(RelativeRoot);
+
+ uint64_t FileCount = Content.FileNames.size();
+ uint64_t DirCount = Content.DirectoryNames.size();
+ uint64_t FilesSize = 0;
+ for (uint64_t FileSize : Content.FileSizes)
+ {
+ FilesSize += FileSize;
+ }
+ TotalBytes += FilesSize;
+ TotalFileCount += FileCount;
+ TotalDirCount += DirCount;
+ }
+
+ std::atomic_uint64_t TotalBytes = 0;
+ std::atomic_uint64_t TotalFileCount = 0;
+ std::atomic_uint64_t TotalDirCount = 0;
+
+ DirStats GetStats()
+ {
+ return {.FileCount = TotalFileCount.load(), .DirCount = TotalDirCount.load(), .ByteCount = TotalBytes.load()};
+ }
+ } DirTraverser;
+
+ Latch PendingWorkCount(1);
+
+ GetDirectoryContent(Dir,
+ DirectoryContentFlags::IncludeAllEntries | DirectoryContentFlags::IncludeFileSizes,
+ DirTraverser,
+ GetSmallWorkerPool(EWorkloadType::Background),
+ PendingWorkCount);
+ PendingWorkCount.CountDown();
+ PendingWorkCount.Wait();
+
+ return DirTraverser.GetStats();
+}
+
+struct StateDiskStats
+{
+ DirStats CacheStats;
+ DirStats CasStats;
+ DirStats ProjectStats;
+};
+
+StateDiskStats
+GetStatsForStateDirectory(std::filesystem::path StateDir)
+{
+ StateDiskStats Stats;
+ Stats.CacheStats = GetStatsForDirectory(StateDir / "cache");
+ Stats.CasStats = GetStatsForDirectory(StateDir / "cas");
+ Stats.ProjectStats = GetStatsForDirectory(StateDir / "projects");
+ return Stats;
+}
+
+HttpAdminService::HttpAdminService(GcScheduler& Scheduler,
+ JobQueue& BackgroundJobQueue,
+ ZenCacheStore* CacheStore,
+ std::function<void()>&& FlushFunction,
+ const LogPaths& LogPaths,
+ const ZenServerOptions& ServerOptions)
+: m_GcScheduler(Scheduler)
+, m_BackgroundJobQueue(BackgroundJobQueue)
+, m_CacheStore(CacheStore)
+, m_FlushFunction(std::move(FlushFunction))
+, m_LogPaths(LogPaths)
+, m_ServerOptions(ServerOptions)
+{
+ using namespace std::literals;
+
+ m_Router.RegisterRoute(
+ "health",
+ [](HttpRouterRequest& Req) {
+ CbObjectWriter Obj;
+ Obj.AddBool("ok", true);
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ },
+ HttpVerb::kGet);
+
+ m_Router.AddPattern("jobid", "([[:digit:]]+?)");
+
+ m_Router.RegisterRoute(
+ "jobs",
+ [&](HttpRouterRequest& Req) {
+ std::vector<JobQueue::JobInfo> Jobs = m_BackgroundJobQueue.GetJobs();
+ CbObjectWriter Obj;
+ Obj.BeginArray("jobs");
+ for (const auto& Job : Jobs)
+ {
+ Obj.BeginObject();
+ Obj.AddInteger("Id", Job.Id.Id);
+ Obj.AddString("Status", JobQueue::ToString(Job.Status));
+ Obj.EndObject();
+ }
+ Obj.EndArray();
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "jobs/{jobid}",
+ [&](HttpRouterRequest& Req) {
+ const auto& JobIdString = Req.GetCapture(1);
+ std::optional<uint64_t> JobIdArg = ParseInt<uint64_t>(JobIdString);
+ if (!JobIdArg)
+ {
+ Req.ServerRequest().WriteResponse(HttpResponseCode::BadRequest);
+ }
+ JobId Id{.Id = JobIdArg.value_or(0)};
+ if (Id.Id == 0)
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::BadRequest,
+ ZenContentType::kText,
+ fmt::format("Invalid Job Id: {}", Id.Id));
+ }
+
+ std::optional<JobQueue::JobDetails> CurrentState = m_BackgroundJobQueue.Get(Id);
+ if (!CurrentState)
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound);
+ }
+
+ auto WriteState = [](CbObjectWriter& Obj, const JobQueue::State& State) {
+ if (!State.CurrentOp.empty())
+ {
+ Obj.AddString(
+ "CurrentOp"sv,
+ State.CurrentOpDetails.empty() ? State.CurrentOp : fmt::format("{}: {}", State.CurrentOp, State.CurrentOpDetails));
+ Obj.AddString("Op"sv, State.CurrentOp);
+ if (!State.CurrentOpDetails.empty())
+ {
+ Obj.AddString("Details"sv, State.CurrentOpDetails);
+ }
+ Obj.AddInteger("TotalCount"sv, gsl::narrow<uint64_t>(State.TotalCount));
+ Obj.AddInteger("RemainingCount"sv, gsl::narrow<uint64_t>(State.RemainingCount));
+ Obj.AddInteger("CurrentOpPercentComplete"sv,
+ State.TotalCount > 0
+ ? gsl::narrow<uint32_t>((100 * (State.TotalCount - State.RemainingCount)) / State.TotalCount)
+ : 0);
+ }
+ if (!State.Messages.empty())
+ {
+ Obj.BeginArray("Messages");
+ for (const std::string& Message : State.Messages)
+ {
+ Obj.AddString(Message);
+ }
+ Obj.EndArray();
+ }
+ if (!State.AbortReason.empty())
+ {
+ Obj.AddString("AbortReason"sv, State.AbortReason);
+ }
+ };
+
+ auto GetAgeAsSeconds = [](std::chrono::system_clock::time_point Start, std::chrono::system_clock::time_point End) {
+ auto Age = End - Start;
+ auto Milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(Age);
+ return Milliseconds.count() / 1000.0;
+ };
+
+ const std::chrono::system_clock::time_point Now = std::chrono::system_clock::now();
+
+ switch (CurrentState->Status)
+ {
+ case JobQueue::Status::Queued:
+ {
+ CbObjectWriter Obj;
+ Obj.AddString("Name"sv, CurrentState->Name);
+ Obj.AddString("Status"sv, "Queued"sv);
+ Obj.AddFloat("QueueTimeS", GetAgeAsSeconds(CurrentState->CreateTime, Now));
+ Obj.AddInteger("WorkerThread", CurrentState->WorkerThreadId);
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ }
+ break;
+ case JobQueue::Status::Running:
+ {
+ CbObjectWriter Obj;
+ Obj.AddString("Name"sv, CurrentState->Name);
+ Obj.AddString("Status"sv, "Running"sv);
+ WriteState(Obj, CurrentState->State);
+ Obj.AddFloat("QueueTimeS", GetAgeAsSeconds(CurrentState->CreateTime, CurrentState->StartTime));
+ Obj.AddFloat("RunTimeS", GetAgeAsSeconds(CurrentState->StartTime, Now));
+ Obj.AddInteger("WorkerThread", CurrentState->WorkerThreadId);
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ }
+ break;
+ case JobQueue::Status::Aborted:
+ {
+ CbObjectWriter Obj;
+ Obj.AddString("Name"sv, CurrentState->Name);
+ Obj.AddString("Status"sv, "Aborted"sv);
+
+ WriteState(Obj, CurrentState->State);
+ Obj.AddFloat("QueueTimeS", GetAgeAsSeconds(CurrentState->CreateTime, CurrentState->StartTime));
+ Obj.AddFloat("RunTimeS", GetAgeAsSeconds(CurrentState->StartTime, CurrentState->EndTime));
+ Obj.AddFloat("CompleteTimeS", GetAgeAsSeconds(CurrentState->EndTime, Now));
+ Obj.AddInteger("ReturnCode", CurrentState->ReturnCode);
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ }
+ break;
+ case JobQueue::Status::Completed:
+ {
+ CbObjectWriter Obj;
+ Obj.AddString("Name"sv, CurrentState->Name);
+ Obj.AddString("Status"sv, "Complete"sv);
+ WriteState(Obj, CurrentState->State);
+ Obj.AddFloat("QueueTimeS", GetAgeAsSeconds(CurrentState->CreateTime, CurrentState->StartTime));
+ Obj.AddFloat("RunTimeS", GetAgeAsSeconds(CurrentState->StartTime, CurrentState->EndTime));
+ Obj.AddFloat("CompleteTimeS", GetAgeAsSeconds(CurrentState->EndTime, Now));
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ }
+ break;
+ }
+ },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "jobs/{jobid}",
+ [&](HttpRouterRequest& Req) {
+ const auto& JobIdString = Req.GetCapture(1);
+ std::optional<uint64_t> JobIdArg = ParseInt<uint64_t>(JobIdString);
+ if (!JobIdArg)
+ {
+ Req.ServerRequest().WriteResponse(HttpResponseCode::BadRequest);
+ }
+ JobId Id{.Id = JobIdArg.value_or(0)};
+ if (m_BackgroundJobQueue.CancelJob(Id))
+ {
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK);
+ }
+ else
+ {
+ Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound);
+ }
+ },
+ HttpVerb::kDelete);
+
+ m_Router.RegisterRoute(
+ "gc",
+ [this](HttpRouterRequest& Req) {
+ const GcSchedulerState State = m_GcScheduler.GetState();
+
+ const HttpServerRequest::QueryParams Params = Req.ServerRequest().GetQueryParams();
+
+ bool Details = false;
+ if (auto Param = Params.GetValue("details"); Param == "true")
+ {
+ Details = true;
+ }
+
+ CbObjectWriter Response;
+ Response << "Status"sv << (GcSchedulerStatus::kIdle == State.Status ? "Idle"sv : "Running"sv);
+ Response.BeginObject("Config");
+ {
+ Response << "RootDirectory" << State.Config.RootDirectory.string();
+ Response << "MonitorInterval" << ToTimeSpan(State.Config.MonitorInterval);
+ Response << "Interval" << ToTimeSpan(State.Config.Interval);
+ Response << "MaxCacheDuration" << ToTimeSpan(State.Config.MaxCacheDuration);
+ Response << "MaxProjectStoreDuration" << ToTimeSpan(State.Config.MaxProjectStoreDuration);
+ Response << "MaxBuildStoreDuration" << ToTimeSpan(State.Config.MaxBuildStoreDuration);
+ Response << "CollectSmallObjects" << State.Config.CollectSmallObjects;
+ Response << "Enabled" << State.Config.Enabled;
+ Response << "DiskReserveSize" << NiceBytes(State.Config.DiskReserveSize);
+ Response << "DiskSizeSoftLimit" << NiceBytes(State.Config.DiskSizeSoftLimit);
+ Response << "MinimumFreeDiskSpaceToAllowWrites" << NiceBytes(State.Config.MinimumFreeDiskSpaceToAllowWrites);
+ Response << "LightweightInterval" << ToTimeSpan(State.Config.LightweightInterval);
+ Response << "UseGCVersion" << ((State.Config.UseGCVersion == GcVersion::kV1_Deprecated) ? "1" : "2");
+ Response << "CompactBlockUsageThresholdPercent" << State.Config.CompactBlockUsageThresholdPercent;
+ Response << "Verbose" << State.Config.Verbose;
+ Response << "SingleThreaded" << State.Config.SingleThreaded;
+ Response << "AttachmentPassCount" << State.Config.AttachmentPassCount;
+ }
+ Response.EndObject();
+ Response << "AreDiskWritesBlocked" << State.AreDiskWritesBlocked;
+ Response << "HasDiskReserve" << State.HasDiskReserve;
+ Response << "DiskSize" << NiceBytes(State.DiskSize);
+ Response << "DiskUsed" << NiceBytes(State.DiskUsed);
+ Response << "DiskFree" << NiceBytes(State.DiskFree);
+
+ Response.BeginObject("FullGC");
+ {
+ Response << "LastTime" << ToDateTime(State.LastFullGcTime);
+ Response << "TimeToNext" << ToTimeSpan(State.RemainingTimeUntilFullGc);
+ if (State.Config.DiskSizeSoftLimit != 0)
+ {
+ Response << "SpaceToNext" << NiceBytes(State.RemainingSpaceUntilFullGC);
+ }
+ if (State.LastFullGCV2Result)
+ {
+ const bool HumanReadable = true;
+ WriteGCResult(Response, State.LastFullGCV2Result.value(), HumanReadable, Details);
+ }
+ else
+ {
+ Response << "LastDuration" << ToTimeSpan(State.LastFullGcDuration);
+ Response << "LastDiskFreed" << NiceBytes(State.LastFullGCDiff.DiskSize);
+ Response << "LastMemoryFreed" << NiceBytes(State.LastFullGCDiff.MemorySize);
+ }
+ if (State.LastFullAttachmentRangeMin != IoHash::Zero || State.LastFullAttachmentRangeMax != IoHash::Max)
+ {
+ Response << "AttachmentRangeMin" << State.LastFullAttachmentRangeMin;
+ Response << "AttachmentRangeMax" << State.LastFullAttachmentRangeMax;
+ }
+ }
+ Response.EndObject();
+ Response.BeginObject("LightweightGC");
+ {
+ Response << "LastTime" << ToDateTime(State.LastLightweightGcTime);
+ Response << "TimeToNext" << ToTimeSpan(State.RemainingTimeUntilLightweightGc);
+
+ if (State.LastLightweightGCV2Result)
+ {
+ const bool HumanReadable = true;
+ WriteGCResult(Response, State.LastLightweightGCV2Result.value(), HumanReadable, Details);
+ }
+ else
+ {
+ Response << "LastDuration" << ToTimeSpan(State.LastLightweightGcDuration);
+ Response << "LastDiskFreed" << NiceBytes(State.LastLightweightGCDiff.DiskSize);
+ Response << "LastMemoryFreed" << NiceBytes(State.LastLightweightGCDiff.MemorySize);
+ }
+ }
+ Response.EndObject();
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Response.Save());
+ },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "gc",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ const HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();
+ GcScheduler::TriggerGcParams GcParams;
+
+ if (auto Param = Params.GetValue("smallobjects"); Param.empty() == false)
+ {
+ GcParams.CollectSmallObjects = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("maxcacheduration"); Param.empty() == false)
+ {
+ if (auto Value = ParseInt<uint64_t>(Param))
+ {
+ GcParams.MaxCacheDuration = std::chrono::seconds(Value.value());
+ }
+ }
+
+ if (auto Param = Params.GetValue("maxprojectstoreduration"); Param.empty() == false)
+ {
+ if (auto Value = ParseInt<uint64_t>(Param))
+ {
+ GcParams.MaxProjectStoreDuration = std::chrono::seconds(Value.value());
+ }
+ }
+
+ if (auto Param = Params.GetValue("maxbuildstoreduration"); Param.empty() == false)
+ {
+ if (auto Value = ParseInt<uint64_t>(Param))
+ {
+ GcParams.MaxBuildStoreDuration = std::chrono::seconds(Value.value());
+ }
+ }
+
+ if (auto Param = Params.GetValue("disksizesoftlimit"); Param.empty() == false)
+ {
+ if (auto Value = ParseInt<uint64_t>(Param))
+ {
+ GcParams.DiskSizeSoftLimit = Value.value();
+ }
+ }
+
+ if (auto Param = Params.GetValue("skipcid"); Param.empty() == false)
+ {
+ GcParams.SkipCid = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("skipdelete"); Param.empty() == false)
+ {
+ GcParams.SkipDelete = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("forceusegcv1"); Param.empty() == false)
+ {
+ GcParams.ForceGCVersion = GcVersion::kV1_Deprecated;
+ }
+
+ if (auto Param = Params.GetValue("forceusegcv2"); Param.empty() == false)
+ {
+ GcParams.ForceGCVersion = GcVersion::kV2;
+ }
+
+ if (auto Param = Params.GetValue("compactblockthreshold"); Param.empty() == false)
+ {
+ if (auto Value = ParseInt<uint32_t>(Param))
+ {
+ GcParams.CompactBlockUsageThresholdPercent = Value.value();
+ }
+ }
+
+ if (auto Param = Params.GetValue("verbose"); Param.empty() == false)
+ {
+ GcParams.Verbose = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("singlethreaded"); Param.empty() == false)
+ {
+ GcParams.SingleThreaded = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("referencehashlow"); Param.empty() == false)
+ {
+ GcParams.AttachmentRangeMin = IoHash::FromHexString(Param);
+ }
+
+ if (auto Param = Params.GetValue("referencehashhigh"); Param.empty() == false)
+ {
+ GcParams.AttachmentRangeMax = IoHash::FromHexString(Param);
+ }
+
+ if (auto Param = Params.GetValue("storecacheattachmentmetadata"); Param.empty() == false)
+ {
+ GcParams.StoreCacheAttachmentMetaData = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("storeprojectattachmentmetadata"); Param.empty() == false)
+ {
+ GcParams.StoreProjectAttachmentMetaData = Param == "true"sv;
+ }
+
+ if (auto Param = Params.GetValue("enablevalidation"); Param.empty() == false)
+ {
+ GcParams.EnableValidation = Param == "true"sv;
+ }
+
+ const bool Started = m_GcScheduler.TriggerGc(GcParams);
+
+ CbObjectWriter Response;
+ Response << "Status"sv << (Started ? "Started"sv : "Running"sv);
+ HttpReq.WriteResponse(HttpResponseCode::Accepted, Response.Save());
+ },
+ HttpVerb::kPost);
+
+ m_Router.RegisterRoute(
+ "gc-stop",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ if (m_GcScheduler.CancelGC())
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::Accepted);
+ }
+ HttpReq.WriteResponse(HttpResponseCode::OK);
+ },
+ HttpVerb::kPost);
+
+#if ZEN_USE_MIMALLOC
+ m_Router.RegisterRoute(
+ "mi_collect",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ const HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();
+
+ bool Force = false;
+
+ if (auto Param = Params.GetValue("force"); Param.empty() == false)
+ {
+ Force = (Param == "true"sv);
+ }
+
+ ExtendableStringBuilder<256> MiStats;
+ ExtendableStringBuilder<256> MiStatsAfter;
+
+ auto MiOutputFun = [](const char* msg, void* arg) {
+ StringBuilderBase* StarsSb = reinterpret_cast<StringBuilderBase*>(arg);
+ StarsSb->AppendAscii(msg);
+ };
+
+ mi_stats_print_out(MiOutputFun, static_cast<StringBuilderBase*>(&MiStats));
+ mi_collect(Force);
+ mi_stats_print_out(MiOutputFun, static_cast<StringBuilderBase*>(&MiStatsAfter));
+
+ CbObjectWriter Response;
+ Response << "force"sv << Force;
+ Response << "stats_before"sv << MiStats;
+ Response << "stats_after"sv << MiStatsAfter;
+ HttpReq.WriteResponse(HttpResponseCode::OK, Response.Save());
+ },
+ HttpVerb::kPost);
+#endif
+
+ m_Router.RegisterRoute(
+ "scrub",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ const HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();
+
+ GcScheduler::TriggerScrubParams ScrubParams;
+ ScrubParams.MaxTimeslice = std::chrono::seconds(100);
+
+ if (auto Param = Params.GetValue("skipdelete"); Param.empty() == false)
+ {
+ ScrubParams.SkipDelete = (Param == "true"sv);
+ }
+
+ if (auto Param = Params.GetValue("skipgc"); Param.empty() == false)
+ {
+ ScrubParams.SkipGc = (Param == "true"sv);
+ }
+
+ if (auto Param = Params.GetValue("skipcid"); Param.empty() == false)
+ {
+ ScrubParams.SkipCas = (Param == "true"sv);
+ }
+
+ m_GcScheduler.TriggerScrub(ScrubParams);
+
+ CbObjectWriter Response;
+ Response << "ok"sv << true;
+ Response << "skip_delete" << ScrubParams.SkipDelete;
+ Response << "skip_gc" << ScrubParams.SkipGc;
+ Response << "skip_cas" << ScrubParams.SkipCas;
+ Response << "max_time" << TimeSpan(0, 0, gsl::narrow<int>(ScrubParams.MaxTimeslice.count()));
+ HttpReq.WriteResponse(HttpResponseCode::OK, Response.Save());
+ },
+ HttpVerb::kPost);
+
+ m_Router.RegisterRoute(
+ "",
+ [](HttpRouterRequest& Req) {
+ CbObject Payload = Req.ServerRequest().ReadPayloadObject();
+
+ CbObjectWriter Obj;
+ Obj.AddBool("ok", true);
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ },
+ HttpVerb::kPost);
+
+#if ZEN_WITH_TRACE
+ m_Router.RegisterRoute(
+ "trace",
+ [this](HttpRouterRequest& Req) {
+ bool Enabled = IsTracing();
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::OK, HttpContentType::kText, Enabled ? "enabled" : "disabled");
+ },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "trace/start",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ const HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();
+ TraceOptions TraceOptions;
+
+ if (!IsTracing())
+ {
+ TraceInit("zenserver");
+ }
+
+ if (auto Channels = Params.GetValue("channels"); Channels.empty() == false)
+ {
+ TraceOptions.Channels = Channels;
+ }
+
+ if (auto File = Params.GetValue("file"); File.empty() == false)
+ {
+ TraceOptions.File = File;
+ }
+ else if (auto Host = Params.GetValue("host"); Host.empty() == false)
+ {
+ TraceOptions.Host = Host;
+ }
+ else
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::BadRequest,
+ HttpContentType::kText,
+ "Invalid trace type, use `file` or `host`"sv);
+ }
+
+ TraceConfigure(TraceOptions);
+
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "Tracing started");
+ },
+ HttpVerb::kPost);
+
+ m_Router.RegisterRoute(
+ "trace/stop",
+ [this](HttpRouterRequest& Req) {
+ if (!IsTracing())
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Tracing is not enabled"sv);
+ }
+ if (TraceStop())
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::OK, HttpContentType::kText, "Tracing stopped");
+ }
+ else
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::InternalServerError,
+ HttpContentType::kText,
+ "Failed stopping trace");
+ }
+ },
+ HttpVerb::kPost);
+#endif // ZEN_WITH_TRACE
+
+ m_Router.RegisterRoute(
+ "info",
+ [this](HttpRouterRequest& Req) {
+ CbObjectWriter Obj;
+
+ Obj << "root" << m_ServerOptions.SystemRootDir.generic_wstring();
+ Obj << "install" << (m_ServerOptions.SystemRootDir / "Install").generic_wstring();
+
+ Obj.BeginObject("primary");
+ Obj << "data" << m_ServerOptions.DataDir.generic_wstring();
+
+ try
+ {
+ auto Stats = GetStatsForStateDirectory(m_ServerOptions.DataDir);
+
+ auto EmitStats = [&](std::string_view Tag, const DirStats& Stats) {
+ Obj.BeginObject(Tag);
+ Obj << "bytes" << Stats.ByteCount;
+ Obj << "files" << Stats.FileCount;
+ Obj << "dirs" << Stats.DirCount;
+ Obj.EndObject();
+ };
+
+ EmitStats("cache", Stats.CacheStats);
+ EmitStats("cas", Stats.CasStats);
+ EmitStats("project", Stats.ProjectStats);
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_WARN("exception in disk stats gathering for '{}': {}", m_ServerOptions.DataDir, Ex.what());
+ }
+ Obj.EndObject();
+
+ try
+ {
+ std::vector<CbObject> Manifests = ReadAllCentralManifests(m_ServerOptions.SystemRootDir);
+
+ Obj.BeginArray("known");
+
+ for (const auto& Manifest : Manifests)
+ {
+ Obj.AddObject(Manifest);
+ }
+
+ Obj.EndArray();
+ }
+ catch (const std::exception& Ex)
+ {
+ ZEN_WARN("exception in state gathering for '{}': {}", m_ServerOptions.SystemRootDir, Ex.what());
+ }
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "logs",
+ [this](HttpRouterRequest& Req) {
+ CbObjectWriter Obj;
+ auto LogLevel = logging::level::ToStringView(logging::GetLogLevel());
+ Obj.AddString("loglevel", std::string_view(LogLevel.data(), LogLevel.size()));
+ Obj.AddString("Logfile", PathToUtf8(m_LogPaths.AbsLogPath));
+ Obj.BeginObject("cache");
+ if (m_CacheStore)
+ {
+ const ZenCacheStore::Configuration& CacheConfig = m_CacheStore->GetConfiguration();
+ Obj.AddString("Logfile", PathToUtf8(m_LogPaths.CacheLogPath));
+ Obj.AddBool("Write", CacheConfig.Logging.EnableWriteLog);
+ Obj.AddBool("Access", CacheConfig.Logging.EnableAccessLog);
+ }
+ Obj.EndObject();
+ Obj.BeginObject("http");
+ {
+ Obj.AddString("Logfile", PathToUtf8(m_LogPaths.HttpLogPath));
+ }
+ Obj.EndObject();
+ Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Obj.Save());
+ },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "logs",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ const HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams();
+ bool SetCacheLogConfig = false;
+ ExtendableStringBuilder<256> StringBuilder;
+ if (m_CacheStore)
+ {
+ ZenCacheStore::Configuration::LogConfig LoggingConfig = m_CacheStore->GetConfiguration().Logging;
+ if (std::string Param(Params.GetValue("cacheenablewritelog")); Param.empty() == false)
+ {
+ LoggingConfig.EnableWriteLog = StrCaseCompare(Param.c_str(), "true") == 0;
+ SetCacheLogConfig = true;
+ }
+ if (std::string Param(Params.GetValue("cacheenableaccesslog")); Param.empty() == false)
+ {
+ LoggingConfig.EnableAccessLog = StrCaseCompare(Param.c_str(), "true") == 0;
+ SetCacheLogConfig = true;
+ }
+ if (SetCacheLogConfig)
+ {
+ m_CacheStore->SetLoggingConfig(LoggingConfig);
+ StringBuilder.Append(fmt::format("cache write log: {}, cache access log: {}",
+ LoggingConfig.EnableWriteLog ? "true" : "false",
+ LoggingConfig.EnableAccessLog ? "true" : "false"));
+ }
+ }
+ if (std::string Param(Params.GetValue("loglevel")); Param.empty() == false)
+ {
+ logging::level::LogLevel NewLevel = logging::level::ParseLogLevelString(Param);
+ std::string_view LogLevel = logging::level::ToStringView(NewLevel);
+ if (LogLevel != Param)
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::BadRequest,
+ HttpContentType::kText,
+ fmt::format("Invalid log level '{}'", Param));
+ }
+ logging::SetLogLevel(NewLevel);
+ if (StringBuilder.Size() > 0)
+ {
+ StringBuilder.Append(", ");
+ }
+ StringBuilder.Append("loglevel: ");
+ StringBuilder.Append(Param);
+ }
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::OK, HttpContentType::kText, StringBuilder.ToView());
+ },
+ HttpVerb::kPost);
+ m_Router.RegisterRoute(
+ "flush",
+ [this](HttpRouterRequest& Req) {
+ HttpServerRequest& HttpReq = Req.ServerRequest();
+ m_FlushFunction();
+ HttpReq.WriteResponse(HttpResponseCode::OK);
+ },
+ HttpVerb::kPost);
+}
+
+HttpAdminService::~HttpAdminService()
+{
+}
+
+const char*
+HttpAdminService::BaseUri() const
+{
+ return "/admin/";
+}
+
+void
+HttpAdminService::HandleRequest(zen::HttpServerRequest& Request)
+{
+ m_Router.HandleRequest(Request);
+}
+
+} // namespace zen