/** * @file IPC.h * @author apio (cloudapio.eu) * @brief Inter-process communication primitives. * * @copyright Copyright (c) 2023, the Luna authors. * */ #pragma once #include #include #include #define IPC_ENUM_SERVER(name) __##name##_SERVER_ERROR = 0 #define IPC_ENUM_CLIENT(name) __##name##_CLIENT_ERROR = 0 namespace os { namespace IPC { static constexpr usize IPC_STRING_LENGTH = 256; #define IPC_STRING(name) char name[os::IPC::IPC_STRING_LENGTH]; #define COPY_IPC_STRING(name) \ TRY(String::from_string_view(StringView::from_fixed_size_cstring(name, os::IPC::IPC_STRING_LENGTH))) #define SET_IPC_STRING(name, value) strlcpy(name, value, os::IPC::IPC_STRING_LENGTH) /** * @brief Class used to send and receive IPC messages on the server, using an already established connection to * the client. */ class ClientConnection { public: /** * @brief Creates a new IPC connection object from an os::LocalServer connection. * * @param connection The existing connection to use, obtained after calling accept() on an os::LocalServer * object. * @return Result> An error, or the IPC connection object. */ static Result> adopt_connection(LocalServer::Client&& connection); /** * @brief Check for new messages from the client. The message handler will be called if there is a new * message. If the connection is non-blocking, this function returns EAGAIN if there are no new * messages, otherwise it blocks until one is received. * * @return Result Whether the operation succeeded. */ Result check_for_messages(); /** * @brief Set the message handler for this connection. This function is required and will be called every * time a new message is received, passing the connection object and the ID of the message type. * * @param handler The message handler to use. * @param arg An arbitrary argument to pass to the handler. */ void set_message_handler(Function&& handler, void* arg) { m_message_handler = move(handler); m_arg = arg; } /** * @brief Read message data from the connection. This function should be called only after the other side of * the connection has signaled that it is going to send a message of the specified type, for example inside * the message handler. * * @tparam T The type of the message to read. * @param out The variable in which to store the message. * @return Result An error, or whether the message was actually read and stored. If this value is * false, this function will have to be called again, either because the connection is non-blocking and the * message data has not been sent yet or because a signal interrupted the call. */ template Result read_message(T& out) { auto rc = m_connection.recv_typed(out); if (rc.has_error()) { if (rc.error() == EAGAIN) { m_ipc_in_progress = true; m_ipc_saved_id = T::ID; return false; } if (rc.error() == EINTR) { m_ipc_in_progress = true; m_ipc_saved_id = T::ID; return false; } else return rc.release_error(); } return true; } /** * @brief Sends an error code, indicating that an operation could not be performed. It is best to send an * error back only when the client is expecting it, that is to say, when they are waiting for a reply to a * synchronous message. * * @param error The error code. * @return Result Whether the operation succeded. */ Result send_error(int error); /** * @brief Sends a message without waiting for a reply. * * @tparam T The type of the message. * @param message The message to send. * @return Result Whether the operation succeeded. */ template Result send_async(const T& message) { u8 id = T::ID; TRY(m_connection.send_typed(id)); TRY(m_connection.send_typed(message)); return {}; } /** * @brief Closes the connection. */ void disconnect() { m_connection.disconnect(); } /** * @brief Returns the file descriptor associated with this connection. * * @return int The file descriptor. */ int fd() { return m_connection.fd(); } private: ClientConnection(LocalServer::Client&& connection); LocalServer::Client m_connection; Function m_message_handler; void* m_arg; bool m_ipc_in_progress { false }; u8 m_ipc_saved_id { 0 }; }; /** * @brief Class used to send and receive IPC messages on the client. */ class Client { public: /** * @brief Connect to an IPC server and return a connection object. * * @param path The path of the socket used by the IPC server. * @param blocking Whether the connection should block when waiting for messages. * @return Result> An error, or a new connection object. */ static Result> connect(StringView path, bool blocking); /** * @brief Check for new messages from the server. The message handler will be called if there is a new * message. If the connection is non-blocking, this function returns EAGAIN if there are no new * messages, otherwise it blocks until one is received. * * @return Result Whether the operation succeeded. */ Result check_for_messages(); /** * @brief Set the message handler for this connection. This function is required and will be called every * time a new message is received, passing the connection object and the ID of the message type. * * @param handler The message handler to use. * @param arg An arbitrary argument to pass to the handler. */ void set_message_handler(Function&& handler, void* arg) { m_message_handler = move(handler); m_arg = arg; } /** * @brief Read message data from the connection. This function should be called only after the other side of * the connection has signaled that it is going to send a message of the specified type, for example inside * the message handler. * * @tparam T The type of the message to read. * @param out The variable in which to store the message. * @return Result An error, or whether the message was actually read and stored. If this value is * false, this function will have to be called again, either because the connection is non-blocking and the * message data has not been sent yet or because a signal interrupted the call. */ template Result read_message(T& out) { auto rc = m_connection->recv_typed(out); if (rc.has_error()) { if (rc.error() == EAGAIN) { m_ipc_in_progress = true; m_ipc_saved_id = T::ID; return false; } if (rc.error() == EINTR) { m_ipc_in_progress = true; m_ipc_saved_id = T::ID; return false; } else return rc.release_error(); } return true; } /** * @brief Sends a message without waiting for a reply. * * @tparam T The type of the message. * @param message The message to send. * @return Result Whether the operation succeeded. */ template Result send_async(const T& message) { u8 id = T::ID; TRY(m_connection->send_typed(id)); TRY(m_connection->send_typed(message)); return {}; } /** * @brief Sends a message and waits for a reply. * * @tparam ResponseType The type of the response. * @tparam T The type of the message. * @param message The message to send. * @return Result An error, or the response. */ template Result send_sync(const T& message) { u8 id = T::ID; TRY(m_connection->send_typed(id)); TRY(m_connection->send_typed(message)); // We allow receiving 5 messages of different types, but if those have passed and we still don't have a // reply, fail with ENOMSG. int max_other_messages = 5; while (max_other_messages) { u8 response_id; auto rc = m_connection->recv_typed(response_id); if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue; if (response_id == 0) // Error result { while (1) { int code; rc = m_connection->recv_typed(code); if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue; return err(code); } } if (response_id != ResponseType::ID) { m_message_handler(*this, response_id, m_arg); max_other_messages--; continue; } while (1) { ResponseType response; rc = m_connection->recv_typed(response); if (rc.has_error() && (rc.error() == EAGAIN || rc.error() == EINTR)) continue; return response; } } return err(ENOMSG); } /** * @brief Closes the connection. */ void disconnect() { m_connection->disconnect(); } /** * @brief Returns the file descriptor associated with this connection. * * @return int The file descriptor. */ int fd() { return m_connection->fd(); } private: Client(OwnedPtr&& connection); OwnedPtr m_connection; Function m_message_handler; void* m_arg; bool m_ipc_in_progress { false }; u8 m_ipc_saved_id { 0 }; }; } }