Program Listing for File Server.h

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