diff --git a/minimal-examples/ws-server/README.md b/minimal-examples/ws-server/README.md index 9d52d6d85..b8e7ec2e5 100644 --- a/minimal-examples/ws-server/README.md +++ b/minimal-examples/ws-server/README.md @@ -7,6 +7,7 @@ minimal-ws-server-pmd-corner|Corner-case tests for permessage-deflate minimal-ws-server-pmd|Simple ws server with permessage-deflate support minimal-ws-server-ring|Like minimal-ws-server but holds the chat in a multi-tail ringbuffer minimal-ws-server-threadpool|Demonstrates how to use a worker thread pool with lws +minimal-ws-server-threads-smp|SMP ws server where data is produced by different threads with multiple lws service threads too minimal-ws-server-threads|Simple ws server where data is produced by different threads minimal-ws-server|Serves an index.html over http that opens a ws shared chat client in a browser diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/CMakeLists.txt b/minimal-examples/ws-server/minimal-ws-server-threads-smp/CMakeLists.txt new file mode 100644 index 000000000..32ecbf5da --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 2.8) +include(CheckIncludeFile) +include(CheckCSourceCompiles) + +set(SAMP lws-minimal-ws-server-threads-smp) +set(SRCS minimal-ws-server.c) + +MACRO(require_pthreads result) + CHECK_INCLUDE_FILE(pthread.h LWS_HAVE_PTHREAD_H) + if (NOT LWS_HAVE_PTHREAD_H) + if (LWS_WITH_MINIMAL_EXAMPLES) + set(result 0) + else() + message(FATAL_ERROR "threading support requires pthreads") + endif() + endif() +ENDMACRO() + +# If we are being built as part of lws, confirm current build config supports +# reqconfig, else skip building ourselves. +# +# If we are being built externally, confirm installed lws was configured to +# support reqconfig, else error out with a helpful message about the problem. +# +MACRO(require_lws_config reqconfig _val result) + + if (DEFINED ${reqconfig}) + if (${reqconfig}) + set (rq 1) + else() + set (rq 0) + endif() + else() + set(rq 0) + endif() + + if (${_val} EQUAL ${rq}) + set(SAME 1) + else() + set(SAME 0) + endif() + + if (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME}) + if (${_val}) + message("${SAMP}: skipping as lws being built without ${reqconfig}") + else() + message("${SAMP}: skipping as lws built with ${reqconfig}") + endif() + set(${result} 0) + else() + if (LWS_WITH_MINIMAL_EXAMPLES) + set(MET ${SAME}) + else() + CHECK_C_SOURCE_COMPILES("#include \nint main(void) {\n#if defined(${reqconfig})\n return 0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_${reqconfig}) + if (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig}) + set(HAS_${reqconfig} 0) + else() + set(HAS_${reqconfig} 1) + endif() + if ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val})) + set(MET 1) + else() + set(MET 0) + endif() + endif() + if (NOT MET) + if (${_val}) + message(FATAL_ERROR "This project requires lws must have been configured with ${reqconfig}") + else() + message(FATAL_ERROR "Lws configuration of ${reqconfig} is incompatible with this project") + endif() + endif() + + endif() +ENDMACRO() + +set(requirements 1) +require_pthreads(requirements) +require_lws_config(LWS_ROLE_WS 1 requirements) +require_lws_config(LWS_WITHOUT_SERVER 0 requirements) + +if (requirements) + add_executable(${SAMP} ${SRCS}) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared pthread) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets pthread) + endif() +endif() diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/README.md b/minimal-examples/ws-server/minimal-ws-server-threads-smp/README.md new file mode 100644 index 000000000..81be3124d --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/README.md @@ -0,0 +1,39 @@ +# lws minimal ws server (threads) + SMP + +This demonstrates both independent threads creating content +as in the -threads example, and multiple service threads +as in the http-server-smp example (but with ws). + +## build + +You must first build libwebsockets itself with cmake `-DLWS_MAX_SMP=8` +or some other number greater than one. + +``` + $ cmake . && make +``` + +Pthreads is required on your system. + +## usage + +``` + $ ./lws-minimal-ws-server-threads-smp +[2019/01/28 06:59:17:4217] USER: LWS minimal ws server + threads + smp | visit http://localhost:7681 +[2019/01/28 06:59:17:4219] NOTICE: Service threads: 2 +[2019/01/28 06:59:17:4220] NOTICE: LWS_CALLBACK_EVENT_WAIT_CANCELLED in svc tid 0x7fec48af8700 +[2019/01/28 06:59:17:4220] NOTICE: LWS_CALLBACK_EVENT_WAIT_CANCELLED in svc tid 0x7fec48af8700 +... +``` + +Visit http://localhost:7681 on multiple browser windows. You may need to open +4 before the second service thread is used (check "svc tid" in the browser output). + +Two lws service threads are started. + +Two separate asynchronous threads generate strings and add them to a ringbuffer, +signalling all lws service threads to send new entries to all the browser windows. + +This demonstrates how to safely manage asynchronously generated content +and hook it up to the lws service threads. + diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/minimal-ws-server.c b/minimal-examples/ws-server/minimal-ws-server-threads-smp/minimal-ws-server.c new file mode 100644 index 000000000..6e652dfbf --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/minimal-ws-server.c @@ -0,0 +1,146 @@ +/* + * lws-minimal-ws-server + * + * Copyright (C) 2018 Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * This demonstrates a minimal ws server that can cooperate with + * other threads cleanly. Two other threads are started, which fill + * a ringbuffer with strings at 10Hz. + * + * The actual work and thread spawning etc are done in the protocol + * implementation in protocol_lws_minimal.c. + * + * To keep it simple, it serves stuff in the subdirectory "./mount-origin" of + * the directory it was started in. + * You can change that by changing mount.origin. + */ + +#include +#include +#include +#include + +#define LWS_PLUGIN_STATIC +#include "protocol_lws_minimal.c" + +#define COUNT_THREADS 2 + +static struct lws_protocols protocols[] = { + { "http", lws_callback_http_dummy, 0, 0 }, + LWS_PLUGIN_PROTOCOL_MINIMAL, + { NULL, NULL, 0, 0 } /* terminator */ +}; + +static struct lws_context *context; +static int interrupted; + +static const struct lws_http_mount mount = { + /* .mount_next */ NULL, /* linked-list "next" */ + /* .mountpoint */ "/", /* mountpoint URL */ + /* .origin */ "./mount-origin", /* serve from dir */ + /* .def */ "index.html", /* default filename */ + /* .protocol */ NULL, + /* .cgienv */ NULL, + /* .extra_mimetypes */ NULL, + /* .interpret */ NULL, + /* .cgi_timeout */ 0, + /* .cache_max_age */ 0, + /* .auth_mask */ 0, + /* .cache_reusable */ 0, + /* .cache_revalidate */ 0, + /* .cache_intermediaries */ 0, + /* .origin_protocol */ LWSMPRO_FILE, /* files in a dir */ + /* .mountpoint_len */ 1, /* char count */ + /* .basic_auth_login_file */ NULL, +}; + +/* + * This demonstrates how to pass a pointer into a specific protocol handler + * running on a specific vhost. In this case, it's our default vhost and + * we pass the pvo named "config" with the value a const char * "myconfig". + * + * This is the preferred way to pass configuration into a specific vhost + + * protocol instance. + */ + +static const struct lws_protocol_vhost_options pvo_ops = { + NULL, + NULL, + "config", /* pvo name */ + (void *)"myconfig" /* pvo value */ +}; + +static const struct lws_protocol_vhost_options pvo = { + NULL, /* "next" pvo linked-list */ + &pvo_ops, /* "child" pvo linked-list */ + "lws-minimal", /* protocol name we belong to on this vhost */ + "" /* ignored */ +}; + +void *thread_service(void *threadid) +{ + while (lws_service_tsi(context, 1000, + (int)(lws_intptr_t)threadid) >= 0 && + !interrupted) + ; + + pthread_exit(NULL); +} + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + int n, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + pthread_t pthread_service[COUNT_THREADS]; + struct lws_context_creation_info info; + const char *p; + void *retval; + + signal(SIGINT, sigint_handler); + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + lwsl_user("LWS minimal ws server + threads + smp | visit http://localhost:7681\n"); + + memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */ + info.port = 7681; + info.mounts = &mount; + info.protocols = protocols; + info.pvo = &pvo; /* per-vhost options */ + info.count_threads = COUNT_THREADS; + info.options = + LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE; + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + lwsl_notice(" Service threads: %d\n", lws_get_count_threads(context)); + + /* start all the service threads */ + + for (n = 0; n < lws_get_count_threads(context); n++) + if (pthread_create(&pthread_service[n], NULL, thread_service, + (void *)(lws_intptr_t)n)) + lwsl_err("Failed to start service thread\n"); + + /* wait for all the service threads to exit */ + + while ((--n) >= 0) + pthread_join(pthread_service[n], &retval); + + lws_context_destroy(context); + + return 0; +} diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/example.js b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/example.js new file mode 100644 index 000000000..a6ff6630f --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/example.js @@ -0,0 +1,71 @@ +var head = 0, tail = 0, ring = new Array(); + +function get_appropriate_ws_url(extra_url) +{ + var pcol; + var u = document.URL; + + /* + * We open the websocket encrypted if this page came on an + * https:// url itself, otherwise unencrypted + */ + + if (u.substring(0, 5) === "https") { + pcol = "wss://"; + u = u.substr(8); + } else { + pcol = "ws://"; + if (u.substring(0, 4) === "http") + u = u.substr(7); + } + + u = u.split("/"); + + /* + "/xxx" bit is for IE10 workaround */ + + return pcol + u[0] + "/" + extra_url; +} + +function new_ws(urlpath, protocol) +{ + if (typeof MozWebSocket != "undefined") + return new MozWebSocket(urlpath, protocol); + + return new WebSocket(urlpath, protocol); +} + +document.addEventListener("DOMContentLoaded", function() { + + ws = new_ws(get_appropriate_ws_url(""), "lws-minimal"); + try { + ws.onopen = function() { + document.getElementById("r").disabled = 0; + }; + + ws.onmessage =function got_packet(msg) { + var n, s = ""; + + ring[head] = msg.data + "\n"; + head = (head + 1) % 50; + if (tail === head) + tail = (tail + 1) % 50; + + n = tail; + do { + s = s + ring[n]; + n = (n + 1) % 50; + } while (n !== head); + + document.getElementById("r").value = s; + document.getElementById("r").scrollTop = + document.getElementById("r").scrollHeight; + }; + + ws.onclose = function(){ + document.getElementById("r").disabled = 1; + }; + } catch(exception) { + alert("

