From b44504500ac9c3de4cb79238294aba92f5b3b08a Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 13:25:34 +1000 Subject: [PATCH] Ui serving --- apps/RenderCadenceCompositor/README.md | 3 +- .../app/AppConfigProvider.cpp | 5 ++ .../app/AppConfigProvider.h | 1 + .../app/RenderCadenceApp.h | 8 ++- .../control/HttpControlServer.cpp | 70 ++++++++++++++++++- .../control/HttpControlServer.h | 6 ++ ...adenceCompositorHttpControlServerTests.cpp | 29 ++++++++ 7 files changed, 118 insertions(+), 4 deletions(-) diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 17f552c..146ef67 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -144,12 +144,13 @@ The app starts a local HTTP control server on `http://127.0.0.1:8080` by default Current endpoints: +- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist` - `GET /api/state`: returns an OpenAPI-shaped state scaffold with cadence telemetry under `performance.cadence` - `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document - `GET /docs`: serves Swagger UI - OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }` -The HTTP server runs on its own thread. It samples/copies telemetry through callbacks and does not call render work or DeckLink scheduling. +The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and does not call render work or DeckLink scheduling. ## Optional DeckLink Output diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp index 3293841..e859c70 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp @@ -207,6 +207,11 @@ void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsig } std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath) +{ + return FindRepoPath(relativePath); +} + +std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath) { std::vector starts; starts.push_back(std::filesystem::current_path()); diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.h b/apps/RenderCadenceCompositor/app/AppConfigProvider.h index 9fc2a0f..5477b7d 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.h +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.h @@ -29,4 +29,5 @@ private: double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94); void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height); std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json"); +std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath); } diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 251fcb3..6b7a159 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -1,6 +1,7 @@ #pragma once #include "AppConfig.h" +#include "AppConfigProvider.h" #include "../logging/Logger.h" #include "../runtime/RuntimeShaderBridge.h" #include "../control/RuntimeStateJson.h" @@ -171,7 +172,12 @@ private: }; std::string error; - if (!mHttpServer.Start(std::filesystem::current_path() / "docs", mConfig.http, callbacks, error)) + if (!mHttpServer.Start( + FindRepoPath("ui/dist"), + FindRepoPath("docs"), + mConfig.http, + callbacks, + error)) { LogWarning("http", "HTTP control server did not start: " + error); return; diff --git a/apps/RenderCadenceCompositor/control/HttpControlServer.cpp b/apps/RenderCadenceCompositor/control/HttpControlServer.cpp index 1706118..5d5fa20 100644 --- a/apps/RenderCadenceCompositor/control/HttpControlServer.cpp +++ b/apps/RenderCadenceCompositor/control/HttpControlServer.cpp @@ -85,6 +85,7 @@ HttpControlServer::~HttpControlServer() } bool HttpControlServer::Start( + const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, HttpControlServerConfig config, HttpControlServerCallbacks callbacks, @@ -96,6 +97,7 @@ bool HttpControlServer::Start( return false; mWinsockStarted = true; + mUiRoot = uiRoot; mDocsRoot = docsRoot; mConfig = config; mCallbacks = std::move(callbacks); @@ -173,6 +175,12 @@ void HttpControlServer::SetCallbacksForTest(HttpControlServerCallbacks callbacks mCallbacks = std::move(callbacks); } +void HttpControlServer::SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot) +{ + mUiRoot = uiRoot; + mDocsRoot = docsRoot; +} + void HttpControlServer::ThreadMain() { while (mRunning.load(std::memory_order_acquire)) @@ -241,8 +249,16 @@ HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& r if (request.path == "/docs" || request.path == "/docs/") return ServeSwaggerDocs(); if (request.path == "/" || request.path == "/index.html") - return TextResponse("200 OK", "RenderCadenceCompositor control server"); - return TextResponse("404 Not Found", "Not Found"); + return ServeUiAsset("index.html"); + if (request.path.rfind("/assets/", 0) == 0) + return ServeUiAsset(request.path.substr(1)); + if (request.path.size() > 1) + { + const HttpResponse asset = ServeUiAsset(request.path.substr(1)); + if (asset.status != "404 Not Found") + return asset; + } + return ServeUiAsset("index.html"); } HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const @@ -279,6 +295,22 @@ HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const return { "200 OK", "text/html", html.str() }; } +HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const +{ + if (mUiRoot.empty()) + return TextResponse("404 Not Found", "UI root is not configured"); + + const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal(); + if (!IsSafeRelativePath(sanitizedPath)) + return TextResponse("404 Not Found", "Not Found"); + + const std::filesystem::path path = mUiRoot / sanitizedPath; + const std::string body = LoadTextFile(path); + if (body.empty()) + return TextResponse("404 Not Found", "Not Found"); + return { "200 OK", GuessContentType(path), body }; +} + std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const { std::ifstream input(path, std::ios::binary); @@ -300,6 +332,11 @@ HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::strin return { status, "text/plain", body }; } +HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body) +{ + return { status, "text/html", body }; +} + std::string HttpControlServer::ActionResponse(bool ok, const std::string& error) { JsonWriter writer; @@ -318,9 +355,38 @@ std::string HttpControlServer::GuessContentType(const std::filesystem::path& pat return "application/yaml"; if (extension == ".json") return "application/json"; + if (extension == ".js" || extension == ".mjs") + return "text/javascript"; + if (extension == ".css") + return "text/css"; + if (extension == ".html" || extension == ".htm") + return "text/html"; + if (extension == ".svg") + return "image/svg+xml"; + if (extension == ".png") + return "image/png"; + if (extension == ".jpg" || extension == ".jpeg") + return "image/jpeg"; + if (extension == ".ico") + return "image/x-icon"; + if (extension == ".map") + return "application/json"; return "text/plain"; } +bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path) +{ + if (path.empty() || path.is_absolute()) + return false; + + for (const std::filesystem::path& part : path) + { + if (part == "..") + return false; + } + return true; +} + std::string HttpControlServer::ToLower(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) { diff --git a/apps/RenderCadenceCompositor/control/HttpControlServer.h b/apps/RenderCadenceCompositor/control/HttpControlServer.h index 9275aff..d816751 100644 --- a/apps/RenderCadenceCompositor/control/HttpControlServer.h +++ b/apps/RenderCadenceCompositor/control/HttpControlServer.h @@ -70,6 +70,7 @@ public: HttpControlServer& operator=(const HttpControlServer&) = delete; bool Start( + const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, HttpControlServerConfig config, HttpControlServerCallbacks callbacks, @@ -80,6 +81,7 @@ public: unsigned short Port() const { return mPort; } void SetCallbacksForTest(HttpControlServerCallbacks callbacks); + void SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot); HttpResponse RouteRequestForTest(const HttpRequest& request) const; static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request); @@ -94,14 +96,18 @@ private: HttpResponse ServePost(const HttpRequest& request) const; HttpResponse ServeOpenApiSpec() const; HttpResponse ServeSwaggerDocs() const; + HttpResponse ServeUiAsset(const std::string& relativePath) const; std::string LoadTextFile(const std::filesystem::path& path) const; static HttpResponse JsonResponse(const std::string& status, const std::string& body); static HttpResponse TextResponse(const std::string& status, const std::string& body); + static HttpResponse HtmlResponse(const std::string& status, const std::string& body); static std::string ActionResponse(bool ok, const std::string& error = std::string()); static std::string GuessContentType(const std::filesystem::path& path); + static bool IsSafeRelativePath(const std::filesystem::path& path); static std::string ToLower(std::string text); + std::filesystem::path mUiRoot; std::filesystem::path mDocsRoot; HttpControlServerConfig mConfig; HttpControlServerCallbacks mCallbacks; diff --git a/tests/RenderCadenceCompositorHttpControlServerTests.cpp b/tests/RenderCadenceCompositorHttpControlServerTests.cpp index 78a4478..33bead0 100644 --- a/tests/RenderCadenceCompositorHttpControlServerTests.cpp +++ b/tests/RenderCadenceCompositorHttpControlServerTests.cpp @@ -1,5 +1,7 @@ #include "HttpControlServer.h" +#include +#include #include #include @@ -61,6 +63,32 @@ void TestStateEndpointUsesCallback() ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON"); } +void TestRootServesUiIndex() +{ + using namespace RenderCadenceCompositor; + + const std::filesystem::path root = std::filesystem::temp_directory_path() / "render-cadence-compositor-ui-test"; + std::filesystem::create_directories(root); + { + std::ofstream output(root / "index.html", std::ios::binary); + output << "
"; + } + + HttpControlServer server; + HttpControlServer::HttpRequest request; + request.method = "GET"; + request.path = "/"; + + server.SetRootsForTest(root, std::filesystem::path()); + const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + + ExpectEquals(response.status, "200 OK", "root endpoint serves UI index"); + ExpectEquals(response.contentType, "text/html", "UI index content type is html"); + Expect(response.body.find("root") != std::string::npos, "UI index body is returned"); + + std::filesystem::remove_all(root); +} + void TestKnownPostEndpointReturnsActionError() { using namespace RenderCadenceCompositor; @@ -96,6 +124,7 @@ int main() { TestParsesHttpRequest(); TestStateEndpointUsesCallback(); + TestRootServesUiIndex(); TestKnownPostEndpointReturnsActionError(); TestUnknownEndpointReturns404();