aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/sessions/httpsessions.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-09 17:43:08 +0100
committerGitHub Enterprise <[email protected]>2026-03-09 17:43:08 +0100
commitb37b34ea6ad906f54e8104526e77ba66aed997da (patch)
treee80ce17d666aff6d2f0d73d4977128ffb4055476 /src/zenserver/sessions/httpsessions.cpp
parentadd fallback for zencache multirange (#816) (diff)
downloadzen-b37b34ea6ad906f54e8104526e77ba66aed997da.tar.xz
zen-b37b34ea6ad906f54e8104526e77ba66aed997da.zip
Dashboard overhaul, compute integration (#814)
- **Frontend dashboard overhaul**: Unified compute/main dashboards into a single shared UI. Added new pages for cache, projects, metrics, sessions, info (build/runtime config, system stats). Added live-update via WebSockets with pause control, sortable detail tables, themed styling. Refactored compute/hub/orchestrator pages into modular JS. - **HTTP server fixes and stats**: Fixed http.sys local-only fallback when default port is in use, implemented root endpoint redirect for http.sys, fixed Linux/Mac port reuse. Added /stats endpoint exposing HTTP server metrics (bytes transferred, request rates). Added WebSocket stats tracking. - **OTEL/diagnostics hardening**: Improved OTLP HTTP exporter with better error handling and resilience. Extended diagnostics services configuration. - **Session management**: Added new sessions service with HTTP endpoints for registering, updating, querying, and removing sessions. Includes session log file support. This is still WIP. - **CLI subcommand support**: Added support for commands with subcommands in the zen CLI tool, with improved command dispatch. - **Misc**: Exposed CPU usage/hostname to frontend, fixed JS compact binary float32/float64 decoding, limited projects displayed on front page to 25 sorted by last access, added vscode:// link support. Also contains some fixes from TSAN analysis.
Diffstat (limited to 'src/zenserver/sessions/httpsessions.cpp')
-rw-r--r--src/zenserver/sessions/httpsessions.cpp264
1 files changed, 264 insertions, 0 deletions
diff --git a/src/zenserver/sessions/httpsessions.cpp b/src/zenserver/sessions/httpsessions.cpp
new file mode 100644
index 000000000..05be3c814
--- /dev/null
+++ b/src/zenserver/sessions/httpsessions.cpp
@@ -0,0 +1,264 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "httpsessions.h"
+
+#include <zencore/compactbinarybuilder.h>
+#include <zencore/compactbinaryvalidation.h>
+#include <zencore/fmtutils.h>
+#include <zencore/logging.h>
+#include <zencore/trace.h>
+#include "sessions.h"
+
+namespace zen {
+using namespace std::literals;
+
+HttpSessionsService::HttpSessionsService(HttpStatusService& StatusService, HttpStatsService& StatsService, SessionsService& Sessions)
+: m_Log(logging::Get("sessions"))
+, m_StatusService(StatusService)
+, m_StatsService(StatsService)
+, m_Sessions(Sessions)
+{
+ Initialize();
+}
+
+HttpSessionsService::~HttpSessionsService()
+{
+ m_StatsService.UnregisterHandler("sessions", *this);
+ m_StatusService.UnregisterHandler("sessions", *this);
+}
+
+const char*
+HttpSessionsService::BaseUri() const
+{
+ return "/sessions/";
+}
+
+void
+HttpSessionsService::HandleRequest(HttpServerRequest& Request)
+{
+ metrics::OperationTiming::Scope $(m_HttpRequests);
+
+ if (m_Router.HandleRequest(Request) == false)
+ {
+ ZEN_WARN("No route found for {0}", Request.RelativeUri());
+ return Request.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, "Not found"sv);
+ }
+}
+
+CbObject
+HttpSessionsService::CollectStats()
+{
+ ZEN_TRACE_CPU("SessionsService::Stats");
+ CbObjectWriter Cbo;
+
+ EmitSnapshot("requests", m_HttpRequests, Cbo);
+
+ Cbo.BeginObject("sessions");
+ {
+ Cbo << "readcount" << m_SessionsStats.SessionReadCount;
+ Cbo << "writecount" << m_SessionsStats.SessionWriteCount;
+ Cbo << "deletecount" << m_SessionsStats.SessionDeleteCount;
+ Cbo << "listcount" << m_SessionsStats.SessionListCount;
+ Cbo << "requestcount" << m_SessionsStats.RequestCount;
+ Cbo << "badrequestcount" << m_SessionsStats.BadRequestCount;
+ Cbo << "count" << m_Sessions.GetSessionCount();
+ }
+ Cbo.EndObject();
+
+ return Cbo.Save();
+}
+
+void
+HttpSessionsService::HandleStatsRequest(HttpServerRequest& HttpReq)
+{
+ HttpReq.WriteResponse(HttpResponseCode::OK, CollectStats());
+}
+
+void
+HttpSessionsService::HandleStatusRequest(HttpServerRequest& Request)
+{
+ ZEN_TRACE_CPU("HttpSessionsService::Status");
+ CbObjectWriter Cbo;
+ Cbo << "ok" << true;
+ Request.WriteResponse(HttpResponseCode::OK, Cbo.Save());
+}
+
+void
+HttpSessionsService::Initialize()
+{
+ using namespace std::literals;
+
+ ZEN_INFO("Initializing Sessions Service");
+
+ static constexpr AsciiSet ValidHexCharactersSet{"0123456789abcdefABCDEF"};
+
+ m_Router.AddMatcher("session_id", [](std::string_view Str) -> bool {
+ return Str.length() == Oid::StringLength && AsciiSet::HasOnly(Str, ValidHexCharactersSet);
+ });
+
+ m_Router.RegisterRoute(
+ "list",
+ [this](HttpRouterRequest& Req) { ListSessionsRequest(Req); },
+ HttpVerb::kGet);
+
+ m_Router.RegisterRoute(
+ "{session_id}",
+ [this](HttpRouterRequest& Req) { SessionRequest(Req); },
+ HttpVerb::kGet | HttpVerb::kPost | HttpVerb::kPut | HttpVerb::kDelete);
+
+ m_Router.RegisterRoute(
+ "",
+ [this](HttpRouterRequest& Req) { ListSessionsRequest(Req); },
+ HttpVerb::kGet);
+
+ m_StatsService.RegisterHandler("sessions", *this);
+ m_StatusService.RegisterHandler("sessions", *this);
+}
+
+static void
+WriteSessionInfo(CbWriter& Writer, const SessionsService::SessionInfo& Info)
+{
+ Writer << "id" << Info.Id;
+ if (!Info.AppName.empty())
+ {
+ Writer << "appname" << Info.AppName;
+ }
+ if (Info.JobId != Oid::Zero)
+ {
+ Writer << "jobid" << Info.JobId;
+ }
+ Writer << "created_at" << Info.CreatedAt;
+ Writer << "updated_at" << Info.UpdatedAt;
+
+ if (Info.Metadata.GetSize() > 0)
+ {
+ Writer.BeginObject("metadata");
+ for (const CbField& Field : Info.Metadata)
+ {
+ Writer.AddField(Field);
+ }
+ Writer.EndObject();
+ }
+}
+
+void
+HttpSessionsService::ListSessionsRequest(HttpRouterRequest& Req)
+{
+ HttpServerRequest& ServerRequest = Req.ServerRequest();
+
+ m_SessionsStats.SessionListCount++;
+ m_SessionsStats.RequestCount++;
+
+ std::vector<Ref<SessionsService::Session>> Sessions = m_Sessions.GetSessions();
+
+ CbObjectWriter Response;
+ Response.BeginArray("sessions");
+ for (const Ref<SessionsService::Session>& Session : Sessions)
+ {
+ Response.BeginObject();
+ {
+ WriteSessionInfo(Response, Session->Info());
+ }
+ Response.EndObject();
+ }
+ Response.EndArray();
+
+ return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save());
+}
+
+void
+HttpSessionsService::SessionRequest(HttpRouterRequest& Req)
+{
+ HttpServerRequest& ServerRequest = Req.ServerRequest();
+
+ const Oid SessionId = Oid::TryFromHexString(Req.GetCapture(1));
+ if (SessionId == Oid::Zero)
+ {
+ m_SessionsStats.BadRequestCount++;
+ return ServerRequest.WriteResponse(HttpResponseCode::BadRequest,
+ HttpContentType::kText,
+ fmt::format("Invalid session id '{}'", Req.GetCapture(1)));
+ }
+
+ m_SessionsStats.RequestCount++;
+
+ switch (ServerRequest.RequestVerb())
+ {
+ case HttpVerb::kPost:
+ case HttpVerb::kPut:
+ {
+ IoBuffer Payload = ServerRequest.ReadPayload();
+ CbObject RequestObject;
+
+ if (Payload.GetSize() > 0)
+ {
+ if (CbValidateError ValidationResult = ValidateCompactBinary(Payload.GetView(), CbValidateMode::All);
+ ValidationResult != CbValidateError::None)
+ {
+ m_SessionsStats.BadRequestCount++;
+ return ServerRequest.WriteResponse(HttpResponseCode::BadRequest,
+ HttpContentType::kText,
+ fmt::format("Invalid payload: {}", zen::ToString(ValidationResult)));
+ }
+ RequestObject = LoadCompactBinaryObject(Payload);
+ }
+
+ if (ServerRequest.RequestVerb() == HttpVerb::kPost)
+ {
+ std::string AppName(RequestObject["appname"sv].AsString());
+ Oid JobId = RequestObject["jobid"sv].AsObjectId();
+ CbObjectView MetadataView = RequestObject["metadata"sv].AsObjectView();
+
+ m_SessionsStats.SessionWriteCount++;
+ if (m_Sessions.RegisterSession(SessionId, std::move(AppName), JobId, MetadataView))
+ {
+ return ServerRequest.WriteResponse(HttpResponseCode::Created, HttpContentType::kText, fmt::format("{}", SessionId));
+ }
+ else
+ {
+ // Already exists - try update instead
+ if (m_Sessions.UpdateSession(SessionId, MetadataView))
+ {
+ return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", SessionId));
+ }
+ return ServerRequest.WriteResponse(HttpResponseCode::InternalServerError);
+ }
+ }
+ else
+ {
+ // PUT - update only
+ m_SessionsStats.SessionWriteCount++;
+ if (m_Sessions.UpdateSession(SessionId, RequestObject["metadata"sv].AsObjectView()))
+ {
+ return ServerRequest.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("{}", SessionId));
+ }
+ return ServerRequest.WriteResponse(HttpResponseCode::NotFound,
+ HttpContentType::kText,
+ fmt::format("Session '{}' not found", SessionId));
+ }
+ }
+ case HttpVerb::kGet:
+ {
+ m_SessionsStats.SessionReadCount++;
+ Ref<SessionsService::Session> Session = m_Sessions.GetSession(SessionId);
+ if (Session)
+ {
+ CbObjectWriter Response;
+ WriteSessionInfo(Response, Session->Info());
+ return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save());
+ }
+ return ServerRequest.WriteResponse(HttpResponseCode::NotFound);
+ }
+ case HttpVerb::kDelete:
+ {
+ m_SessionsStats.SessionDeleteCount++;
+ if (m_Sessions.RemoveSession(SessionId))
+ {
+ return ServerRequest.WriteResponse(HttpResponseCode::OK);
+ }
+ return ServerRequest.WriteResponse(HttpResponseCode::NotFound);
+ }
+ }
+}
+
+} // namespace zen