Ui serving
This commit is contained in:
@@ -144,12 +144,13 @@ The app starts a local HTTP control server on `http://127.0.0.1:8080` by default
|
|||||||
|
|
||||||
Current endpoints:
|
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 /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/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
||||||
- `GET /docs`: serves Swagger UI
|
- `GET /docs`: serves Swagger UI
|
||||||
- OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
|
- 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
|
## Optional DeckLink Output
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsig
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
|
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
|
||||||
|
{
|
||||||
|
return FindRepoPath(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath)
|
||||||
{
|
{
|
||||||
std::vector<std::filesystem::path> starts;
|
std::vector<std::filesystem::path> starts;
|
||||||
starts.push_back(std::filesystem::current_path());
|
starts.push_back(std::filesystem::current_path());
|
||||||
|
|||||||
@@ -29,4 +29,5 @@ private:
|
|||||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
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 FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||||
|
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "AppConfig.h"
|
#include "AppConfig.h"
|
||||||
|
#include "AppConfigProvider.h"
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
#include "../runtime/RuntimeShaderBridge.h"
|
#include "../runtime/RuntimeShaderBridge.h"
|
||||||
#include "../control/RuntimeStateJson.h"
|
#include "../control/RuntimeStateJson.h"
|
||||||
@@ -171,7 +172,12 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::string error;
|
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);
|
LogWarning("http", "HTTP control server did not start: " + error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ HttpControlServer::~HttpControlServer()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool HttpControlServer::Start(
|
bool HttpControlServer::Start(
|
||||||
|
const std::filesystem::path& uiRoot,
|
||||||
const std::filesystem::path& docsRoot,
|
const std::filesystem::path& docsRoot,
|
||||||
HttpControlServerConfig config,
|
HttpControlServerConfig config,
|
||||||
HttpControlServerCallbacks callbacks,
|
HttpControlServerCallbacks callbacks,
|
||||||
@@ -96,6 +97,7 @@ bool HttpControlServer::Start(
|
|||||||
return false;
|
return false;
|
||||||
mWinsockStarted = true;
|
mWinsockStarted = true;
|
||||||
|
|
||||||
|
mUiRoot = uiRoot;
|
||||||
mDocsRoot = docsRoot;
|
mDocsRoot = docsRoot;
|
||||||
mConfig = config;
|
mConfig = config;
|
||||||
mCallbacks = std::move(callbacks);
|
mCallbacks = std::move(callbacks);
|
||||||
@@ -173,6 +175,12 @@ void HttpControlServer::SetCallbacksForTest(HttpControlServerCallbacks callbacks
|
|||||||
mCallbacks = std::move(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()
|
void HttpControlServer::ThreadMain()
|
||||||
{
|
{
|
||||||
while (mRunning.load(std::memory_order_acquire))
|
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/")
|
if (request.path == "/docs" || request.path == "/docs/")
|
||||||
return ServeSwaggerDocs();
|
return ServeSwaggerDocs();
|
||||||
if (request.path == "/" || request.path == "/index.html")
|
if (request.path == "/" || request.path == "/index.html")
|
||||||
return TextResponse("200 OK", "RenderCadenceCompositor control server");
|
return ServeUiAsset("index.html");
|
||||||
return TextResponse("404 Not Found", "Not Found");
|
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
|
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||||
@@ -279,6 +295,22 @@ HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
|||||||
return { "200 OK", "text/html", html.str() };
|
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::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||||
{
|
{
|
||||||
std::ifstream input(path, std::ios::binary);
|
std::ifstream input(path, std::ios::binary);
|
||||||
@@ -300,6 +332,11 @@ HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::strin
|
|||||||
return { status, "text/plain", body };
|
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)
|
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||||
{
|
{
|
||||||
JsonWriter writer;
|
JsonWriter writer;
|
||||||
@@ -318,9 +355,38 @@ std::string HttpControlServer::GuessContentType(const std::filesystem::path& pat
|
|||||||
return "application/yaml";
|
return "application/yaml";
|
||||||
if (extension == ".json")
|
if (extension == ".json")
|
||||||
return "application/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";
|
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::string HttpControlServer::ToLower(std::string text)
|
||||||
{
|
{
|
||||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ public:
|
|||||||
HttpControlServer& operator=(const HttpControlServer&) = delete;
|
HttpControlServer& operator=(const HttpControlServer&) = delete;
|
||||||
|
|
||||||
bool Start(
|
bool Start(
|
||||||
|
const std::filesystem::path& uiRoot,
|
||||||
const std::filesystem::path& docsRoot,
|
const std::filesystem::path& docsRoot,
|
||||||
HttpControlServerConfig config,
|
HttpControlServerConfig config,
|
||||||
HttpControlServerCallbacks callbacks,
|
HttpControlServerCallbacks callbacks,
|
||||||
@@ -80,6 +81,7 @@ public:
|
|||||||
unsigned short Port() const { return mPort; }
|
unsigned short Port() const { return mPort; }
|
||||||
|
|
||||||
void SetCallbacksForTest(HttpControlServerCallbacks callbacks);
|
void SetCallbacksForTest(HttpControlServerCallbacks callbacks);
|
||||||
|
void SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot);
|
||||||
HttpResponse RouteRequestForTest(const HttpRequest& request) const;
|
HttpResponse RouteRequestForTest(const HttpRequest& request) const;
|
||||||
|
|
||||||
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
||||||
@@ -94,14 +96,18 @@ private:
|
|||||||
HttpResponse ServePost(const HttpRequest& request) const;
|
HttpResponse ServePost(const HttpRequest& request) const;
|
||||||
HttpResponse ServeOpenApiSpec() const;
|
HttpResponse ServeOpenApiSpec() const;
|
||||||
HttpResponse ServeSwaggerDocs() const;
|
HttpResponse ServeSwaggerDocs() const;
|
||||||
|
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||||
std::string LoadTextFile(const std::filesystem::path& path) const;
|
std::string LoadTextFile(const std::filesystem::path& path) const;
|
||||||
|
|
||||||
static HttpResponse JsonResponse(const std::string& status, const std::string& body);
|
static HttpResponse JsonResponse(const std::string& status, const std::string& body);
|
||||||
static HttpResponse TextResponse(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 ActionResponse(bool ok, const std::string& error = std::string());
|
||||||
static std::string GuessContentType(const std::filesystem::path& path);
|
static std::string GuessContentType(const std::filesystem::path& path);
|
||||||
|
static bool IsSafeRelativePath(const std::filesystem::path& path);
|
||||||
static std::string ToLower(std::string text);
|
static std::string ToLower(std::string text);
|
||||||
|
|
||||||
|
std::filesystem::path mUiRoot;
|
||||||
std::filesystem::path mDocsRoot;
|
std::filesystem::path mDocsRoot;
|
||||||
HttpControlServerConfig mConfig;
|
HttpControlServerConfig mConfig;
|
||||||
HttpControlServerCallbacks mCallbacks;
|
HttpControlServerCallbacks mCallbacks;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "HttpControlServer.h"
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -61,6 +63,32 @@ void TestStateEndpointUsesCallback()
|
|||||||
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
|
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 << "<!doctype html><div id=\"root\"></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
void TestKnownPostEndpointReturnsActionError()
|
||||||
{
|
{
|
||||||
using namespace RenderCadenceCompositor;
|
using namespace RenderCadenceCompositor;
|
||||||
@@ -96,6 +124,7 @@ int main()
|
|||||||
{
|
{
|
||||||
TestParsesHttpRequest();
|
TestParsesHttpRequest();
|
||||||
TestStateEndpointUsesCallback();
|
TestStateEndpointUsesCallback();
|
||||||
|
TestRootServesUiIndex();
|
||||||
TestKnownPostEndpointReturnsActionError();
|
TestKnownPostEndpointReturnsActionError();
|
||||||
TestUnknownEndpointReturns404();
|
TestUnknownEndpointReturns404();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user