diff --git a/include/webui.h b/include/webui.h index 83bbd1a0d..d5681127b 100644 --- a/include/webui.h +++ b/include/webui.h @@ -369,7 +369,10 @@ WEBUI_EXPORT bool webui_show_browser(size_t window, const char* content, size_t * Pass `NULL` or an empty string to start in folder mode using the * current root folder and index fallback logic. * - * @return Returns the url of this window server. + * @return Returns the URL of this window server. The returned string is owned + * by WebUI and remains valid until a later successful `webui_start_server()` + * call for the same window or `webui_clean()`. Destroying the window does not + * invalidate it. * * @example const char* url = webui_start_server(myWindow, "/full/root/path"); */ @@ -516,6 +519,8 @@ WEBUI_EXPORT void webui_close_client(webui_event_t* e); /** * @brief Close a specific window and free all memory resources. + * This function may be called from a WebUI callback. In that case, final + * reclamation is deferred until the callback and active server work retire. * * @param window The window number * @@ -859,6 +864,8 @@ WEBUI_EXPORT void webui_navigate_client(webui_event_t* e, const char* url); /** * @brief Free all memory resources. Should be called only at the end. + * When called from a WebUI callback, this function requests application exit + * and defers cleanup. Call it again after `webui_wait()` returns. * * @example * webui_wait(); diff --git a/include/webui.hpp b/include/webui.hpp index 248b92dfd..870a088c6 100644 --- a/include/webui.hpp +++ b/include/webui.hpp @@ -286,7 +286,9 @@ namespace webui { webui_close(webui_window); } - // Close a specific window and free all memory resources. + // Close a specific window and free all memory resources. Safe to call + // from a WebUI callback; final reclamation is deferred until the + // callback and active server work retire. void destroy() const { webui_destroy(webui_window); } @@ -439,6 +441,9 @@ namespace webui { // Start only the local web server and return the URL. // Empty content means: use current root folder + index fallback. + // The returned view is owned by WebUI and remains valid until a later + // successful call for this window or clean(). Destroying the window + // does not invalidate it. std::string_view start_server(const std::string_view content = "") const { return std::string_view{webui_start_server(webui_window, content.data())}; } @@ -568,7 +573,9 @@ namespace webui { return webui_malloc(size); } - // Free all memory resources. Should be called only at the end. + // Free all memory resources. Should be called only at the end. From a WebUI + // callback this requests exit and defers cleanup; call it again after + // wait() returns. inline void clean() { webui_clean(); } diff --git a/src/civetweb/README.md b/src/civetweb/README.md index 13b8abd24..7fd8b647b 100644 --- a/src/civetweb/README.md +++ b/src/civetweb/README.md @@ -14,4 +14,12 @@ Changes currently required when updating the submodule to ensure full direct int + #endif /* Copyright (c) 2013-2024 the Civetweb developers - ``` \ No newline at end of file + ``` + +- Preserve WebUI-owned connection lifetimes during server shutdown. + + WebUI carries `webui_civetweb_shutdown_context_connections()` and related + worker socket synchronization so it can stop admitting new connections and + interrupt blocked I/O before `mg_stop()` releases connection storage. + Preserve these changes when updating the vendored CivetWeb source until an + equivalent upstream facility is available. diff --git a/src/civetweb/civetweb.c b/src/civetweb/civetweb.c index a399ce453..4a26584c0 100644 --- a/src/civetweb/civetweb.c +++ b/src/civetweb/civetweb.c @@ -2431,7 +2431,9 @@ struct mg_context { /* Thread related */ stop_flag_t stop_flag; /* Should we stop event loop */ + stop_flag_t stop_accepting; /* Should newly accepted sockets be rejected */ pthread_mutex_t thread_mutex; /* Protects client_socks or queue */ + pthread_mutex_t socket_mutex; /* Synchronizes worker socket scan and close */ pthread_t masterthreadid; /* The master thread ID */ unsigned int cfg_max_worker_threads; /* How many worker-threads we are @@ -18405,6 +18407,9 @@ close_connection(struct mg_connection *conn) conn->ssl = NULL; } #endif + if (conn->phys_ctx->context_type == CONTEXT_SERVER) { + (void)pthread_mutex_lock(&conn->phys_ctx->socket_mutex); + } if (conn->client.sock != INVALID_SOCKET) { #if defined(__ZEPHYR__) closesocket(conn->client.sock); @@ -18413,6 +18418,9 @@ close_connection(struct mg_connection *conn) #endif conn->client.sock = INVALID_SOCKET; } + if (conn->phys_ctx->context_type == CONTEXT_SERVER) { + (void)pthread_mutex_unlock(&conn->phys_ctx->socket_mutex); + } /* call the connection_closed callback if assigned */ if (conn->phys_ctx->callbacks.connection_closed != NULL) { @@ -18493,6 +18501,59 @@ mg_close_connection(struct mg_connection *conn) } +/* WebUI-private extension: quiesce a server without releasing connection + * storage retained by WebUI callback tasks. Not part of the CivetWeb API. */ +void +webui_civetweb_shutdown_context_connections(struct mg_context *ctx) +{ + if ((ctx == NULL) || (ctx->worker_connections == NULL)) { + return; + } + + /* Quiesce socket dispatch before taking the worker snapshot. A socket that + * was accepted just before this flag changed is rejected by produce_socket, + * and dynamic worker publication uses the same mutex. Workers themselves + * remain alive so external users may safely retire their conn pointers. */ + (void)pthread_mutex_lock(&ctx->thread_mutex); + STOP_FLAG_ASSIGN(&ctx->stop_accepting, 1); + (void)pthread_mutex_lock(&ctx->socket_mutex); + +#if defined(ALTERNATIVE_QUEUE) + for (unsigned int i = 0; i < ctx->spawned_worker_threads; i++) { + if ((ctx->client_socks != NULL) + && (ctx->client_socks[i].in_use == 1) + && (ctx->client_socks[i].sock != INVALID_SOCKET)) { + shutdown(ctx->client_socks[i].sock, SHUTDOWN_BOTH); + } + } +#else + /* No worker may consume the queue while thread_mutex is held. Close queued + * sockets so every connection that can still be used is represented in + * worker_connections below. */ + while (ctx->sq_tail < ctx->sq_head) { + struct socket *queued = &ctx->squeue[ctx->sq_tail % ctx->sq_size]; + if (queued->sock != INVALID_SOCKET) { + set_blocking_mode(queued->sock); + closesocket(queued->sock); + queued->sock = INVALID_SOCKET; + } + ctx->sq_tail++; + } + (void)pthread_cond_broadcast(&ctx->sq_empty); +#endif + + unsigned int count = ctx->spawned_worker_threads; + for (unsigned int i = 0; i < count; i++) { + struct mg_connection *conn = &ctx->worker_connections[i]; + if (conn->client.sock != INVALID_SOCKET) { + shutdown(conn->client.sock, SHUTDOWN_BOTH); + } + } + (void)pthread_mutex_unlock(&ctx->socket_mutex); + (void)pthread_mutex_unlock(&ctx->thread_mutex); +} + + static struct mg_connection * mg_connect_client_impl(const struct mg_client_options *client_options, int use_ssl, @@ -20099,17 +20160,29 @@ static void produce_socket(struct mg_context *ctx, const struct socket *sp) { unsigned int i; + (void)pthread_mutex_lock(&ctx->thread_mutex); + int accepting = STOP_FLAG_IS_ZERO(&ctx->stop_flag) + && STOP_FLAG_IS_ZERO(&ctx->stop_accepting); + (void)pthread_mutex_unlock(&ctx->thread_mutex); + if (!accepting) { + set_blocking_mode(sp->sock); + closesocket(sp->sock); + return; + } (void)mg_start_worker_thread( ctx, 1); /* will start a worker-thread only if there aren't currently any idle worker-threads */ - while (!ctx->stop_flag) { + while (STOP_FLAG_IS_ZERO(&ctx->stop_flag) + && STOP_FLAG_IS_ZERO(&ctx->stop_accepting)) { for (i = 0; i < ctx->spawned_worker_threads; i++) { /* find a free worker slot and signal it */ if (ctx->client_socks[i].in_use == 2) { (void)pthread_mutex_lock(&ctx->thread_mutex); - if ((ctx->client_socks[i].in_use == 2) && !ctx->stop_flag) { + if ((ctx->client_socks[i].in_use == 2) + && STOP_FLAG_IS_ZERO(&ctx->stop_flag) + && STOP_FLAG_IS_ZERO(&ctx->stop_accepting)) { ctx->client_socks[i] = *sp; ctx->client_socks[i].in_use = 1; /* socket has been moved to the consumer */ @@ -20149,6 +20222,11 @@ consume_socket(struct mg_context *ctx, (void)pthread_mutex_lock(&ctx->thread_mutex); *sp = ctx->client_socks[thread_index]; + /* Ownership moves to the worker connection. Do not leave a stale socket + * number in the handoff slot where a later shutdown scan could mistake it + * for an unconsumed connection after the descriptor has been reused. */ + ctx->client_socks[thread_index].sock = INVALID_SOCKET; + ctx->client_socks[thread_index].in_use = 0; if (ctx->stop_flag) { (void)pthread_mutex_unlock(&ctx->thread_mutex); if (sp->in_use == 1) { @@ -20223,13 +20301,22 @@ static void produce_socket(struct mg_context *ctx, const struct socket *sp) { int queue_filled; + int queued = 0; (void)pthread_mutex_lock(&ctx->thread_mutex); + if (!STOP_FLAG_IS_ZERO(&ctx->stop_flag) + || !STOP_FLAG_IS_ZERO(&ctx->stop_accepting)) { + (void)pthread_mutex_unlock(&ctx->thread_mutex); + set_blocking_mode(sp->sock); + closesocket(sp->sock); + return; + } queue_filled = ctx->sq_head - ctx->sq_tail; /* If the queue is full, wait */ while (STOP_FLAG_IS_ZERO(&ctx->stop_flag) + && STOP_FLAG_IS_ZERO(&ctx->stop_accepting) && (queue_filled >= ctx->sq_size)) { ctx->sq_blocked = 1; /* Status information: All threads busy */ #if defined(USE_SERVER_STATS) @@ -20242,10 +20329,13 @@ produce_socket(struct mg_context *ctx, const struct socket *sp) queue_filled = ctx->sq_head - ctx->sq_tail; } - if (queue_filled < ctx->sq_size) { + if (STOP_FLAG_IS_ZERO(&ctx->stop_flag) + && STOP_FLAG_IS_ZERO(&ctx->stop_accepting) + && (queue_filled < ctx->sq_size)) { /* Copy socket to the queue and increment head */ ctx->squeue[ctx->sq_head % ctx->sq_size] = *sp; ctx->sq_head++; + queued = 1; DEBUG_TRACE("queued socket %d", sp ? sp->sock : -1); } @@ -20258,6 +20348,11 @@ produce_socket(struct mg_context *ctx, const struct socket *sp) (void)pthread_cond_signal(&ctx->sq_full); (void)pthread_mutex_unlock(&ctx->thread_mutex); + if (!queued) { + set_blocking_mode(sp->sock); + closesocket(sp->sock); + return; + } (void)mg_start_worker_thread( ctx, 1); /* will start a worker-thread only if there aren't currently @@ -20740,6 +20835,7 @@ master_thread_run(struct mg_context *ctx) * Therefore, we're checking pfd[i].revents & POLLIN, not * pfd[i].revents == POLLIN. */ if (STOP_FLAG_IS_ZERO(&ctx->stop_flag) + && STOP_FLAG_IS_ZERO(&ctx->stop_accepting) && (pfd[i].revents & POLLIN)) { accept_new_connection(&ctx->listening_sockets[i], ctx); } @@ -20865,6 +20961,7 @@ free_context(struct mg_context *ctx) * condvars */ (void)pthread_mutex_destroy(&ctx->thread_mutex); + (void)pthread_mutex_destroy(&ctx->socket_mutex); #if defined(ALTERNATIVE_QUEUE) mg_free(ctx->client_socks); @@ -20979,7 +21076,7 @@ mg_stop(struct mg_context *ctx) STOP_FLAG_ASSIGN(&ctx->stop_flag, 1); /* Closing this socket will cause mg_poll() in all the I/O threads to return - * immediately */ + * immediately. */ closesocket(ctx->user_shutdown_notification_socket); ctx->user_shutdown_notification_socket = -1; /* to avoid calling closesocket() again in free_context() */ @@ -21154,13 +21251,24 @@ mg_socketpair(int *sockA, int *sockB) static int mg_start_worker_thread(struct mg_context *ctx, int only_if_no_idle_threads) { + /* Keep worker reservation, creation and publication atomic with respect to + * an external stop/connection-interrupt request. The new worker may start + * running immediately, but it cannot consume a socket until this mutex is + * released. */ + (void)pthread_mutex_lock(&ctx->thread_mutex); const unsigned int i = ctx->spawned_worker_threads; if (i >= ctx->cfg_max_worker_threads) { + (void)pthread_mutex_unlock(&ctx->thread_mutex); return -1; /* Oops, we hit our worker-thread limit! No more worker threads, ever! */ } - (void)pthread_mutex_lock(&ctx->thread_mutex); + if (!STOP_FLAG_IS_ZERO(&ctx->stop_flag) + || !STOP_FLAG_IS_ZERO(&ctx->stop_accepting)) { + (void)pthread_mutex_unlock(&ctx->thread_mutex); + return -1; + } + #if defined(ALTERNATIVE_QUEUE) if ((only_if_no_idle_threads) && (ctx->idle_worker_thread_count > 0)) { #else @@ -21175,7 +21283,6 @@ mg_start_worker_thread(struct mg_context *ctx, int only_if_no_idle_threads) ctx->idle_worker_thread_count++; /* we do this here to avoid a race condition while the thread is starting up */ - (void)pthread_mutex_unlock(&ctx->thread_mutex); ctx->worker_connections[i].phys_ctx = ctx; int ret = mg_start_thread_with_id(worker_thread, @@ -21186,10 +21293,9 @@ mg_start_worker_thread(struct mg_context *ctx, int only_if_no_idle_threads) the table */ DEBUG_TRACE("Started worker_thread #%i", ctx->spawned_worker_threads); } else { - (void)pthread_mutex_lock(&ctx->thread_mutex); ctx->idle_worker_thread_count--; /* whoops, roll-back on error */ - (void)pthread_mutex_unlock(&ctx->thread_mutex); } + (void)pthread_mutex_unlock(&ctx->thread_mutex); return ret; } @@ -21266,6 +21372,7 @@ mg_start2(struct mg_init_data *init, struct mg_error_data *error) pthread_setspecific(sTlsKey, &tls); ok = (0 == pthread_mutex_init(&ctx->thread_mutex, &pthread_mutex_attr)); + ok &= (0 == pthread_mutex_init(&ctx->socket_mutex, &pthread_mutex_attr)); #if !defined(ALTERNATIVE_QUEUE) ok &= (0 == pthread_cond_init(&ctx->sq_empty, NULL)); ok &= (0 == pthread_cond_init(&ctx->sq_full, NULL)); @@ -21773,6 +21880,9 @@ mg_start2(struct mg_init_data *init, struct mg_error_data *error) pthread_setspecific(sTlsKey, NULL); return NULL; } + for (i = 0; (unsigned)i < ctx->cfg_max_worker_threads; i++) { + ctx->worker_connections[i].client.sock = INVALID_SOCKET; + } #if defined(ALTERNATIVE_QUEUE) ctx->client_wait_events = diff --git a/src/webui.c b/src/webui.c index c9a178633..a6f67c842 100644 --- a/src/webui.c +++ b/src/webui.c @@ -46,6 +46,9 @@ #define MG_BUF_LEN (WEBUI_MAX_BUF) #include "civetweb/civetweb.h" +/* Private extension implemented by our vendored CivetWeb source. */ +void webui_civetweb_shutdown_context_connections(struct mg_context* ctx); + // -- Disable Non-critical warnings --- #ifdef _MSC_VER #pragma warning(push, 0) @@ -146,6 +149,14 @@ typedef pthread_mutex_t webui_mutex_t; typedef pthread_cond_t webui_condition_t; #endif +#if defined(_MSC_VER) +#define WEBUI_THREAD_LOCAL __declspec(thread) +#elif defined(__GNUC__) || defined(__clang__) +#define WEBUI_THREAD_LOCAL __thread +#else +#define WEBUI_THREAD_LOCAL _Thread_local +#endif + // Compiler #if defined(_MSC_VER) #define WEBUI_COMPILER "MSVC" @@ -386,6 +397,13 @@ typedef struct _webui_window_t { // Server bool wait; // Let server thread wait more time for websocket bool server_running; // Slow check + bool server_thread_active; // Full thread lifetime, including startup and cleanup + bool destroy_requested; // Stop admitting work and reclaim after all users retire + bool destroy_server_owned; // The server thread finalizes callback-triggered destruction + bool destroy_finalizing; // A lifecycle owner has committed to final reclamation + bool accepting_tasks; // Gate for detached work that retains this window + size_t active_tasks; // Detached work and user callbacks retaining this window + size_t active_show_calls; // Public show operations retaining this window bool connected; // Fast check size_t server_port; char* url; @@ -453,6 +471,7 @@ typedef struct _webui_window_t { webui_mutex_t mutex_win_exit_now; webui_mutex_t mutex_win_reusable; webui_mutex_t mutex_win_server_running; + webui_condition_t condition_win_lifecycle; bool win_exit_now; // WebView bool allow_webview; @@ -489,6 +508,7 @@ typedef struct _webui_core_t { bool cookies_single_set[WEBUI_MAX_IDS]; size_t servers; size_t showing; + bool accepting_show_calls; size_t used_ports[WEBUI_MAX_IDS]; size_t startup_timeout; size_t cb_count; @@ -508,8 +528,8 @@ typedef struct _webui_core_t { size_t current_browser; _webui_window_t* wins[WEBUI_MAX_IDS]; bool wins_reserved[WEBUI_MAX_IDS]; + char server_urls[WEBUI_MAX_IDS][64]; webui_mutex_t mutex_server_start; - webui_mutex_t mutex_send; webui_mutex_t mutex_receive; webui_mutex_t mutex_wait; webui_mutex_t mutex_bridge; @@ -525,6 +545,8 @@ typedef struct _webui_core_t { webui_mutex_t mutex_js_run_id; webui_mutex_t mutex_ws_process_number; webui_condition_t condition_wait; + webui_mutex_t mutex_windows; + webui_condition_t condition_windows; char* default_server_root_path; bool ui; char* custom_browser_folder_path; @@ -656,15 +678,32 @@ static void _webui_update_wait_state(void); static void _webui_servers_count(int delta); static size_t _webui_servers_get(void); static void _webui_showing_count(int delta); +static bool _webui_lifecycle_users_exist(void); static bool _webui_wait_is_needed(void); static void _webui_wait_wake_up(void); static void _webui_wait_for_servers(void); static bool _webui_mutex_win_is_exit_now(_webui_window_t* win, int update); static bool _webui_mutex_is_webview_update(_webui_window_t* win, int update); static bool _webui_mutex_is_server_running(_webui_window_t* win, int update); +static bool _webui_mutex_try_start_server_thread(_webui_window_t* win); +static bool _webui_mutex_server_thread_stopped(_webui_window_t* win); +static bool _webui_mutex_is_window_stopping(_webui_window_t* win); +static bool _webui_window_prepare_show(_webui_window_t* win); +static bool _webui_window_task_begin(_webui_window_t* win); +static void _webui_window_task_end(_webui_window_t* win); +static bool _webui_window_show_begin(size_t window, _webui_window_t** win); +static void _webui_window_show_end(_webui_window_t* win); +static void _webui_window_stop_tasks(_webui_window_t* win); +static void _webui_window_wait_for_tasks(_webui_window_t* win); +static bool _webui_user_callback_begin(_webui_window_t* win, _webui_window_t** previous); +static void _webui_user_callback_end(_webui_window_t* win, _webui_window_t* previous); +static void _webui_finalize_window_when_idle(_webui_window_t* win); +static void _webui_finalize_window(_webui_window_t* win); +static bool _webui_start_webview_thread(_webui_window_t* win); static void _webui_condition_init(webui_condition_t* cond); static void _webui_condition_wait(webui_condition_t* cond, webui_mutex_t* mutex); static void _webui_condition_signal(webui_condition_t* cond); +static void _webui_condition_broadcast(webui_condition_t* cond); static void _webui_condition_destroy(webui_condition_t* cond); static void _webui_http_send(_webui_window_t* win, struct mg_connection* client, const char* mime_type, const char* body, size_t body_len, bool cache); @@ -802,6 +841,7 @@ static WEBUI_THREAD_MONITOR; // -- Heap ---------------------------- static _webui_core_t _webui; static _webui_log_t _webui_log_data = { NULL, NULL }; +static WEBUI_THREAD_LOCAL _webui_window_t* _webui_callback_window = NULL; static const char* webui_html_served = "Access Denied" @@ -1198,13 +1238,25 @@ size_t webui_new_window_id(size_t num) { return 0; // Check window ID - if (num < 1 || num > WEBUI_MAX_IDS) + if (num < 1 || num >= WEBUI_MAX_IDS) return 0; // Destroy the window if already exist - if (_webui.wins[num] != NULL) + _webui_mutex_lock(&_webui.mutex_windows); + bool exists = (_webui.wins[num] != NULL); + _webui_mutex_unlock(&_webui.mutex_windows); + if (exists) webui_destroy(num); + // Callback-triggered destruction is deferred. Never overwrite its slot: + // the old server and a replacement would otherwise share the same numeric + // identity while the old generation is still retiring. + _webui_mutex_lock(&_webui.mutex_windows); + if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[num] != NULL) { + _webui_mutex_unlock(&_webui.mutex_windows); + return 0; + } + // Create a new window _webui_window_t* win = (_webui_window_t* ) _webui_malloc(sizeof(_webui_window_t)); _webui.wins[num] = win; @@ -1215,8 +1267,10 @@ size_t webui_new_window_id(size_t num) { _webui_mutex_init(&win->mutex_webview_update); _webui_mutex_init(&win->mutex_win_server_running); _webui_condition_init(&win->condition_webview_update); + _webui_condition_init(&win->condition_win_lifecycle); // Initialisation + win->accepting_tasks = true; win->ws_block = _webui.config.ws_block; win->num = num; win->browser_path = (char*)_webui_malloc(WEBUI_MAX_PATH); @@ -1239,6 +1293,8 @@ size_t webui_new_window_id(size_t num) { // Auto bind JavaScript-Bridge Core API Handler webui_bind(num, "__webui_core_api__", _webui_bridge_api_handler); + _webui_mutex_unlock(&_webui.mutex_windows); + #ifdef WEBUI_LOG _webui_log_info("[User] webui_new_window_id() -> New window #%zu @ 0x%p\n", num, win); #endif @@ -1257,12 +1313,15 @@ size_t webui_get_new_window_id(void) { if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) return 0; + _webui_mutex_lock(&_webui.mutex_windows); for (size_t i = 1; i < WEBUI_MAX_IDS; i++) { if (_webui.wins[i] == NULL && !_webui.wins_reserved[i]) { _webui.wins_reserved[i] = true; + _webui_mutex_unlock(&_webui.mutex_windows); return i; } } + _webui_mutex_unlock(&_webui.mutex_windows); // We should never reach here WEBUI_ASSERT("webui_get_new_window_id() failed"); @@ -1493,48 +1552,77 @@ void webui_destroy(size_t window) { // Initialization _webui_init(); - // Dereference - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[window] == NULL) + // Dereference while serialized with finalization. This prevents two + // concurrent destroy callers from acquiring a lifecycle mutex that the + // first caller has just reclaimed. + _webui_mutex_lock(&_webui.mutex_windows); + if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[window] == NULL) { + _webui_mutex_unlock(&_webui.mutex_windows); return; + } _webui_window_t* win = _webui.wins[window]; - if (_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) { + // Destruction has two phases. First prevent new work and ask the server to + // stop. Memory is reclaimed only after every thread retaining this window + // has retired. A user callback cannot wait for that point because CivetWeb + // may be waiting for the callback itself, so the server owns finalization + // for the first destroy request made from a callback. + bool in_callback = (_webui_callback_window == win); + bool first_request = false; + _webui_mutex_lock(&win->mutex_win_server_running); + if (!win->destroy_requested) { + first_request = true; + win->destroy_requested = true; + win->accepting_tasks = false; + win->destroy_server_owned = in_callback; + } + bool server_owned = win->destroy_server_owned; + bool server_active = win->server_thread_active; + _webui_condition_broadcast(&win->condition_win_lifecycle); + _webui_mutex_unlock(&win->mutex_win_server_running); + _webui_mutex_unlock(&_webui.mutex_windows); - // Freindly close - webui_close(window); + if (server_active) { + if (_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) + webui_close(window); + _webui_mutex_win_is_exit_now(win, WEBUI_MUTEX_SET_TRUE); + } - // Wait for server threads to stop - _webui_timer_t timer_1; - _webui_timer_start(&timer_1); - for (;;) { - _webui_sleep(10); - if (!_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) - break; - if (_webui_timer_is_end(&timer_1, 2500)) - break; - } + if (in_callback) + return; - if (_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) { + if (!first_request || server_owned) { + // Another destroy caller or the server owns finalization and may free + // the per-window lifecycle primitives. Wait on the process-wide + // registry condition instead of touching `win` again. + _webui_mutex_lock(&_webui.mutex_windows); + while (_webui.wins[window] == win) + _webui_condition_wait(&_webui.condition_windows, &_webui.mutex_windows); + _webui_mutex_unlock(&_webui.mutex_windows); + return; + } - #ifdef WEBUI_LOG - _webui_log_info("[User] webui_destroy([%zu]) -> Forced close\n", window); - #endif + _webui_finalize_window_when_idle(win); +} - // Forced close - _webui_mutex_is_connected(win, WEBUI_MUTEX_SET_FALSE); +static void _webui_finalize_window_when_idle(_webui_window_t* win) { - // Wait for server threads to stop - _webui_timer_t timer_2; - _webui_timer_start(&timer_2); - for (;;) { - _webui_sleep(100); - if (!_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) - break; - if (_webui_timer_is_end(&timer_2, 1500)) - break; - } - } - } + _webui_mutex_lock(&win->mutex_win_server_running); + while (win->server_thread_active || win->active_tasks > 0 || win->active_show_calls > 0) + _webui_condition_wait(&win->condition_win_lifecycle, &win->mutex_win_server_running); + win->destroy_finalizing = true; + _webui_mutex_unlock(&win->mutex_win_server_running); + + _webui_finalize_window(win); +} + +static void _webui_finalize_window(_webui_window_t* win) { + + size_t window = win->num; + + // Keep new destroy callers from resolving this window while its lifecycle + // primitives and allocation are being reclaimed. + _webui_mutex_lock(&_webui.mutex_windows); // Free memory resources _webui_free_mem((void*)win->url); @@ -1554,15 +1642,22 @@ void webui_destroy(size_t window) { // Free Mutex _webui_condition_destroy(&win->condition_webview_update); + _webui_condition_destroy(&win->condition_win_lifecycle); _webui_mutex_destroy(&win->mutex_webview_update); _webui_mutex_destroy(&win->mutex_win_exit_now); _webui_mutex_destroy(&win->mutex_win_reusable); _webui_mutex_destroy(&win->mutex_win_server_running); - // Free window struct - _webui_free_mem((void*)_webui.wins[window]); - _webui.wins[window] = NULL; - _webui.wins_reserved[window] = false; + // Publish completion only after all resources, including the window + // allocation itself, are gone. This also wakes a synchronous destroy call + // that arrived after callback-triggered deferred destruction began. + _webui_free_mem((void*)win); + if (_webui.wins[window] == win) { + _webui.wins[window] = NULL; + _webui.wins_reserved[window] = false; + } + _webui_condition_broadcast(&_webui.condition_windows); + _webui_mutex_unlock(&_webui.mutex_windows); } bool webui_is_shown(size_t window) { @@ -1808,6 +1903,17 @@ void webui_clean(void) { // Initialization _webui_init(); + // Global cleanup cannot safely complete while the server is waiting for + // this callback to return. Request application shutdown, but leave final + // process-wide reclamation to a caller after webui_wait(). + if (_webui_callback_window != NULL) { + #ifdef WEBUI_LOG + _webui_log_info("[User] webui_clean() -> Deferred from user callback; call again after webui_wait().\n"); + #endif + webui_exit(); + return; + } + // Final memory cleaning _webui_clean(); } @@ -2021,14 +2127,15 @@ const char* webui_start_server(size_t window, const char* content) { // Initialization _webui_init(); - // Dereference - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[window] == NULL) + _webui_window_t* win = NULL; + if (!_webui_window_show_begin(window, &win)) return ""; - _webui_window_t* win = _webui.wins[window]; // Check - if (_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) + if (_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) { + _webui_window_show_end(win); return ""; + } // Make `wait()` waits forever webui_set_timeout(0); @@ -2040,14 +2147,22 @@ const char* webui_start_server(size_t window, const char* content) { if (_webui_is_empty(content)) { started = _webui_show_window(win, NULL, win->server_root_path, WEBUI_SHOW_FOLDER, NoBrowser); } else { - started = webui_show_browser(window, content, NoBrowser); + win->allow_browser = true; + win->allow_webview = false; + started = _webui_show(win, NULL, content, NoBrowser); } - if (started) { - return win->url; + const char* result = ""; + if (started && win->url != NULL) { + // Keep one result buffer per window so starting another window does + // not overwrite a URL already returned to consuming code. + _webui_mutex_lock(&_webui.mutex_windows); + WEBUI_SN_PRINTF_STATIC(_webui.server_urls[window], sizeof(_webui.server_urls[window]), "%s", win->url); + result = _webui.server_urls[window]; + _webui_mutex_unlock(&_webui.mutex_windows); } - - return ""; + _webui_window_show_end(win); + return result; } bool webui_show_client(webui_event_t* e, const char* content) { @@ -2059,17 +2174,18 @@ bool webui_show_client(webui_event_t* e, const char* content) { // Initialization _webui_init(); - // Dereference - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[e->window] == NULL) + _webui_window_t* win = NULL; + if (!_webui_window_show_begin(e->window, &win)) return false; - _webui_window_t* win = _webui.wins[e->window]; // Show the window using WebView or using any browser win->allow_browser = true; win->allow_webview = true; // Show for single a client - return _webui_show(win, _webui.clients[e->connection_id], content, AnyBrowser); + bool status = _webui_show(win, _webui.clients[e->connection_id], content, AnyBrowser); + _webui_window_show_end(win); + return status; } #ifdef _WIN32 @@ -2143,17 +2259,18 @@ bool webui_show(size_t window, const char* content) { // Initialization _webui_init(); - // Dereference - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[window] == NULL) + _webui_window_t* win = NULL; + if (!_webui_window_show_begin(window, &win)) return false; - _webui_window_t* win = _webui.wins[window]; // Show the window using WebView or using any browser win->allow_browser = true; win->allow_webview = true; // Show for all connected clients - return _webui_show(win, NULL, content, AnyBrowser); + bool status = _webui_show(win, NULL, content, AnyBrowser); + _webui_window_show_end(win); + return status; } bool webui_show_wv(size_t window, const char* content) { @@ -2165,17 +2282,18 @@ bool webui_show_wv(size_t window, const char* content) { // Initialization _webui_init(); - // Dereference - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[window] == NULL) + _webui_window_t* win = NULL; + if (!_webui_window_show_begin(window, &win)) return false; - _webui_window_t* win = _webui.wins[window]; // Show the window using WebView only win->allow_browser = false; win->allow_webview = true; // Show for all connected clients - return _webui_show(win, NULL, content, Webview); + bool status = _webui_show(win, NULL, content, Webview); + _webui_window_show_end(win); + return status; } bool webui_show_browser(size_t window, const char* content, size_t browser) { @@ -2187,17 +2305,18 @@ bool webui_show_browser(size_t window, const char* content, size_t browser) { // Initialization _webui_init(); - // Dereference - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || _webui.wins[window] == NULL) + _webui_window_t* win = NULL; + if (!_webui_window_show_begin(window, &win)) return false; - _webui_window_t* win = _webui.wins[window]; // Show the window using a specific browser only win->allow_browser = (browser == Webview ? false : true); win->allow_webview = (browser == Webview ? true : false); // Show for all connected clients - return _webui_show(win, NULL, content, browser); + bool status = _webui_show(win, NULL, content, browser); + _webui_window_show_end(win); + return status; } void* webui_get_context(webui_event_t* e) { @@ -3842,13 +3961,15 @@ void webui_exit(void) { // Stop all threads _webui_mutex_app_is_exit_now(WEBUI_MUTEX_SET_TRUE); - // Let's give other threads more time to - // safely exit and finish cleaning up. - for (size_t i = 0; i < 4; i++) { - _webui_sleep(500); - if (_webui_servers_get() < 1) { - // No more server threads are running - break; + // A callback must return before server shutdown can complete. Other + // callers retain the existing short grace period. + if (_webui_callback_window == NULL) { + for (size_t i = 0; i < 4; i++) { + _webui_sleep(500); + if (_webui_servers_get() < 1) { + // No more server threads are running + break; + } } } @@ -4340,7 +4461,11 @@ static void _webui_interface_bind_handler_all(webui_event_t* e) { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t_webui_interface_bind_handler_all() -> Calling user all-events callback at address 0x%p\n[Call]\n", win->cb_interface[events_cb_index]); #endif - win->cb_interface[events_cb_index](e->window, e->event_type, e->element, e->event_number, e->bind_id); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + win->cb_interface[events_cb_index](e->window, e->event_type, e->element, e->event_number, e->bind_id); + _webui_user_callback_end(win, previous); + } } } } @@ -4378,7 +4503,11 @@ static void _webui_interface_bind_handler(webui_event_t* e) { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t_webui_interface_bind_handler() -> Calling user callback at address 0x%p\n[Call]\n", win->cb_interface[cb_index]); #endif - win->cb_interface[cb_index](e->window, e->event_type, e->element, e->event_number, e->bind_id); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + win->cb_interface[cb_index](e->window, e->event_type, e->element, e->event_number, e->bind_id); + _webui_user_callback_end(win, previous); + } } } @@ -4398,8 +4527,9 @@ static void _webui_interface_bind_handler(webui_event_t* e) { if (event_inf->done) done = true; _webui_mutex_unlock(&_webui.mutex_async_response); - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) { - break; // App is exiting, Stop waiting. + if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || + _webui_mutex_is_window_stopping(win)) { + break; // App or window is exiting, Stop waiting. } } } @@ -5525,9 +5655,17 @@ static const void* _webui_call_external_file_handler_cb(_webui_window_t* win, co // Call user callback const void* callback_resp = NULL; if (win->files_handler_window != NULL) { - callback_resp = win->files_handler_window(win->num, path, (int*)length); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + callback_resp = win->files_handler_window(win->num, path, (int*)length); + _webui_user_callback_end(win, previous); + } } else if (win->files_handler != NULL) { - callback_resp = win->files_handler(path, (int*)length); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + callback_resp = win->files_handler(path, (int*)length); + _webui_user_callback_end(win, previous); + } } else { // No callback return NULL; @@ -5552,8 +5690,9 @@ static const void* _webui_call_external_file_handler_cb(_webui_window_t* win, co bool done = false; while (!done) { - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) { - break; // App is exiting, Stop waiting. + if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || + _webui_mutex_is_window_stopping(win)) { + break; // App or window is exiting, Stop waiting. } _webui_sleep(10); @@ -5916,6 +6055,15 @@ static void _webui_condition_signal(webui_condition_t* cond) { #endif } +static void _webui_condition_broadcast(webui_condition_t* cond) { + + #ifdef _WIN32 + WakeAllConditionVariable(cond); + #else + pthread_cond_broadcast(cond); + #endif +} + static void _webui_condition_destroy(webui_condition_t* cond) { #ifdef _WIN32 @@ -6340,6 +6488,14 @@ static void _webui_showing_count(int delta) { _webui_update_wait_state(); } +static bool _webui_lifecycle_users_exist(void) { + + _webui_mutex_lock(&_webui.mutex_is_more_servers); + bool active = (_webui.servers > 0 || _webui.showing > 0); + _webui_mutex_unlock(&_webui.mutex_is_more_servers); + return active; +} + static bool _webui_wait_is_needed(void) { // Should `wait()` keep waiting? @@ -6388,6 +6544,158 @@ static bool _webui_mutex_is_server_running(_webui_window_t* win, int update) { return status; } +static bool _webui_mutex_try_start_server_thread(_webui_window_t* win) { + + bool can_start = false; + _webui_mutex_lock(&win->mutex_win_server_running); + if (!win->destroy_requested && !win->server_thread_active) { + win->server_thread_active = true; + win->accepting_tasks = true; + can_start = true; + } + _webui_mutex_unlock(&win->mutex_win_server_running); + return can_start; +} + +static bool _webui_mutex_server_thread_stopped(_webui_window_t* win) { + + _webui_mutex_lock(&win->mutex_win_server_running); + win->server_running = false; + win->server_thread_active = false; + if (!win->destroy_requested) + win->accepting_tasks = true; + bool finalize = win->destroy_requested && win->destroy_server_owned && + !win->destroy_finalizing && win->active_tasks == 0 && win->active_show_calls == 0; + if (finalize) + win->destroy_finalizing = true; + _webui_condition_broadcast(&win->condition_win_lifecycle); + _webui_mutex_unlock(&win->mutex_win_server_running); + return finalize; +} + +static bool _webui_mutex_is_window_stopping(_webui_window_t* win) { + + _webui_mutex_lock(&win->mutex_win_server_running); + bool stopping = win->destroy_requested || !win->accepting_tasks; + _webui_mutex_unlock(&win->mutex_win_server_running); + return stopping; +} + +static bool _webui_window_prepare_show(_webui_window_t* win) { + + // Serialize clearing the previous exit signal with a concurrent destroy + // request. Once destruction is requested, a show call must never erase the + // signal that the server thread relies on to leave its main loop. + _webui_mutex_lock(&win->mutex_win_server_running); + bool can_show = !win->destroy_requested; + if (can_show) + _webui_mutex_win_is_exit_now(win, WEBUI_MUTEX_SET_FALSE); + _webui_mutex_unlock(&win->mutex_win_server_running); + return can_show; +} + +static bool _webui_window_task_begin(_webui_window_t* win) { + + bool admitted = false; + _webui_mutex_lock(&win->mutex_win_server_running); + if (win->accepting_tasks && !win->destroy_requested) { + win->active_tasks++; + admitted = true; + } + _webui_mutex_unlock(&win->mutex_win_server_running); + return admitted; +} + +static void _webui_window_task_end(_webui_window_t* win) { + + _webui_mutex_lock(&win->mutex_win_server_running); + if (win->active_tasks > 0) + win->active_tasks--; + bool finalize = win->destroy_requested && win->destroy_server_owned && + !win->destroy_finalizing && !win->server_thread_active && + win->active_tasks == 0 && win->active_show_calls == 0; + if (finalize) + win->destroy_finalizing = true; + _webui_condition_broadcast(&win->condition_win_lifecycle); + _webui_mutex_unlock(&win->mutex_win_server_running); + + if (finalize) + _webui_finalize_window(win); +} + +static bool _webui_window_show_begin(size_t window, _webui_window_t** win) { + + if (window < 1 || window >= WEBUI_MAX_IDS) + return false; + + bool admitted = false; + _webui_mutex_lock(&_webui.mutex_windows); + if (_webui.accepting_show_calls && + !_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) && _webui.wins[window] != NULL) { + _webui_window_t* candidate = _webui.wins[window]; + _webui_mutex_lock(&candidate->mutex_win_server_running); + if (!candidate->destroy_requested) { + candidate->active_show_calls++; + *win = candidate; + admitted = true; + _webui_showing_count(+1); + } + _webui_mutex_unlock(&candidate->mutex_win_server_running); + } + _webui_mutex_unlock(&_webui.mutex_windows); + return admitted; +} + +static void _webui_window_show_end(_webui_window_t* win) { + + _webui_mutex_lock(&win->mutex_win_server_running); + if (win->active_show_calls > 0) + win->active_show_calls--; + bool finalize = win->destroy_requested && win->destroy_server_owned && + !win->destroy_finalizing && !win->server_thread_active && + win->active_tasks == 0 && win->active_show_calls == 0; + if (finalize) + win->destroy_finalizing = true; + _webui_condition_broadcast(&win->condition_win_lifecycle); + _webui_mutex_unlock(&win->mutex_win_server_running); + + if (finalize) + _webui_finalize_window(win); + + _webui_showing_count(-1); +} + +static void _webui_window_stop_tasks(_webui_window_t* win) { + + _webui_mutex_lock(&win->mutex_win_server_running); + win->accepting_tasks = false; + _webui_condition_broadcast(&win->condition_win_lifecycle); + _webui_mutex_unlock(&win->mutex_win_server_running); +} + +static void _webui_window_wait_for_tasks(_webui_window_t* win) { + + _webui_mutex_lock(&win->mutex_win_server_running); + while (win->active_tasks > 0) + _webui_condition_wait(&win->condition_win_lifecycle, &win->mutex_win_server_running); + _webui_mutex_unlock(&win->mutex_win_server_running); +} + +static bool _webui_user_callback_begin(_webui_window_t* win, _webui_window_t** previous) { + + if (!_webui_window_task_begin(win)) + return false; + *previous = _webui_callback_window; + _webui_callback_window = win; + return true; +} + +static void _webui_user_callback_end(_webui_window_t* win, _webui_window_t* previous) { + + _webui_callback_window = previous; + _webui_window_task_end(win); +} + static bool _webui_mutex_win_is_exit_now(_webui_window_t* win, int update) { bool status = false; @@ -7830,18 +8138,21 @@ static void _webui_clean(void) { return; cleaned = true; + // Close show admission before requesting exit. A show admitted before this + // gate owns the process-wide showing count until its last window access; + // calls arriving afterward are rejected without touching window storage. + _webui_mutex_lock(&_webui.mutex_windows); + _webui.accepting_show_calls = false; + _webui_mutex_unlock(&_webui.mutex_windows); + // Make sure app is stopped webui_exit(); - // Let's give other threads more time to - // safely exit and finish cleaning up before - // cleaning memory. - for (size_t i = 0; i < 4; i++) { - _webui_sleep(500); - if (_webui_servers_get() < 1) { - break; // No more server threads are running - } - } + // Server threads and public show calls own references to process-wide + // services and allocations. Wait for their complete lifetimes before + // reclaiming either window memory or global synchronization primitives. + while (_webui_lifecycle_users_exist()) + _webui_sleep(1); // Clean all servers services mg_exit_library(); @@ -7851,7 +8162,6 @@ static void _webui_clean(void) { // Destroy all mutex _webui_mutex_destroy(&_webui.mutex_server_start); - _webui_mutex_destroy(&_webui.mutex_send); _webui_mutex_destroy(&_webui.mutex_receive); _webui_mutex_destroy(&_webui.mutex_wait); _webui_mutex_destroy(&_webui.mutex_js_run); @@ -7866,6 +8176,8 @@ static void _webui_clean(void) { _webui_mutex_destroy(&_webui.mutex_js_run_id); _webui_mutex_destroy(&_webui.mutex_ws_process_number); _webui_condition_destroy(&_webui.condition_wait); + _webui_condition_destroy(&_webui.condition_windows); + _webui_mutex_destroy(&_webui.mutex_windows); _webui.initialized = false; @@ -9145,6 +9457,8 @@ static void _webui_start_server_thread(_webui_window_t* win) { // Start the server thread of a window. The thread is counted in before it is // created so that `wait()` cannot exit in the gap between this call and the // thread actually starting. + if (!_webui_mutex_try_start_server_thread(win)) + return; _webui_servers_count(+1); @@ -9153,8 +9467,12 @@ static void _webui_start_server_thread(_webui_window_t* win) { win->server_thread = thread; if (thread != NULL) CloseHandle(thread); - else + else { + bool finalize = _webui_mutex_server_thread_stopped(win); + if (finalize) + _webui_finalize_window(win); _webui_servers_count(-1); // Thread creation failed + } #else pthread_t thread; if (pthread_create(&thread, NULL, &_webui_server_thread, (void*)win) == 0) { @@ -9162,11 +9480,37 @@ static void _webui_start_server_thread(_webui_window_t* win) { win->server_thread = thread; } else { + bool finalize = _webui_mutex_server_thread_stopped(win); + if (finalize) + _webui_finalize_window(win); _webui_servers_count(-1); // Thread creation failed } #endif } +static bool _webui_start_webview_thread(_webui_window_t* win) { + + if (!_webui_window_task_begin(win)) + return false; + + #ifdef _WIN32 + HANDLE thread = CreateThread(NULL, 0, _webui_webview_thread, (void*)win, 0, NULL); + if (thread != NULL) { + CloseHandle(thread); + return true; + } + #else + pthread_t thread; + if (pthread_create(&thread, NULL, &_webui_webview_thread, (void*)win) == 0) { + pthread_detach(thread); + return true; + } + #endif + + _webui_window_task_end(win); + return false; +} + static bool _webui_show_window_impl(_webui_window_t* win, struct mg_connection* client, const char* content, int type, size_t browser) { #ifdef WEBUI_LOG @@ -9180,8 +9524,9 @@ static bool _webui_show_window_impl(_webui_window_t* win, struct mg_connection* _webui_log_debug("[Core]\t\t_webui_show_window(FILE, [%zu])\n", browser); #endif - // Stop any window exit signal as we are going to show a window - _webui_mutex_win_is_exit_now(win, WEBUI_MUTEX_SET_FALSE); + // Stop any previous window exit signal unless destruction won the race. + if (!_webui_window_prepare_show(win)) + return false; #if __linux__ GTK_WEBVIEW_SET_IN_SHOW(win, true) @@ -9566,6 +9911,10 @@ static bool _webui_show_window_impl(_webui_window_t* win, struct mg_connection* // Stop if window is connected & token is valid if (_webui_mutex_is_single_client_token_valid(win, WEBUI_MUTEX_GET_STATUS)) break; + + // Stop if this show is racing window/server teardown. + if (_webui_mutex_is_window_stopping(win)) + break; // Stop if timer is finished if (_webui_timer_is_end(&timer, (timeout * 1000))) @@ -9584,6 +9933,10 @@ static bool _webui_show_window_impl(_webui_window_t* win, struct mg_connection* // Stop if window is connected & token is valid if (_webui_mutex_is_single_client_token_valid(win, WEBUI_MUTEX_GET_STATUS)) break; + + // Stop if this show is racing window/server teardown. + if (_webui_mutex_is_window_stopping(win)) + break; // Stop if timer is finished if (_webui_timer_is_end(&timer, (timeout * 1000))) @@ -9601,15 +9954,9 @@ static bool _webui_show_window_impl(_webui_window_t* win, struct mg_connection* static bool _webui_show_window(_webui_window_t* win, struct mg_connection* client, const char* content, int type, size_t browser) { - // Re-showing a window stops its old server thread before starting the new - // one, so the servers counter can legitimately drop to zero in the middle - // of this call. Counting the show itself keeps `wait()` from exiting in - // that gap, and in the gap before the very first server thread starts. - _webui_showing_count(+1); - bool status = _webui_show_window_impl(win, client, content, type, browser); - _webui_showing_count(-1); - - return status; + // The public show admission guard owns both the process-wide show count + // and the per-window reference across this entire call chain. + return _webui_show_window_impl(win, client, content, type, browser); } static void _webui_window_event( @@ -9642,7 +9989,11 @@ static void _webui_window_event( _webui_log_debug("[Call]\n"); #endif e.bind_id = events_cb_index; - win->cb[events_cb_index](&e); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + win->cb[events_cb_index](&e); + _webui_user_callback_end(win, previous); + } } } @@ -9658,7 +10009,11 @@ static void _webui_window_event( _webui_log_debug("[Call]\n"); #endif e.bind_id = cb_index; - win->cb[cb_index](&e); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + win->cb[cb_index](&e); + _webui_user_callback_end(win, previous); + } } } } @@ -9679,8 +10034,9 @@ static void _webui_window_event( if (event_inf->done) done = true; _webui_mutex_unlock(&_webui.mutex_async_response); - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) { - break; // App is exiting, Stop waiting. + if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || + _webui_mutex_is_window_stopping(win)) { + break; // App or window is exiting, Stop waiting. } } } @@ -9714,10 +10070,10 @@ static void _webui_send_client_ws(_webui_window_t* win, struct mg_connection* cl int ret = 0; if (win->num > 0 && win->num < WEBUI_MAX_IDS) { if (client != NULL) { - // Mutex - _webui_mutex_lock(&_webui.mutex_send); + // CivetWeb serializes each connection internally. Avoid a + // process-wide send lock so stopping one window cannot be held up + // by a blocked write belonging to another window. ret = mg_websocket_write(client, MG_WEBSOCKET_OPCODE_BINARY, packet, packets_size); - _webui_mutex_unlock(&_webui.mutex_send); } } @@ -9818,7 +10174,6 @@ static void _webui_init(void) { // Initializing mutex _webui_mutex_init(&_webui.mutex_server_start); - _webui_mutex_init(&_webui.mutex_send); _webui_mutex_init(&_webui.mutex_receive); _webui_mutex_init(&_webui.mutex_wait); _webui_mutex_init(&_webui.mutex_bridge); @@ -9833,7 +10188,9 @@ static void _webui_init(void) { _webui_mutex_init(&_webui.mutex_token); _webui_mutex_init(&_webui.mutex_js_run_id); _webui_mutex_init(&_webui.mutex_ws_process_number); + _webui_mutex_init(&_webui.mutex_windows); _webui_condition_init(&_webui.condition_wait); + _webui_condition_init(&_webui.condition_windows); // Random #ifdef _WIN32 @@ -9843,6 +10200,7 @@ static void _webui_init(void) { #endif // Initializing core + _webui.accepting_show_calls = true; _webui.startup_timeout = WEBUI_DEF_TIMEOUT; _webui.executable_path = _webui_get_current_path(); _webui.default_server_root_path = (char*)_webui_malloc(WEBUI_MAX_PATH); @@ -10773,10 +11131,14 @@ static WEBUI_THREAD_SERVER_START { // Mutex _webui_mutex_lock(&_webui.mutex_server_start); + _webui_window_t* thread_win = (_webui_window_t*)arg; _webui_window_t* win = _webui_dereference_win_ptr(arg); if (win == NULL || _webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS)) { _webui_mutex_unlock(&_webui.mutex_server_start); // This thread was counted in by `_webui_start_server_thread()` + bool finalize = _webui_mutex_server_thread_stopped(thread_win); + if (finalize) + _webui_finalize_window(thread_win); _webui_servers_count(-1); WEBUI_THREAD_RETURN } @@ -10970,14 +11332,11 @@ static WEBUI_THREAD_SERVER_START { // Folder monitor thread if (_webui.config.folder_monitor && !monitor_created) { - monitor_created = true; #ifdef _WIN32 monitor_thread = CreateThread(NULL, 0, _webui_folder_monitor_thread, (void*)win, 0, NULL); - if (monitor_thread != NULL) - CloseHandle(monitor_thread); + monitor_created = (monitor_thread != NULL); #else - pthread_create(&monitor_thread, NULL, &_webui_folder_monitor_thread, (void*)win); - pthread_detach(monitor_thread); + monitor_created = (pthread_create(&monitor_thread, NULL, &_webui_folder_monitor_thread, (void*)win) == 0); #endif } @@ -11075,14 +11434,11 @@ static WEBUI_THREAD_SERVER_START { // Folder monitor thread if (_webui.config.folder_monitor && !monitor_created) { - monitor_created = true; #ifdef _WIN32 monitor_thread = CreateThread(NULL, 0, _webui_folder_monitor_thread, (void*)win, 0, NULL); - if (monitor_thread != NULL) - CloseHandle(monitor_thread); + monitor_created = (monitor_thread != NULL); #else - pthread_create(&monitor_thread, NULL, &_webui_folder_monitor_thread, (void*)win); - pthread_detach(monitor_thread); + monitor_created = (pthread_create(&monitor_thread, NULL, &_webui_folder_monitor_thread, (void*)win) == 0); #endif } @@ -11132,6 +11488,31 @@ static WEBUI_THREAD_SERVER_START { #endif } + // Stop admitting detached work and new CivetWeb requests, then interrupt + // existing connection I/O. Worker connection storage stays alive until + // mg_stop(), after all detached users of those connections have retired. + _webui_window_stop_tasks(win); + webui_civetweb_shutdown_context_connections(http_ctx); + + // The monitor observes the task-admission stop flag and polls its OS + // watcher with a bounded timeout, so it can unwind without forced thread + // cancellation while holding a WebUI or CivetWeb lock. + if (monitor_created) { + #ifdef WEBUI_LOG + _webui_log_debug("[Core]\t\t_webui_server_thread([%zu]) -> Waiting for folder monitor thread\n", win->num); + #endif + #ifdef _WIN32 + WaitForSingleObject(monitor_thread, INFINITE); + CloseHandle(monitor_thread); + #else + pthread_join(monitor_thread, NULL); + #endif + } + + // WebView and WebSocket tasks already in flight finish before CivetWeb + // releases the client/context objects they retain. + _webui_window_wait_for_tasks(win); + // Stop server services mg_stop(http_ctx); @@ -11139,9 +11520,6 @@ static WEBUI_THREAD_SERVER_START { _webui_free_port(win->server_port); _webui_free_mem((void*)server_port); - // Mutex - _webui_mutex_is_server_running(win, WEBUI_MUTEX_SET_FALSE); - #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t_webui_server_thread([%zu]) -> Server stopped.\n", win->num); @@ -11151,26 +11529,18 @@ static WEBUI_THREAD_SERVER_START { // call `webui_show()` again if needed. _webui_make_window_reusable(win); + // This must be the final access through the window pointer. destroy() can + // free the window and its mutexes as soon as the lifetime flag is cleared. + bool finalize = _webui_mutex_server_thread_stopped(win); + + if (finalize) + _webui_finalize_window(win); + // Let the main wait() know that this server thread is finished. This // breaks the main loop when nothing else is left running (no other // server thread, and no `webui_show()` call in progress). _webui_servers_count(-1); - // Clean monitor thread - if (_webui.config.folder_monitor && monitor_created) { - #ifdef WEBUI_LOG - _webui_log_debug("[Core]\t\t_webui_server_thread([%zu]) -> Killing folder monitor thread\n", win->num); - #endif - #ifdef _WIN32 - TerminateThread(monitor_thread, 0); - CloseHandle(monitor_thread); - #else - if (monitor_thread) { - pthread_cancel(monitor_thread); - } - #endif - } - WEBUI_THREAD_RETURN } @@ -11380,6 +11750,10 @@ static void _webui_receive(_webui_window_t* win, struct mg_connection* client, // -- New Method -- // Process the packet always in a new thread + if (!_webui_window_task_begin(win)) { + _webui_free_mem((void*)arg_ptr); + return; + } _webui_recv_arg_t* arg = (_webui_recv_arg_t* ) _webui_malloc(sizeof(_webui_recv_arg_t)); arg->win = win; arg->ptr = arg_ptr; @@ -11392,10 +11766,20 @@ static void _webui_receive(_webui_window_t* win, struct mg_connection* client, HANDLE thread = CreateThread(NULL, 0, _webui_ws_process_thread, (void*)arg, 0, NULL); if (thread != NULL) CloseHandle(thread); + else { + _webui_free_mem((void*)arg->ptr); + _webui_free_mem((void*)arg); + _webui_window_task_end(win); + } #else pthread_t thread; - pthread_create(&thread, NULL, &_webui_ws_process_thread, (void*)arg); - pthread_detach(thread); + if (pthread_create(&thread, NULL, &_webui_ws_process_thread, (void*)arg) == 0) + pthread_detach(thread); + else { + _webui_free_mem((void*)arg->ptr); + _webui_free_mem((void*)arg); + _webui_window_task_end(win); + } #endif } @@ -11873,7 +12257,11 @@ static void _webui_ws_process( ); #endif e.bind_id = cb_index; - win->cb[cb_index](&e); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + win->cb[cb_index](&e); + _webui_user_callback_end(win, previous); + } // Async response wait if (_webui.config.asynchronous_response) { @@ -11889,8 +12277,9 @@ static void _webui_ws_process( if (event_inf->done) done = true; _webui_mutex_unlock(&_webui.mutex_async_response); - if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) { - break; // App is exiting, Stop waiting. + if (_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) || + _webui_mutex_is_window_stopping(win)) { + break; // App or window is exiting, Stop waiting. } } } @@ -12219,6 +12608,7 @@ static WEBUI_THREAD_RECEIVE { // Get arguments _webui_recv_arg_t* arg = (_webui_recv_arg_t* ) _arg; + _webui_window_t* win = arg->win; // Process _webui_ws_process(arg->win, arg->client, arg->connection_id, arg->ptr, arg->len, arg->recvNum, arg->event_type); @@ -12226,6 +12616,7 @@ static WEBUI_THREAD_RECEIVE { // Free _webui_free_mem((void*)arg->ptr); _webui_free_mem((void*)arg); + _webui_window_task_end(win); WEBUI_THREAD_RETURN } @@ -12717,7 +13108,11 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { if (win) { bool can_close = true; if (win->close_handler_wv != NULL) { - can_close = win->close_handler_wv(win->num); + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + can_close = win->close_handler_wv(win->num); + _webui_user_callback_end(win, previous); + } #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\tClose handler installed for %zu, result = %d\n", win->num, can_close); #endif @@ -12844,15 +13239,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_webview_update(win); // Win32 WebView thread - #ifdef _WIN32 - HANDLE thread = CreateThread(NULL, 0, _webui_webview_thread, (void*)win, 0, NULL); - if (thread != NULL) - CloseHandle(thread); - #else - pthread_t thread; - pthread_create(&thread, NULL, &_webui_webview_thread, (void*)win); - pthread_detach(thread); - #endif + if (!_webui_start_webview_thread(win)) + return false; // Wait for WebView thread to start _webui_timer_t timer; @@ -12952,13 +13340,16 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_log_debug("[Core]\t\t[Thread .] _webui_webview_thread()\n"); #endif + _webui_window_t* thread_win = (_webui_window_t*)arg; _webui_window_t* win = _webui_dereference_win_ptr(arg); if (win == NULL || win->webView == NULL || win->webView->cpp_handle == NULL) { if (win && win->webView) { _webui_wv_free(win->webView); win->webView = NULL; } - _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + if (win) + _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -12967,6 +13358,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_wv_free(win->webView); win->webView = NULL; _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -12979,6 +13371,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_wv_free(win->webView); win->webView = NULL; _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -13006,6 +13399,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_wv_free(win->webView); win->webView = NULL; _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -13047,6 +13441,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_wv_free(win->webView); win->webView = NULL; _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -13178,6 +13573,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_log_debug("[Core]\t\t[Thread .] _webui_webview_thread() -> End\n"); #endif + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN }; @@ -13298,7 +13694,12 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_window_t* win = _webui_dereference_win_ptr(arg); if (win) { if (win->close_handler_wv) { - bool can_close = win->close_handler_wv(win->num); + bool can_close = true; + _webui_window_t* previous = NULL; + if (_webui_user_callback_begin(win, &previous)) { + can_close = win->close_handler_wv(win->num); + _webui_user_callback_end(win, previous); + } #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t_webui_wv_event_on_close() -> can_close = %d\n", can_close); #endif @@ -13646,15 +14047,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { // processed in one single thread for each window. // Linux WebView thread - #ifdef _WIN32 - HANDLE thread = CreateThread(NULL, 0, _webui_webview_thread, (void*)win, 0, NULL); - if (thread != NULL) - CloseHandle(thread); - #else - pthread_t thread; - pthread_create(&thread, NULL, &_webui_webview_thread, (void*)win); - pthread_detach(thread); - #endif + if (!_webui_start_webview_thread(win)) + return; // WebUI Exit Event for GTK g_timeout_add((1 * 1000), _webui_wv_exit_schedule, NULL); @@ -14053,11 +14447,10 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_log_debug("[Core]\t\t[Thread .] _webui_webview_thread()\n"); #endif + _webui_window_t* thread_win = (_webui_window_t*)arg; _webui_window_t* win = _webui_dereference_win_ptr(arg); if (win == NULL) { - _webui_wv_close(win->webView); - win->webView = NULL; - _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -14066,6 +14459,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_wv_close(win->webView); win->webView = NULL; _webui_mutex_is_webview_update(win, WEBUI_MUTEX_SET_FALSE); + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -14130,6 +14524,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_log_debug("[Core]\t\t[Thread .] _webui_webview_thread() -> End\n"); #endif + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } #else @@ -14303,15 +14698,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_webview_update(win); // macOS WebView thread - #ifdef _WIN32 - HANDLE thread = CreateThread(NULL, 0, _webui_webview_thread, (void*)win, 0, NULL); - if (thread != NULL) - CloseHandle(thread); - #else - pthread_t thread; - pthread_create(&thread, NULL, &_webui_webview_thread, (void*)win); - pthread_detach(thread); - #endif + if (!_webui_start_webview_thread(win)) + return false; // Wait for WebView thread to start _webui_timer_t timer; @@ -14337,10 +14725,10 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_log_debug("[Core]\t\t[Thread .] _webui_webview_thread()\n"); #endif + _webui_window_t* thread_win = (_webui_window_t*)arg; _webui_window_t* win = _webui_dereference_win_ptr(arg); if (win == NULL) { - _webui_wv_free(win->webView); - win->webView = NULL; + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } @@ -14410,6 +14798,7 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { _webui_log_debug("[Core]\t\t[Thread .] _webui_webview_thread() -> End\n"); #endif + _webui_window_task_end(thread_win); WEBUI_THREAD_RETURN } #endif @@ -14438,11 +14827,12 @@ static WEBUI_THREAD_MONITOR { #ifdef _WIN32 // Windows - HANDLE hDir = CreateFile( - win->server_root_path, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL + HANDLE change_notification = FindFirstChangeNotificationA( + win->server_root_path, TRUE, + FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE ); - if (hDir == INVALID_HANDLE_VALUE) { + if (change_notification == INVALID_HANDLE_VALUE) { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Failed to open folder: %s\n", win->server_root_path @@ -14453,37 +14843,33 @@ static WEBUI_THREAD_MONITOR { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Monitoring [%s]\n", win->server_root_path); #endif - char buffer[1024]; - DWORD bytesReturned; - while ((!_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) && - (_webui_mutex_is_server_running(win, WEBUI_MUTEX_GET_STATUS))) { - if (ReadDirectoryChangesW( - hDir, buffer, sizeof(buffer), TRUE, - FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | - FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE, &bytesReturned, NULL, NULL - )) - { + while (!_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) && + !_webui_mutex_is_window_stopping(win)) { + DWORD wait_status = WaitForSingleObject(change_notification, 100); + if (wait_status == WAIT_OBJECT_0) { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Folder updated\n"); #endif // Loop trough all connected clients in this window - for (size_t i = 0; i < WEBUI_MAX_IDS; i++) { + for (size_t i = 0; i < WEBUI_MAX_IDS && !_webui_mutex_is_window_stopping(win); i++) { if ((_webui.clients[i] != NULL) && (_webui.clients_win_num[i] == win->num) && (_webui_mutex_is_multi_client_token_valid(win, WEBUI_MUTEX_GET_STATUS, i))) { _webui_send_client(win, _webui.clients[i], 0, WEBUI_CMD_JS_QUICK, js, js_len, false); } } - } else { + if (!FindNextChangeNotification(change_notification)) + break; + } else if (wait_status != WAIT_TIMEOUT) { #ifdef WEBUI_LOG - _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Failed to read folder changes\n"); + _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Folder notification failed\n"); #endif break; } } - CloseHandle(hDir); + FindCloseChangeNotification(change_notification); #elif __linux__ // Linux - int fd = inotify_init(); + int fd = inotify_init1(IN_NONBLOCK); if (fd < 0) { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> inotify_init error\n"); @@ -14502,9 +14888,14 @@ static WEBUI_THREAD_MONITOR { _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Monitoring [%s]\n", win->server_root_path); #endif char buffer[1024]; - while (!_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) { + while (!_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) && + !_webui_mutex_is_window_stopping(win)) { int length = read(fd, buffer, sizeof(buffer)); if (length < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + _webui_sleep(50); + continue; + } #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> read error\n"); #endif @@ -14519,7 +14910,7 @@ static WEBUI_THREAD_MONITOR { _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Folder updated\n"); #endif // Loop trough all connected clients in this window - for (size_t i = 0; i < WEBUI_MAX_IDS; i++) { + for (size_t i = 0; i < WEBUI_MAX_IDS && !_webui_mutex_is_window_stopping(win); i++) { if ((_webui.clients[i] != NULL) && (_webui.clients_win_num[i] == win->num) && (_webui_mutex_is_multi_client_token_valid(win, WEBUI_MUTEX_GET_STATUS, i))) { _webui_send_client(win, _webui.clients[i], 0, WEBUI_CMD_JS_QUICK, js, js_len, false); @@ -14554,9 +14945,11 @@ static WEBUI_THREAD_MONITOR { #endif struct kevent change; EV_SET(&change, fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_ONESHOT, NOTE_WRITE, 0, NULL); - while (!_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS)) { + while (!_webui_mutex_app_is_exit_now(WEBUI_MUTEX_GET_STATUS) && + !_webui_mutex_is_window_stopping(win)) { struct kevent event; - int nev = kevent(kq, &change, 1, &event, 1, NULL); + struct timespec timeout = {0, 100000000}; + int nev = kevent(kq, &change, 1, &event, 1, &timeout); if (nev == -1) { #ifdef WEBUI_LOG _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> kevent error\n"); @@ -14568,7 +14961,7 @@ static WEBUI_THREAD_MONITOR { _webui_log_debug("[Core]\t\t[Thread .] _webui_folder_monitor_thread() -> Folder updated\n"); #endif // Loop trough all connected clients in this window - for (size_t i = 0; i < WEBUI_MAX_IDS; i++) { + for (size_t i = 0; i < WEBUI_MAX_IDS && !_webui_mutex_is_window_stopping(win); i++) { if ((_webui.clients[i] != NULL) && (_webui.clients_win_num[i] == win->num) && (_webui_mutex_is_multi_client_token_valid(win, WEBUI_MUTEX_GET_STATUS, i))) { _webui_send_client(win, _webui.clients[i], 0, WEBUI_CMD_JS_QUICK, js, js_len, false);