Error " + exception); + } + +}, false); diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/favicon.ico b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/favicon.ico new file mode 100644 index 000000000..c0cc2e3df Binary files /dev/null and b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/favicon.ico differ diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/index.html b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/index.html new file mode 100644 index 000000000..13145f6ad --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/index.html @@ -0,0 +1,19 @@ + + + + + + + +
+ + Minimal ws server threads SMP example.
+ Strings generated by server threads are sent to + all browsers open on this page.
+ The textarea show the last 50 lines received. +
+
+
+ + + diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/libwebsockets.org-logo.svg b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/libwebsockets.org-logo.svg new file mode 100644 index 000000000..7baea649f --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/libwebsockets.org-logo.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/strict-csp.svg b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/strict-csp.svg new file mode 100644 index 000000000..cd128f1d2 --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/mount-origin/strict-csp.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/minimal-examples/ws-server/minimal-ws-server-threads-smp/protocol_lws_minimal.c b/minimal-examples/ws-server/minimal-ws-server-threads-smp/protocol_lws_minimal.c new file mode 100644 index 000000000..2dbe34db5 --- /dev/null +++ b/minimal-examples/ws-server/minimal-ws-server-threads-smp/protocol_lws_minimal.c @@ -0,0 +1,327 @@ +/* + * ws protocol handler plugin for "lws-minimal" demonstrating multithread + * + * Copyright (C) 2010-2018 Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + */ + +#if !defined (LWS_PLUGIN_STATIC) +#define LWS_DLL +#define LWS_INTERNAL +#include +#endif + +#include + +/* one of these created for each message in the ringbuffer */ + +struct msg { + void *payload; /* is malloc'd */ + size_t len; +}; + +/* + * One of these is created for each client connecting to us. + * + * It is ONLY read or written from the lws service thread context. + */ + +struct per_session_data__minimal { + struct per_session_data__minimal *pss_list; + struct lws *wsi; + uint32_t tail; +}; + +/* one of these is created for each vhost our protocol is used with */ + +struct per_vhost_data__minimal { + struct lws_context *context; + struct lws_vhost *vhost; + const struct lws_protocols *protocol; + + struct per_session_data__minimal *pss_list; /* linked-list of live pss*/ + pthread_t pthread_spam[2]; + + pthread_mutex_t lock_ring; /* serialize access to the ring buffer */ + struct lws_ring *ring; /* {lock_ring} ringbuffer holding unsent content */ + + const char *config; + char finished; +}; + +/* + * This runs under both lws service and "spam threads" contexts. + * Access is serialized by vhd->lock_ring. + */ + +static void +__minimal_destroy_message(void *_msg) +{ + struct msg *msg = _msg; + + free(msg->payload); + msg->payload = NULL; + msg->len = 0; +} + +/* + * This runs under the "spam thread" thread context only. + * + * We spawn two threads that generate messages with this. + * + */ + +static void * +thread_spam(void *d) +{ + struct per_vhost_data__minimal *vhd = + (struct per_vhost_data__minimal *)d; + struct msg amsg; + int len = 128, index = 1, n; + + do { + /* don't generate output if nobody connected */ + if (!vhd->pss_list) + goto wait; + + pthread_mutex_lock(&vhd->lock_ring); /* --------- ring lock { */ + + /* only create if space in ringbuffer */ + n = (int)lws_ring_get_count_free_elements(vhd->ring); + if (!n) { + lwsl_user("dropping!\n"); + goto wait_unlock; + } + + amsg.payload = malloc(LWS_PRE + len); + if (!amsg.payload) { + lwsl_user("OOM: dropping\n"); + goto wait_unlock; + } + n = lws_snprintf((char *)amsg.payload + LWS_PRE, len, + "%s: spam tid: %p, msg: %d", vhd->config, + (void *)pthread_self(), index++); + amsg.len = n; + n = lws_ring_insert(vhd->ring, &amsg, 1); + if (n != 1) { + __minimal_destroy_message(&amsg); + lwsl_user("dropping!\n"); + } else + /* + * This will cause a LWS_CALLBACK_EVENT_WAIT_CANCELLED + * in the lws service thread context. + */ + lws_cancel_service(vhd->context); + +wait_unlock: + pthread_mutex_unlock(&vhd->lock_ring); /* } ring lock ------- */ + +wait: + usleep(100000); + + } while (!vhd->finished); + + lwsl_notice("thread_spam %p exiting\n", (void *)pthread_self()); + + pthread_exit(NULL); +} + +/* this runs under the lws service thread context only */ + +static int +callback_minimal(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct per_session_data__minimal *pss = + (struct per_session_data__minimal *)user; + struct per_vhost_data__minimal *vhd = + (struct per_vhost_data__minimal *) + lws_protocol_vh_priv_get(lws_get_vhost(wsi), + lws_get_protocol(wsi)); + const struct lws_protocol_vhost_options *pvo; + const struct msg *pmsg; + char temp[LWS_PRE + 256]; + void *retval; + int n, m, r = 0; + + switch (reason) { + case LWS_CALLBACK_PROTOCOL_INIT: + /* create our per-vhost struct */ + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), + sizeof(struct per_vhost_data__minimal)); + if (!vhd) + return 1; + + pthread_mutex_init(&vhd->lock_ring, NULL); + + /* recover the pointer to the globals struct */ + pvo = lws_pvo_search( + (const struct lws_protocol_vhost_options *)in, + "config"); + if (!pvo || !pvo->value) { + lwsl_err("%s: Can't find \"config\" pvo\n", __func__); + return 1; + } + vhd->config = pvo->value; + + vhd->context = lws_get_context(wsi); + vhd->protocol = lws_get_protocol(wsi); + vhd->vhost = lws_get_vhost(wsi); + + vhd->ring = lws_ring_create(sizeof(struct msg), 8, + __minimal_destroy_message); + if (!vhd->ring) { + lwsl_err("%s: failed to create ring\n", __func__); + return 1; + } + + /* start the content-creating threads */ + + for (n = 0; n < (int)LWS_ARRAY_SIZE(vhd->pthread_spam); n++) + if (pthread_create(&vhd->pthread_spam[n], NULL, + thread_spam, vhd)) { + lwsl_err("thread creation failed\n"); + r = 1; + goto init_fail; + } + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: +init_fail: + vhd->finished = 1; + for (n = 0; n < (int)LWS_ARRAY_SIZE(vhd->pthread_spam); n++) + if (vhd->pthread_spam[n]) + pthread_join(vhd->pthread_spam[n], &retval); + + if (vhd->ring) + lws_ring_destroy(vhd->ring); + + pthread_mutex_destroy(&vhd->lock_ring); + break; + + case LWS_CALLBACK_ESTABLISHED: + /* add ourselves to the list of live pss held in the vhd */ + lws_ll_fwd_insert(pss, pss_list, vhd->pss_list); + pss->tail = lws_ring_get_oldest_tail(vhd->ring); + pss->wsi = wsi; + break; + + case LWS_CALLBACK_CLOSED: + /* remove our closing pss from the list of live pss */ + lws_ll_fwd_remove(struct per_session_data__minimal, pss_list, + pss, vhd->pss_list); + break; + + case LWS_CALLBACK_SERVER_WRITEABLE: + pthread_mutex_lock(&vhd->lock_ring); /* --------- ring lock { */ + + pmsg = lws_ring_get_element(vhd->ring, &pss->tail); + if (!pmsg) { + pthread_mutex_unlock(&vhd->lock_ring); /* } ring lock ------- */ + break; + } + + n = lws_snprintf(temp + LWS_PRE, sizeof(temp) - LWS_PRE, + "svc tid:%p, %s", (void *)pthread_self(), + (char *)pmsg->payload + LWS_PRE); + + /* notice we allowed for LWS_PRE in the payload already */ + m = lws_write(wsi, (unsigned char *)temp + LWS_PRE, n, + LWS_WRITE_TEXT); + if (m < n) { + pthread_mutex_unlock(&vhd->lock_ring); /* } ring lock ------- */ + lwsl_err("ERROR %d writing to ws socket\n", m); + return -1; + } + + lws_ring_consume_and_update_oldest_tail( + vhd->ring, /* lws_ring object */ + struct per_session_data__minimal, /* type of objects with tails */ + &pss->tail, /* tail of guy doing the consuming */ + 1, /* number of payload objects being consumed */ + vhd->pss_list, /* head of list of objects with tails */ + tail, /* member name of tail in objects with tails */ + pss_list /* member name of next object in objects with tails */ + ); + + /* more to do? */ + if (lws_ring_get_element(vhd->ring, &pss->tail)) + /* come back as soon as we can write more */ + lws_callback_on_writable(pss->wsi); + + pthread_mutex_unlock(&vhd->lock_ring); /* } ring lock ------- */ + break; + + case LWS_CALLBACK_RECEIVE: + break; + + case LWS_CALLBACK_EVENT_WAIT_CANCELLED: + lwsl_notice("LWS_CALLBACK_EVENT_WAIT_CANCELLED in svc tid %p\n", + (void *)pthread_self()); + if (!vhd) + break; + /* + * When the "spam" threads add a message to the ringbuffer, + * they create this event in the lws service thread context + * using lws_cancel_service(). + * + * We respond by scheduling a writable callback for all + * connected clients. + */ + lws_start_foreach_llp(struct per_session_data__minimal **, + ppss, vhd->pss_list) { + lws_callback_on_writable((*ppss)->wsi); + } lws_end_foreach_llp(ppss, pss_list); + break; + + default: + break; + } + + return r; +} + +#define LWS_PLUGIN_PROTOCOL_MINIMAL \ + { \ + "lws-minimal", \ + callback_minimal, \ + sizeof(struct per_session_data__minimal), \ + 128, \ + 0, NULL, 0 \ + } + +#if !defined (LWS_PLUGIN_STATIC) + +/* boilerplate needed if we are built as a dynamic plugin */ + +static const struct lws_protocols protocols[] = { + LWS_PLUGIN_PROTOCOL_MINIMAL +}; + +LWS_EXTERN LWS_VISIBLE int +init_protocol_minimal(struct lws_context *context, + struct lws_plugin_capability *c) +{ + if (c->api_magic != LWS_PLUGIN_API_MAGIC) { + lwsl_err("Plugin API %d, library API %d", LWS_PLUGIN_API_MAGIC, + c->api_magic); + return 1; + } + + c->protocols = protocols; + c->count_protocols = LWS_ARRAY_SIZE(protocols); + c->extensions = NULL; + c->count_extensions = 0; + + return 0; +} + +LWS_EXTERN LWS_VISIBLE int +destroy_protocol_minimal(struct lws_context *context) +{ + return 0; +} +#endif