diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bfeab2..b27bce9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(msocket LANGUAGES C CXX VERSION 2.0.0) +project(msocket LANGUAGES C CXX VERSION 2.0.1) set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) set(CMAKE_DISABLE_SOURCE_CHANGES ON) diff --git a/README.md b/README.md index 346f0e9..c231c34 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ ctest --preset clang-asan # ThreadSanitizer (TSan) cmake --preset clang-tsan cmake --build --preset clang-tsan -ctest --preset clang-tsan +ctest --preset clang-tsan -V # Static Analysis cmake --preset clang-tidy diff --git a/docs/Doxyfile b/docs/Doxyfile index caec118..683b26c 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -1,6 +1,6 @@ # Doxyfile for Sphinx + Breathe integration PROJECT_NAME = "msocket" -PROJECT_NUMBER = "2.0.0" +PROJECT_NUMBER = "2.0.1" PROJECT_BRIEF = "Event-driven socket library for Linux and Windows" # Input configuration diff --git a/docs/conf.py b/docs/conf.py index bae3ad7..bfa55e3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ project = 'msocket' copyright = '2026, Conny Gustafsson' author = 'Conny Gustafsson' -release = '2.0.0' +release = '2.0.1' # -- General configuration --------------------------------------------------- diff --git a/docs/msocket_server.rst b/docs/msocket_server.rst index a0c46af..5ad9e2b 100644 --- a/docs/msocket_server.rst +++ b/docs/msocket_server.rst @@ -6,15 +6,41 @@ The ``msocket_server`` module manages listening sockets and handles accepted cli Architecture & Acceptance Workflow ----------------------------------- -1. Initialize the server using :cpp:func:`msocket_server_create` or :cpp:func:`msocket_server_new`. +The server operates in one of two modes depending on the destructor passed to :cpp:func:`msocket_server_create` or :cpp:func:`msocket_server_new`: + +Default Auto-Reap Mode +~~~~~~~~~~~~~~~~~~~~~~ + +When initialized with ``NULL`` or :cpp:func:`msocket_vdelete` as the destructor, accepted sockets are managed automatically: + +1. Initialize the server using :cpp:func:`msocket_server_create` (e.g. ``msocket_server_create(srv, family, NULL)``) or :cpp:func:`msocket_server_new` (e.g. ``msocket_server_new(family, NULL)``). 2. Register an accept handler callback table with :cpp:func:`msocket_server_set_handler`. Inside the ``stream_accept`` callback (see :doc:`Socket Event Handler `): - - Configure per-connection event handlers on the child socket using :cpp:func:`msocket_set_handler`. - - Attach the child socket to the server using :cpp:func:`msocket_set_server(child_socket, srv) ` for automatic cleanup upon disconnect. + - Attach per-connection event handlers to the child socket using :cpp:func:`msocket_set_handler`. - Start background I/O on the child connection by calling :cpp:func:`msocket_start_io(child_socket) `. -3. Start the server on the desired TCP/UDP port using :cpp:func:`msocket_server_start` or on a UNIX domain socket using :cpp:func:`msocket_server_unix_start`. -4. When stopping, destroy the server with :cpp:func:`msocket_server_destroy` or :cpp:func:`msocket_server_delete`, which stops all listening threads and cleans up reaped client resources. + - *Note:* Accepted child sockets are automatically cleaned up by the server upon disconnect. When the remote client disconnects, the socket's background thread automatically enqueues the raw socket to the cleanup thread for asynchronous deletion. + +3. Start listening with :cpp:func:`msocket_server_start` (TCP/UDP) or :cpp:func:`msocket_server_unix_start` (UNIX domain). +4. When stopping, destroy the server with :cpp:func:`msocket_server_destroy` or :cpp:func:`msocket_server_delete`. + +Custom Wrapper Mode +~~~~~~~~~~~~~~~~~~~ + +When wrapping client sockets inside application-level connection or session structures (as in APX): + +1. Initialize the server with a custom destructor function: :cpp:func:`msocket_server_new` (e.g. ``msocket_server_new(family, on_custom_conn_delete)``). +2. Inside ``stream_accept``: + + - Allocate your wrapper structure and wrap the accepted ``child_socket``. + - Configure per-connection event handlers on ``child_socket``, passing the wrapper as callback context. + - Start background I/O on ``child_socket`` using :cpp:func:`msocket_start_io`. + - *Note:* Accepted child sockets are not automatically cleaned up by the server in this mode, preventing raw socket pointers from being passed to the custom destructor. + +3. Inside the ``stream_disconnected`` handler of your wrapper: + + - Enqueue the wrapper structure for deletion by calling :cpp:func:`msocket_server_cleanup_connection` (e.g. ``msocket_server_cleanup_connection(srv, wrapper)``). + - The server cleanup thread invokes your custom destructor, safely deleting both the wrapper and the inner socket. API Reference ------------- diff --git a/docs/socket_handler.rst b/docs/socket_handler.rst index 55fd17f..cd0f79c 100644 --- a/docs/socket_handler.rst +++ b/docs/socket_handler.rst @@ -68,8 +68,12 @@ Invoked on the server when a remote client initiates a new connection. **Implementation Contract:** Configure the child socket before starting I/O: 1. Attach per-client callbacks and context: ``msocket_set_handler(child, &client_handler, client_ctx);`` -2. Bind to server for automatic lifecycle cleanup: ``msocket_set_server(child, srv);`` -3. Start background I/O: ``msocket_start_io(child);`` +2. Start background I/O: ``msocket_start_io(child);`` + +*Connection Lifecycle Cleanup:* + +* Under **Default Auto-Reap Mode** (server created with ``NULL`` or :cpp:func:`msocket_vdelete`), the server accept task automatically associates the child socket for background cleanup upon disconnect. Manual binding is not required. +* Under **Custom Wrapper Mode** (server created with a custom destructor), wrap the socket in your application connection object, listen for ``stream_disconnected``, and call :cpp:func:`msocket_server_cleanup_connection` (e.g. ``msocket_server_cleanup_connection(srv, wrapper)``). stream_connected ~~~~~~~~~~~~~~~~ diff --git a/examples/echo/echo_server.c b/examples/echo/echo_server.c index 4b53928..7de9c3f 100644 --- a/examples/echo/echo_server.c +++ b/examples/echo/echo_server.c @@ -65,6 +65,11 @@ static void on_client_disconnected(void *arg, void *socket) { (void)arg; (void)socket; + /* + * Note: In default auto-reap mode, the disconnected child socket is automatically + * reaped and deleted by the server's background cleanup thread. Do not call + * msocket_delete here. + */ printf("[SERVER] Client disconnected\n"); } @@ -121,7 +126,12 @@ int main(int argc, char *argv[]) printf("[SERVER] Starting Echo Server on port %u (press Ctrl+C to exit)...\n", port); - msocket_server_t *server = msocket_server_new(MSOCKET_ADDR_INET, msocket_vdelete); + /* + * Initialize server with default auto-reap mode (NULL defaults to msocket_vdelete). + * In this mode, accepted child sockets are automatically bound to the server and + * reaped by the server cleanup thread upon disconnection. + */ + msocket_server_t *server = msocket_server_new(MSOCKET_ADDR_INET, NULL); if (server == NULL) { fprintf(stderr, "[SERVER] Failed to allocate msocket_server\n"); return 1; diff --git a/include/msocket.h b/include/msocket.h index 9c987f5..6382783 100644 --- a/include/msocket.h +++ b/include/msocket.h @@ -242,6 +242,10 @@ void msocket_set_handler(msocket_t *self, const msocket_handler_t *handler_table /** * Associates a parent msocket_server instance with this socket for automatic reaping upon disconnection. * + * NOTE: For servers created with default destructor (NULL or msocket_vdelete), accepted child + * sockets are associated with the server automatically by the accept task. Manual association + * is only needed when explicitly opting into automatic raw socket reaping under custom setups. + * * @param self Pointer to msocket_t instance. * @param server Pointer to parent msocket_server instance, or NULL to detach. */ diff --git a/include/msocket_server.h b/include/msocket_server.h index 86abbb1..55d6fb8 100644 --- a/include/msocket_server.h +++ b/include/msocket_server.h @@ -49,6 +49,14 @@ typedef struct msocket_server_tag { /** * Initializes an existing msocket_server instance. * + * The server supports two connection cleanup modes: + * - Default Auto-Reap Mode (destructor == NULL or msocket_vdelete): Accepted child sockets + * are automatically cleaned up by the server. When disconnected, the socket's background I/O + * thread automatically queues the raw socket for deletion via msocket_vdelete. + * - Custom Wrapper Mode (destructor != msocket_vdelete): Accepted child sockets are NOT + * automatically cleaned up by the server. The application should wrap the child socket in a custom structure + * and invoke msocket_server_cleanup_connection(self, wrapper) from the stream_disconnected callback. + * * @param self Pointer to msocket_server_t instance. * @param address_family Address family (MSOCKET_ADDR_INET, MSOCKET_ADDR_INET6, or MSOCKET_ADDR_UNIX). * @param destructor Destructor function for cleanup items, or NULL to use default msocket_vdelete. @@ -65,6 +73,9 @@ void msocket_server_destroy(msocket_server_t *self); /** * Dynamically allocates and initializes a new msocket_server instance. * + * Supports default auto-reap mode (destructor == NULL or msocket_vdelete) or custom + * wrapper mode (custom destructor function). + * * @param address_family Address family (MSOCKET_ADDR_INET, MSOCKET_ADDR_INET6, or MSOCKET_ADDR_UNIX). * @param destructor Destructor function for cleanup items, or NULL to use default msocket_vdelete. * @return Pointer to newly allocated msocket_server_t, or NULL on failure. @@ -81,13 +92,13 @@ void msocket_server_delete(msocket_server_t *self); /** * Registers the server handler table. * - * The `handler->tcp_accept` callback is called whenever a new client connection is accepted: - * void on_accept(void *arg, msocket_server_t *srv, msocket_t *child_socket) + * The `handler->stream_accept` callback is called whenever a new client connection is accepted: + * void on_accept(void *arg, msocket_server_t *srv, void *socket) * Inside that callback, the application should attach per-connection handlers to `child_socket` * via msocket_set_handler(), and then call msocket_start_io(child_socket). * * @param self Pointer to msocket_server_t instance. - * @param handler Pointer to handler table containing tcp_accept callback. + * @param handler Pointer to handler table containing stream_accept callback. * @param handler_arg User context pointer passed to callback functions. */ void msocket_server_set_handler(msocket_server_t *self, const msocket_handler_t *handler, void *handler_arg); @@ -118,20 +129,22 @@ void msocket_server_unix_start(msocket_server_t *self, const char *socket_path); void msocket_server_disable_cleanup(msocket_server_t *self); /** - * Enqueues a disconnected connection socket for asynchronous deletion by the cleanup thread. + * Enqueues an item directly for asynchronous deletion by the cleanup thread. * * @param self Pointer to msocket_server_t instance. - * @param arg Pointer to connection object (passed to destructor). + * @param arg Pointer to object passed to destructor. */ void msocket_server_reap_connection(msocket_server_t *self, void *arg); /** - * Enqueues a closed connection socket or wrapper item for asynchronous deletion by the cleanup thread. + * Safely enqueues a closed connection socket or wrapper item for asynchronous deletion. * * Safe to call from within client connection callbacks (e.g. `stream_disconnected`). + * In default auto-reap mode (destructor == msocket_vdelete), this safely detaches the socket + * from the server before enqueuing to prevent double-reaping upon I/O thread termination. * * @param self Pointer to msocket_server_t instance. - * @param arg Pointer to connection object (passed to destructor). + * @param arg Pointer to connection object or wrapper (passed to destructor). */ void msocket_server_cleanup_connection(msocket_server_t *self, void *arg); diff --git a/source/msocket_adapter.cpp b/source/msocket_adapter.cpp index 767cad5..606bbb0 100644 --- a/source/msocket_adapter.cpp +++ b/source/msocket_adapter.cpp @@ -67,6 +67,7 @@ namespace auto child_socket = reinterpret_cast(socket); if ((listener != nullptr) && (child_socket != nullptr)) { + msocket_set_server(child_socket, nullptr); auto accepted = std::make_unique(child_socket, true); listener->on_connection_accepted(std::move(accepted)); } @@ -181,15 +182,12 @@ namespace msocket { if (m_socket != nullptr) { + msocket_close(m_socket); TcpSocket::set_listener(nullptr); if (m_owns_socket) { msocket_delete(m_socket); } - else - { - msocket_close(m_socket); - } m_socket = nullptr; } m_listener = nullptr; diff --git a/source/msocket_common.c b/source/msocket_common.c index edf8ebf..fc81ce3 100644 --- a/source/msocket_common.c +++ b/source/msocket_common.c @@ -343,14 +343,24 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child) msocket_os_mutex_lock(self->os); self->state = MSOCKET_STATE_ACCEPTING; + os_socket_t accept_fd = self->os->tcp_sockfd; msocket_os_mutex_unlock(self->os); + if (OS_SOCKET_IS_INVALID(accept_fd)) { + if (placement_new) { + msocket_destroy(child); + } else { + msocket_delete(child); + } + return NULL; + } + os_socket_t sockfd; int one = 1; msocket_error_t result = MSOCKET_NO_ERROR; if (self->address_family == MSOCKET_ADDR_UNIX) { - sockfd = accept(self->os->tcp_sockfd, NULL, NULL); + sockfd = accept(accept_fd, NULL, NULL); if (OS_SOCKET_IS_INVALID(sockfd)) { result = MSOCKET_SOCKET_ERROR; } else { @@ -362,7 +372,7 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child) struct sockaddr_in6 cli_addr6; OS_SOCK_LEN_T cli_len = (OS_SOCK_LEN_T)sizeof(cli_addr6); memset(&cli_addr6, 0, sizeof(cli_addr6)); - sockfd = accept(self->os->tcp_sockfd, (struct sockaddr *)&cli_addr6, &cli_len); + sockfd = accept(accept_fd, (struct sockaddr *)&cli_addr6, &cli_len); if (OS_SOCKET_IS_INVALID(sockfd)) { result = MSOCKET_SOCKET_ERROR; } else if (inet_ntop(AF_INET6, &(cli_addr6.sin6_addr), child->stream_info.addr, MSOCKET_ADDRSTRLEN) == NULL) { @@ -377,7 +387,7 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child) struct sockaddr_in cli_addr; OS_SOCK_LEN_T cli_len = (OS_SOCK_LEN_T)sizeof(cli_addr); memset(&cli_addr, 0, sizeof(cli_addr)); - sockfd = accept(self->os->tcp_sockfd, (struct sockaddr *)&cli_addr, &cli_len); + sockfd = accept(accept_fd, (struct sockaddr *)&cli_addr, &cli_len); if (OS_SOCKET_IS_INVALID(sockfd)) { result = MSOCKET_SOCKET_ERROR; } else if (inet_ntop(AF_INET, &(cli_addr.sin_addr), child->stream_info.addr, MSOCKET_ADDRSTRLEN) == NULL) { @@ -391,7 +401,9 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child) } msocket_os_mutex_lock(self->os); - self->state = MSOCKET_STATE_LISTENING; + if (self->state == MSOCKET_STATE_ACCEPTING) { + self->state = MSOCKET_STATE_LISTENING; + } msocket_os_mutex_unlock(self->os); if (result != MSOCKET_NO_ERROR) { @@ -416,14 +428,17 @@ static msocket_error_t msocket_start_io_thread(msocket_t *self) if (self == NULL || self->os == NULL) { return MSOCKET_INVALID_ARGUMENT_ERROR; } + msocket_os_mutex_lock(self->os); if (!self->os->thread_running) { self->os->thread_running = true; self->os->io_thread = msocket_thread_create(io_task, self); if (self->os->io_thread == NULL) { self->os->thread_running = false; + msocket_os_mutex_unlock(self->os); return MSOCKET_MEM_ERROR; } } + msocket_os_mutex_unlock(self->os); return MSOCKET_NO_ERROR; } @@ -586,24 +601,30 @@ void msocket_close(msocket_t *self) return; } + msocket_os_mutex_lock(self->os); /* Prevent joining I/O thread from within itself */ if (self->os->thread_running && msocket_thread_is_current(self->os->io_thread)) { + msocket_os_mutex_unlock(self->os); return; } - msocket_os_mutex_lock(self->os); self->state = MSOCKET_STATE_CLOSING; if (OS_SOCKET_IS_VALID(self->os->tcp_sockfd)) { OS_SOCKET_SHUTDOWN(self->os->tcp_sockfd); } - msocket_os_mutex_unlock(self->os); + msocket_thread_t *io_thread = NULL; if (self->os->thread_running) { - msocket_thread_join(self->os->io_thread); - msocket_thread_delete(self->os->io_thread); + io_thread = self->os->io_thread; self->os->io_thread = NULL; self->os->thread_running = false; } + msocket_os_mutex_unlock(self->os); + + if (io_thread != NULL) { + msocket_thread_join(io_thread); + msocket_thread_delete(io_thread); + } msocket_os_mutex_lock(self->os); if (OS_SOCKET_IS_VALID(self->os->tcp_sockfd)) { diff --git a/source/msocket_server.c b/source/msocket_server.c index e719c53..946c617 100644 --- a/source/msocket_server.c +++ b/source/msocket_server.c @@ -250,7 +250,7 @@ static void accept_task(void *arg) if (child == NULL) { break; } - if (self->destructor != NULL) { + if (self->destructor == msocket_vdelete) { msocket_set_server(child, self); } if (self->handler_table.stream_accept != NULL) { diff --git a/tests/testsuite_cpp_socket.cpp b/tests/testsuite_cpp_socket.cpp index 6022396..57bdfdb 100644 --- a/tests/testsuite_cpp_socket.cpp +++ b/tests/testsuite_cpp_socket.cpp @@ -17,6 +17,10 @@ extern "C" { #include "msocket_adapter.h" #include #include +#include +#include +#include +#include namespace { @@ -80,6 +84,23 @@ namespace } }; + class MockServerListener : public msocket::ServerListener + { + public: + std::mutex m_mutex; + std::condition_variable m_cv; + int accept_calls{ 0 }; + std::unique_ptr accepted_socket; + + void on_connection_accepted(std::unique_ptr socket) override + { + std::lock_guard lock(m_mutex); + accept_calls++; + accepted_socket = std::move(socket); + m_cv.notify_one(); + } + }; + void helper_send_via_base(msocket::Socket& sock, std::string_view message) { sock.send(message); @@ -206,6 +227,37 @@ static void test_cpp_tcp_socket_lifecycle(CuTest* tc) // Destructor of sock_moved will safely call msocket_delete via RAII } +static void test_cpp_tcp_server_lifecycle(CuTest* tc) +{ + MockServerListener srv_listener; + msocket::TcpServer server(MSOCKET_ADDR_INET); + server.set_listener(&srv_listener); + int res = server.start(19890); + CuAssertIntEquals(tc, 0, res); + + MockSocketListener cli_listener; + msocket::TcpSocket client(MSOCKET_ADDR_INET); + client.set_listener(&cli_listener); + res = client.connect("127.0.0.1", 19890); + CuAssertIntEquals(tc, 0, res); + + std::unique_lock lock(srv_listener.m_mutex); + bool accepted = srv_listener.m_cv.wait_for(lock, std::chrono::milliseconds(1000), [&]() { + return srv_listener.accept_calls > 0; + }); + CuAssertTrue(tc, accepted); + CuAssertIntEquals(tc, 1, srv_listener.accept_calls); + CuAssertPtrNotNull(tc, srv_listener.accepted_socket.get()); + auto accepted_sock = std::move(srv_listener.accepted_socket); + lock.unlock(); + + client.close(); + accepted_sock->close(); + accepted_sock.reset(); + + server.stop(); +} + extern "C" CuSuite* testsuite_cpp_socket(void) { CuSuite* suite = CuSuiteNew(); @@ -214,5 +266,6 @@ extern "C" CuSuite* testsuite_cpp_socket(void) SUITE_ADD_TEST(suite, test_cpp_test_socket_move); SUITE_ADD_TEST(suite, test_cpp_legacy_handler_compat); SUITE_ADD_TEST(suite, test_cpp_tcp_socket_lifecycle); + SUITE_ADD_TEST(suite, test_cpp_tcp_server_lifecycle); return suite; } diff --git a/tests/testsuite_msocket_loopback.c b/tests/testsuite_msocket_loopback.c index 09e8774..20f8802 100644 --- a/tests/testsuite_msocket_loopback.c +++ b/tests/testsuite_msocket_loopback.c @@ -159,6 +159,9 @@ static void test_tcp_loopback(CuTest *tc) CuAssertStrEquals(tc, "Pong!", g_client_recv_buf); /* Cleanup */ + msocket_thread_join(accept_thread); + msocket_thread_delete(accept_thread); + msocket_close(cli); msocket_delete(cli); @@ -169,8 +172,6 @@ static void test_tcp_loopback(CuTest *tc) } msocket_close(srv); - msocket_thread_join(accept_thread); - msocket_thread_delete(accept_thread); msocket_delete(srv); msocket_sem_delete(g_sem_client_connected); diff --git a/tests/testsuite_msocket_server.c b/tests/testsuite_msocket_server.c index 2798ee7..294a235 100644 --- a/tests/testsuite_msocket_server.c +++ b/tests/testsuite_msocket_server.c @@ -16,27 +16,35 @@ #define SERVER_TEST_PORT 19878u -static msocket_sem_t *g_sem_accepted = NULL; -static msocket_sem_t *g_sem_cleaned_up = NULL; -static int g_cleanup_count = 0; +static msocket_sem_t *m_sem_accepted = NULL; +static msocket_sem_t *m_sem_cleaned_up = NULL; +static msocket_sem_t *m_sem_disconnected = NULL; +static int m_cleanup_count = 0; +static msocket_t *m_accepted_socket = NULL; -static void custom_destructor(void *arg) +typedef struct custom_conn_tag { + msocket_t *sock; + msocket_server_t *srv; + int custom_id; +} custom_conn_t; + +static void on_custom_destructor(void *arg) { msocket_t *sock = (msocket_t *)arg; if (sock != NULL) { msocket_delete(sock); - g_cleanup_count++; - msocket_sem_post(g_sem_cleaned_up); + m_cleanup_count++; + msocket_sem_post(m_sem_cleaned_up); } } -static void server_on_accept(void *arg, msocket_server_t *srv, void *socket) +static void on_server_accept_cleanup(void *arg, msocket_server_t *srv, void *socket) { (void)arg; (void)srv; msocket_t *child = (msocket_t *)socket; if (child != NULL) { - msocket_sem_post(g_sem_accepted); + msocket_sem_post(m_sem_accepted); /* Queue for asynchronous destruction */ msocket_server_cleanup_connection(srv, child); } @@ -44,16 +52,16 @@ static void server_on_accept(void *arg, msocket_server_t *srv, void *socket) static void test_msocket_server_cleanup(CuTest *tc) { - g_sem_accepted = msocket_sem_new(0u); - g_sem_cleaned_up = msocket_sem_new(0u); - g_cleanup_count = 0; + m_sem_accepted = msocket_sem_new(0u); + m_sem_cleaned_up = msocket_sem_new(0u); + m_cleanup_count = 0; - msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, custom_destructor); + msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, on_custom_destructor); CuAssertPtrNotNull(tc, srv); msocket_handler_t handler; memset(&handler, 0, sizeof(handler)); - handler.stream_accept = server_on_accept; + handler.stream_accept = on_server_accept_cleanup; msocket_server_set_handler(srv, &handler, NULL); msocket_server_start(srv, NULL, 0u, SERVER_TEST_PORT); @@ -71,53 +79,53 @@ static void test_msocket_server_cleanup(CuTest *tc) /* Wait for server accept */ int attempts = 0; - while (msocket_sem_test(g_sem_accepted) <= 0 && attempts++ < 50) { + while (msocket_sem_test(m_sem_accepted) <= 0 && attempts++ < 50) { msocket_sleep_ms(20u); } CuAssertTrue(tc, attempts < 50); /* Wait for asynchronous cleanup queue to run destructor */ attempts = 0; - while (msocket_sem_test(g_sem_cleaned_up) <= 0 && attempts++ < 50) { + while (msocket_sem_test(m_sem_cleaned_up) <= 0 && attempts++ < 50) { msocket_sleep_ms(20u); } CuAssertTrue(tc, attempts < 50); - CuAssertIntEquals(tc, 1, g_cleanup_count); + CuAssertIntEquals(tc, 1, m_cleanup_count); msocket_close(cli); msocket_delete(cli); msocket_server_delete(srv); - msocket_sem_delete(g_sem_accepted); - msocket_sem_delete(g_sem_cleaned_up); + msocket_sem_delete(m_sem_accepted); + msocket_sem_delete(m_sem_cleaned_up); } -static void server_on_accept_auto(void *arg, msocket_server_t *srv, void *socket) +static void on_server_accept_auto(void *arg, msocket_server_t *srv, void *socket) { (void)arg; - (void)srv; msocket_t *child = (msocket_t *)socket; if (child != NULL) { + msocket_set_server(child, srv); msocket_handler_t child_handler; memset(&child_handler, 0, sizeof(child_handler)); msocket_set_handler(child, &child_handler, NULL); msocket_start_io(child); - msocket_sem_post(g_sem_accepted); + msocket_sem_post(m_sem_accepted); } } static void test_msocket_server_auto_reap(CuTest *tc) { - g_sem_accepted = msocket_sem_new(0u); - g_sem_cleaned_up = msocket_sem_new(0u); - g_cleanup_count = 0; + m_sem_accepted = msocket_sem_new(0u); + m_sem_cleaned_up = msocket_sem_new(0u); + m_cleanup_count = 0; - msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, custom_destructor); + msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, on_custom_destructor); CuAssertPtrNotNull(tc, srv); msocket_handler_t handler; memset(&handler, 0, sizeof(handler)); - handler.stream_accept = server_on_accept_auto; + handler.stream_accept = on_server_accept_auto; msocket_server_set_handler(srv, &handler, NULL); msocket_server_start(srv, NULL, 0u, SERVER_TEST_PORT + 1u); @@ -136,7 +144,7 @@ static void test_msocket_server_auto_reap(CuTest *tc) /* Wait for server accept */ int attempts = 0; - while (msocket_sem_test(g_sem_accepted) <= 0 && attempts++ < 50) { + while (msocket_sem_test(m_sem_accepted) <= 0 && attempts++ < 50) { msocket_sleep_ms(20u); } CuAssertTrue(tc, attempts < 50); @@ -147,16 +155,349 @@ static void test_msocket_server_auto_reap(CuTest *tc) /* Wait for automatic cleanup queue to run destructor */ attempts = 0; - while (msocket_sem_test(g_sem_cleaned_up) <= 0 && attempts++ < 50) { + while (msocket_sem_test(m_sem_cleaned_up) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + CuAssertIntEquals(tc, 1, m_cleanup_count); + + msocket_server_delete(srv); + + msocket_sem_delete(m_sem_accepted); + msocket_sem_delete(m_sem_cleaned_up); +} + +static void on_child_disconnected_default(void *arg, void *socket) +{ + (void)arg; + (void)socket; + msocket_sem_post(m_sem_disconnected); +} + +static void on_server_accept_default(void *arg, msocket_server_t *srv, void *socket) +{ + (void)arg; + (void)srv; + msocket_t *child = (msocket_t *)socket; + if (child != NULL) { + msocket_handler_t child_handler; + memset(&child_handler, 0, sizeof(child_handler)); + child_handler.stream_disconnected = on_child_disconnected_default; + msocket_set_handler(child, &child_handler, NULL); + msocket_start_io(child); + msocket_sem_post(m_sem_accepted); + } +} + +static void test_msocket_server_default_auto_reap(CuTest *tc) +{ + m_sem_accepted = msocket_sem_new(0u); + m_sem_disconnected = msocket_sem_new(0u); + + /* Server with default destructor (msocket_vdelete) automatically attaches accepted sockets */ + msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, NULL); + CuAssertPtrNotNull(tc, srv); + + msocket_handler_t handler; + memset(&handler, 0, sizeof(handler)); + handler.stream_accept = on_server_accept_default; + msocket_server_set_handler(srv, &handler, NULL); + + msocket_server_start(srv, NULL, 0u, SERVER_TEST_PORT + 2u); + + /* Connect client */ + msocket_t *cli = msocket_new(MSOCKET_ADDR_INET); + CuAssertPtrNotNull(tc, cli); + + msocket_handler_t cli_handler; + memset(&cli_handler, 0, sizeof(cli_handler)); + msocket_set_handler(cli, &cli_handler, NULL); + + msocket_error_t rc = msocket_connect(cli, "127.0.0.1", SERVER_TEST_PORT + 2u); + CuAssertIntEquals(tc, MSOCKET_NO_ERROR, rc); + msocket_start_io(cli); + + /* Wait for server accept */ + int attempts = 0; + while (msocket_sem_test(m_sem_accepted) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + + /* Disconnect client - auto-reap should handle child socket without error */ + msocket_close(cli); + msocket_delete(cli); + + /* Wait for child socket to detect disconnection */ + attempts = 0; + while (msocket_sem_test(m_sem_disconnected) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + + /* Give server cleanup queue a moment to process the reaped socket */ + msocket_sleep_ms(20u); + + msocket_server_delete(srv); + msocket_sem_delete(m_sem_accepted); + msocket_sem_delete(m_sem_disconnected); +} + +static void on_custom_conn_delete(void *arg) +{ + custom_conn_t *conn = (custom_conn_t *)arg; + if (conn != NULL) { + if (conn->sock != NULL) { + msocket_delete(conn->sock); + conn->sock = NULL; + } + free(conn); + m_cleanup_count++; + msocket_sem_post(m_sem_cleaned_up); + } +} + +static void on_wrapper_client_disconnected(void *arg, void *socket) +{ + (void)socket; + custom_conn_t *conn = (custom_conn_t *)arg; + if (conn != NULL && conn->srv != NULL) { + msocket_server_cleanup_connection(conn->srv, conn); + } +} + +static void on_server_accept_custom_wrapper(void *arg, msocket_server_t *srv, void *socket) +{ + (void)arg; + msocket_t *child = (msocket_t *)socket; + if (child != NULL) { + custom_conn_t *conn = (custom_conn_t *)malloc(sizeof(custom_conn_t)); + if (conn != NULL) { + conn->sock = child; + conn->srv = srv; + conn->custom_id = 42; + msocket_handler_t child_handler; + memset(&child_handler, 0, sizeof(child_handler)); + child_handler.stream_disconnected = on_wrapper_client_disconnected; + msocket_set_handler(child, &child_handler, (void *)conn); + msocket_start_io(child); + msocket_sem_post(m_sem_accepted); + } + } +} + +static void test_msocket_server_custom_wrapper(CuTest *tc) +{ + m_sem_accepted = msocket_sem_new(0u); + m_sem_cleaned_up = msocket_sem_new(0u); + m_cleanup_count = 0; + + /* Server with custom wrapper destructor (e.g. c-apx json_server_connection_vdelete pattern) */ + msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, on_custom_conn_delete); + CuAssertPtrNotNull(tc, srv); + + msocket_handler_t handler; + memset(&handler, 0, sizeof(handler)); + handler.stream_accept = on_server_accept_custom_wrapper; + msocket_server_set_handler(srv, &handler, NULL); + + msocket_server_start(srv, NULL, 0u, SERVER_TEST_PORT + 3u); + + /* Connect client */ + msocket_t *cli = msocket_new(MSOCKET_ADDR_INET); + CuAssertPtrNotNull(tc, cli); + + msocket_handler_t cli_handler; + memset(&cli_handler, 0, sizeof(cli_handler)); + msocket_set_handler(cli, &cli_handler, NULL); + + msocket_error_t rc = msocket_connect(cli, "127.0.0.1", SERVER_TEST_PORT + 3u); + CuAssertIntEquals(tc, MSOCKET_NO_ERROR, rc); + msocket_start_io(cli); + + /* Wait for server accept */ + int attempts = 0; + while (msocket_sem_test(m_sem_accepted) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + + /* Disconnect client - wrapper disconnect callback invokes msocket_server_cleanup_connection */ + msocket_close(cli); + msocket_delete(cli); + + /* Wait for custom destructor to execute */ + attempts = 0; + while (msocket_sem_test(m_sem_cleaned_up) <= 0 && attempts++ < 50) { msocket_sleep_ms(20u); } CuAssertTrue(tc, attempts < 50); - CuAssertIntEquals(tc, 1, g_cleanup_count); + CuAssertIntEquals(tc, 1, m_cleanup_count); msocket_server_delete(srv); + msocket_sem_delete(m_sem_accepted); + msocket_sem_delete(m_sem_cleaned_up); +} + +static void on_child_disconnected_cleanup(void *arg, void *socket) +{ + msocket_server_t *srv = (msocket_server_t *)arg; + msocket_t *child = (msocket_t *)socket; + if (srv != NULL && child != NULL) { + msocket_server_cleanup_connection(srv, child); + msocket_sem_post(m_sem_disconnected); + } +} + +static void on_server_accept_manual_default(void *arg, msocket_server_t *srv, void *socket) +{ + (void)arg; + msocket_t *child = (msocket_t *)socket; + if (child != NULL) { + msocket_handler_t child_handler; + memset(&child_handler, 0, sizeof(child_handler)); + child_handler.stream_disconnected = on_child_disconnected_cleanup; + msocket_set_handler(child, &child_handler, (void *)srv); + msocket_start_io(child); + msocket_sem_post(m_sem_accepted); + } +} + +static void test_msocket_server_manual_cleanup_default(CuTest *tc) +{ + m_sem_accepted = msocket_sem_new(0u); + m_sem_disconnected = msocket_sem_new(0u); + + /* Server with default destructor */ + msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, NULL); + CuAssertPtrNotNull(tc, srv); + + msocket_handler_t handler; + memset(&handler, 0, sizeof(handler)); + handler.stream_accept = on_server_accept_manual_default; + msocket_server_set_handler(srv, &handler, NULL); + + msocket_server_start(srv, NULL, 0u, SERVER_TEST_PORT + 4u); + + /* Connect client */ + msocket_t *cli = msocket_new(MSOCKET_ADDR_INET); + CuAssertPtrNotNull(tc, cli); + + msocket_handler_t cli_handler; + memset(&cli_handler, 0, sizeof(cli_handler)); + msocket_set_handler(cli, &cli_handler, NULL); + + msocket_error_t rc = msocket_connect(cli, "127.0.0.1", SERVER_TEST_PORT + 4u); + CuAssertIntEquals(tc, MSOCKET_NO_ERROR, rc); + msocket_start_io(cli); + + /* Wait for server accept */ + int attempts = 0; + while (msocket_sem_test(m_sem_accepted) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + + /* Disconnect client - on_child_disconnected_cleanup calls msocket_server_cleanup_connection. + * It unsets child->server, so when io_task finishes it will not double reap. */ + msocket_close(cli); + msocket_delete(cli); + + /* Wait for disconnect callback to execute and enqueue cleanup */ + attempts = 0; + while (msocket_sem_test(m_sem_disconnected) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + + /* Give cleanup thread a brief moment to finish destruction */ + msocket_sleep_ms(20u); + + msocket_server_delete(srv); + msocket_sem_delete(m_sem_accepted); + msocket_sem_delete(m_sem_disconnected); +} - msocket_sem_delete(g_sem_accepted); - msocket_sem_delete(g_sem_cleaned_up); +static void on_disabled_client_disconnected(void *arg, void *socket) +{ + (void)arg; + (void)socket; + msocket_sem_post(m_sem_disconnected); +} + +static void on_server_accept_disabled(void *arg, msocket_server_t *srv, void *socket) +{ + (void)arg; + (void)srv; + msocket_t *child = (msocket_t *)socket; + if (child != NULL) { + m_accepted_socket = child; + msocket_handler_t child_handler; + memset(&child_handler, 0, sizeof(child_handler)); + child_handler.stream_disconnected = on_disabled_client_disconnected; + msocket_set_handler(child, &child_handler, NULL); + msocket_start_io(child); + msocket_sem_post(m_sem_accepted); + } +} + +static void test_msocket_server_disable_cleanup(CuTest *tc) +{ + m_sem_accepted = msocket_sem_new(0u); + m_sem_disconnected = msocket_sem_new(0u); + m_accepted_socket = NULL; + + msocket_server_t *srv = msocket_server_new(MSOCKET_ADDR_INET, NULL); + CuAssertPtrNotNull(tc, srv); + + /* Disable automatic cleanup (as in apx_socket_extension) */ + msocket_server_disable_cleanup(srv); + CuAssertFnPtrEquals(tc, NULL, srv->destructor); + + msocket_handler_t handler; + memset(&handler, 0, sizeof(handler)); + handler.stream_accept = on_server_accept_disabled; + msocket_server_set_handler(srv, &handler, NULL); + + msocket_server_start(srv, NULL, 0u, SERVER_TEST_PORT + 5u); + + /* Connect client */ + msocket_t *cli = msocket_new(MSOCKET_ADDR_INET); + CuAssertPtrNotNull(tc, cli); + + msocket_handler_t cli_handler; + memset(&cli_handler, 0, sizeof(cli_handler)); + msocket_set_handler(cli, &cli_handler, NULL); + + msocket_error_t rc = msocket_connect(cli, "127.0.0.1", SERVER_TEST_PORT + 5u); + CuAssertIntEquals(tc, MSOCKET_NO_ERROR, rc); + msocket_start_io(cli); + + /* Wait for server accept */ + int attempts = 0; + while (msocket_sem_test(m_sem_accepted) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + CuAssertPtrNotNull(tc, m_accepted_socket); + + /* Disconnect client */ + msocket_close(cli); + msocket_delete(cli); + + /* Wait for disconnect event on accepted socket before manual deletion */ + attempts = 0; + while (msocket_sem_test(m_sem_disconnected) <= 0 && attempts++ < 50) { + msocket_sleep_ms(20u); + } + CuAssertTrue(tc, attempts < 50); + + /* Application manually manages child socket lifecycle */ + msocket_delete(m_accepted_socket); + + msocket_server_delete(srv); + msocket_sem_delete(m_sem_accepted); + msocket_sem_delete(m_sem_disconnected); } CuSuite *testsuite_msocket_server(void) @@ -164,5 +505,9 @@ CuSuite *testsuite_msocket_server(void) CuSuite *suite = CuSuiteNew(); SUITE_ADD_TEST(suite, test_msocket_server_cleanup); SUITE_ADD_TEST(suite, test_msocket_server_auto_reap); + SUITE_ADD_TEST(suite, test_msocket_server_default_auto_reap); + SUITE_ADD_TEST(suite, test_msocket_server_custom_wrapper); + SUITE_ADD_TEST(suite, test_msocket_server_manual_cleanup_default); + SUITE_ADD_TEST(suite, test_msocket_server_disable_cleanup); return suite; }