From b37b34ea6ad906f54e8104526e77ba66aed997da Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Mon, 9 Mar 2026 17:43:08 +0100 Subject: 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. --- src/zenserver/sessions/httpsessions.cpp | 264 ++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 src/zenserver/sessions/httpsessions.cpp (limited to 'src/zenserver/sessions/httpsessions.cpp') 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 +#include +#include +#include +#include +#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> Sessions = m_Sessions.GetSessions(); + + CbObjectWriter Response; + Response.BeginArray("sessions"); + for (const Ref& 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 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 -- cgit v1.2.3 From d0a07e555577dcd4a8f55f1b45d9e8e4e6366ab7 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Tue, 10 Mar 2026 17:27:26 +0100 Subject: HttpClient using libcurl, Unix Sockets for HTTP. HTTPS support (#770) The main goal of this change is to eliminate the cpr back-end altogether and replace it with the curl implementation. I would expect to drop cpr as soon as we feel happy with the libcurl back-end. That would leave us with a direct dependency on libcurl only, and cpr can be eliminated as a dependency. ### HttpClient Backend Overhaul - Implemented a new **libcurl-based HttpClient** backend (`httpclientcurl.cpp`, ~2000 lines) as an alternative to the cpr-based one - Made HttpClient backend **configurable at runtime** via constructor arguments and `-httpclient=...` CLI option (for zen, zenserver, and tests) - Extended HttpClient test suite to cover multipart/content-range scenarios ### Unix Domain Socket Support - Added Unix domain socket support to **httpasio** (server side) - Added Unix domain socket support to **HttpClient** - Added Unix domain socket support to **HttpWsClient** (WebSocket client) - Templatized `HttpServerConnectionT` and `WsAsioConnectionT` to handle TCP, Unix, and SSL sockets uniformly via `if constexpr` dispatch ### HTTPS Support - Added **preliminary HTTPS support to httpasio** (for Mac/Linux via OpenSSL) - Added **basic HTTPS support for http.sys** (Windows) - Implemented HTTPS test for httpasio - Split `InitializeServer` into smaller sub-functions for http.sys ### Other Notable Changes - Improved **zenhttp-test stability** with dynamic port allocation - Enhanced port retry logic in http.sys (handles ERROR_ACCESS_DENIED) - Fatal signal/exception handlers for backtrace generation in tests - Added `zen bench http` subcommand to exercise network + HTTP client/server communication stack --- src/zenserver/sessions/httpsessions.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/zenserver/sessions/httpsessions.cpp') diff --git a/src/zenserver/sessions/httpsessions.cpp b/src/zenserver/sessions/httpsessions.cpp index 05be3c814..6cf12bea4 100644 --- a/src/zenserver/sessions/httpsessions.cpp +++ b/src/zenserver/sessions/httpsessions.cpp @@ -258,6 +258,10 @@ HttpSessionsService::SessionRequest(HttpRouterRequest& Req) } return ServerRequest.WriteResponse(HttpResponseCode::NotFound); } + default: + { + return ServerRequest.WriteResponse(HttpResponseCode::MethodNotAllowed); + } } } -- cgit v1.2.3