Program Listing for File Server.h#
↰ Return to documentation for file (include/Karana/WebUI/Server.h)
/*
* Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
*
* NOTICE TO USER:
*
* This source code and/or documentation (the "Licensed Materials") is
* the confidential and proprietary information of Karana Dynamics Inc.
* Use of these Licensed Materials is governed by the terms and conditions
* of a separate software license agreement between Karana Dynamics and the
* Licensee ("License Agreement"). Unless expressly permitted under that
* agreement, any reproduction, modification, distribution, or disclosure
* of the Licensed Materials, in whole or in part, to any third party
* without the prior written consent of Karana Dynamics is strictly prohibited.
*
* THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
* KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
* FITNESS FOR A PARTICULAR PURPOSE.
*
* IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
* INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
* OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
*
* U.S. Government End Users: The Licensed Materials are a "commercial item"
* as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
* only as a commercial end item under the terms of this license.
*
* Any use of the Licensed Materials in individual or commercial software must
* include, in the user documentation and internal source code comments,
* this Notice, Disclaimer, and U.S. Government Use Provision.
*/
/**
* @file
* @brief Contains the declarations for the Connection and Server classes.
*/
#pragma once
#include <condition_variable>
#include <cstdlib>
#include <filesystem>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <thread>
#include <type_traits>
#include <boost/asio.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
namespace Karana::WebUI {
/// Parameters when launching a local client
struct LocalClientOptions {
/// Suppress output from the local client
bool silent = true;
/// Enable electron sandboxing
bool sandbox = false;
};
/// Class to manage a single client connection
class Connection {
public:
/// Type alias for the low level stream
using conn_t = boost::beast::websocket::stream<boost::beast::tcp_stream>;
/// Type alias for a basic callback
using handler_t = std::function<void(Connection &)>;
/// Type alias for a messenger handler callback
using msg_handler_t = std::function<void(Connection &, const uint8_t *data, size_t size)>;
/**
* @brief Connection constructor
* @param conn - Low level stream
* @param on_message - Message handler callback
* @param on_close - On close callback
*/
Connection(std::unique_ptr<conn_t> conn, msg_handler_t on_message, handler_t on_close);
~Connection();
/**
* @brief Send a message to the client
* @param data - The message buffer to send.
* @param len - Number of bytes to send.
*/
void send(const uint8_t *data, size_t len);
/**
* @brief Set the message handler
* @param on_message - The message handler
*/
void onMessage(msg_handler_t on_message);
/**
* @brief Set the close handler
* @param on_close - The close handler
*/
void onClose(handler_t on_close);
private:
void _doRead();
std::unique_ptr<conn_t> _conn;
msg_handler_t _on_message;
handler_t _on_close;
boost::beast::flat_buffer _buffer;
};
/**
* @class Server
* @brief Multi-client http/websocket server
*
* See \sref{webui_sec} for more discussion on WebUI.
*/
class Server {
public:
/**
* @brief Server constructor.
* @param port - the port to bind to.
*/
Server(unsigned short port = 29523);
~Server();
/**
* @brief Execute the callable on the io thread
* @param callable - The callback function
* @return A future that resolves to the callback's return value
*/
template <class T, class R = typename std::invoke_result<T>::type>
std::future<R> callAsync(const T &callable) {
std::promise<R> promise;
std::future<R> future = promise.get_future();
boost::asio::post(_io_context, [promise = std::move(promise), callable]() mutable {
if constexpr (std::is_void_v<R>) {
callable();
promise.set_value();
} else {
promise.set_value(callable());
}
});
return future;
}
/**
* @brief Execute the callable on the io thread, blocking until done
* @param callable - The callback function
* @return The callback's return value
*/
template <class T, class R = typename std::invoke_result<T>::type>
R callSync(const T &callable) {
std::promise<R> promise;
std::future<R> future = promise.get_future();
boost::asio::post(_io_context, [promise = std::move(promise), callable]() mutable {
if constexpr (std::is_void_v<R>) {
callable();
promise.set_value();
} else {
promise.set_value(callable());
}
});
if constexpr (std::is_void_v<R>) {
future.get();
return;
} else {
return future.get();
}
}
/// Wait for currently queued tasks to complete
void sync();
/**
* @brief Generate a URL to connect to the sever.
*
* Note that this URL is a best guess from the point of view of the
* server. It may not reflect the publicly accessible URL for the
* server.
*
* @return The URL guess
*/
std::string guessUrl() const;
/**
* @brief Get the port the server is bound to.
* @return The port number.
*/
unsigned short port() const;
/**
* @brief Check whether the current thread is the io thread
* @return Whether the current thread is the io thread
*/
bool onThread();
/**
* @brief Send the data buffer to all clients
*
* This sends the data asynchronously and should only be called from
* the io thread, e.g., via callAsync
*
* @param data - The data to send
* @param len - Number of bytes to send
*/
void broadcast(const uint8_t *data, size_t len);
/**
* @brief Send the data buffer to one client
*
* This sends the data asynchronously and should only be called from
* the io thread, e.g., via callAsync
*
* @param data - The data to send
* @param len - Number of bytes to send
*/
void sendOne(const uint8_t *data, size_t len);
/**
* @brief Send a handler to be called on new connections.
* @param on_connect - The callback.
*/
void onConnect(Connection::handler_t on_connect);
/**
* @brief Send a handler to be called on incoming messages.
* @param on_message - The callback.
*/
void onMessage(Connection::msg_handler_t on_message);
/**
* @brief Send a handler to be called on when a connection closes.
* @param on_close - The callback.
*/
void onClose(Connection::handler_t on_close);
/**
* @brief Wait for a given number of clients to connect.
* @param clients - Number of clients to wait for.
* @param timeout - Timeout in seconds, if positive
*/
void waitForClients(int clients = 1, float timeout = 0);
// Launch an instance of the electron frontend as a
// subprocess and automatically connect. The frontend's
// lifetime will be managed by the server.
/**
* @brief Spawn a managed electron client as a subprocess.
* @param options - Parameters struct for the local client.
*/
void launchLocalClient(const LocalClientOptions &options = LocalClientOptions{});
/**
* @brief Check if the client executable can be found....
* @returns Whether the client executable can be found
*/
static bool canFindLocalClientExecutable();
/**
* @brief Close all managed clients.
*/
void closeLocalClients();
/**
* @brief Print info to stdout about how to connect
*/
void printConnectionInfo();
private:
// Helper to get the contexts of a text file as a string
std::string _loadText(const std::string &path);
// Helper to handle a request by serving a static file
void _serveFile(boost::beast::tcp_stream &stream,
const boost::beast::http::request<boost::beast::http::string_body> &req,
const std::string &path,
const std::string &content_type);
// Helper to simply send a 200 ok response
void _sendOk(boost::beast::tcp_stream &stream,
const boost::beast::http::request<boost::beast::http::string_body> &req);
// Helper to handle a request by sending a bad request response
void _badRequest(boost::beast::tcp_stream &stream,
const boost::beast::http::request<boost::beast::http::string_body> &req);
// Helper to handle a request by upgrading to a persistent websocket connection
void
_upgradeWebsocket(boost::beast::tcp_stream &stream,
const boost::beast::http::request<boost::beast::http::string_body> &req);
// Helper to handle http requests
void _handleRequest(boost::asio::ip::tcp::socket socket);
// Helper to asynchronously accept http requests
void _doAccept();
private:
boost::asio::io_context _io_context;
Connection::handler_t _on_connect;
Connection::msg_handler_t _on_message;
Connection::handler_t _on_close;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> _work_guard;
boost::asio::ip::tcp::acceptor _acceptor;
const unsigned short _port;
std::thread _thread;
std::vector<std::unique_ptr<Connection>> _connections;
std::filesystem::path _base_path;
std::unique_ptr<class ClientManager> _client_manager;
std::mutex _client_count_mutex;
std::condition_variable _client_count_cv;
int _client_count = 0;
};
} // namespace Karana::WebUI