libmicrohttpd

HTTP/1.x server C library (MHD 1.x, stable)
Log | Files | Refs | Submodules | README | LICENSE

commit bf25ee3a3220596b83b6530b06d4106f2f0bfda1
parent fcca12c2708013aef90efc15e34805a158ce58d6
Author: Evgeny Grin (Karlson2k) <k2k@narod.ru>
Date:   Thu,  1 Apr 2021 17:52:08 +0300

Implemented new API function MHD_run_wait().

Diffstat:
MChangeLog | 5+++++
Msrc/include/microhttpd.h | 35++++++++++++++++++++++++++++++++++-
Msrc/microhttpd/daemon.c | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/testcurl/.gitignore | 2++
Msrc/testcurl/Makefile.am | 20++++++++++++++++++++
Asrc/testcurl/test_get_wait.c | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 439 insertions(+), 62 deletions(-)

diff --git a/ChangeLog b/ChangeLog @@ -1,3 +1,8 @@ +Thu 01 Apr 2021 17:46:00 MSK + Added new function MHD_run_wait() useful for single-threaded applications + without other network activity. + Added tests for the new function. -EG + Wed 17 Mar 2021 20:53:33 MSK Re-factored startup log parameters processing. Warn user if wrong logger could be used potentially. diff --git a/src/include/microhttpd.h b/src/include/microhttpd.h @@ -135,7 +135,7 @@ typedef intptr_t ssize_t; * they are parsed as decimal numbers. * Example: 0x01093001 = 1.9.30-1. */ -#define MHD_VERSION 0x00097205 +#define MHD_VERSION 0x00097206 /** * Operational results from MHD calls. @@ -2699,6 +2699,39 @@ MHD_run (struct MHD_Daemon *daemon); /** + * Run websever operation with possible blocking. + * This function do the following: waits for any network event not more than + * specified number of milliseconds, processes all incoming and outgoing + * data, processes new connections, processes any timed-out connection, and + * do other things required to run webserver. + * Once all connections are processed, function returns. + * This function is useful for quick and simple webserver implementation if + * application needs to run a single thread only and does not have any other + * network activity. + * @param daemon the daemon to run + * @param millisec the maximum time in milliseconds to wait for network and + * other events. Note: there is no guarantee that function + * blocks for specified amount of time. The real processing + * time can be shorter (if some data comes earlier) or + * longer (if data processing requires more time, especially + * in the user callbacks). + * If set to '0' then function does not block and processes + * only already available data (if any). + * If set to '-1' then function waits for events + * indefinitely (blocks until next network activity). + * @return #MHD_YES on success, #MHD_NO if this + * daemon was not started with the right + * options for this call or some serious + * unrecoverable error occurs. + * @note Available since #MHD_VERSION 0x00097206 + * @ingroup event + */ +enum MHD_Result +MHD_run_wait (struct MHD_Daemon *daemon, + int32_t millisec); + + +/** * Run webserver operations. This method should be called by clients * in combination with #MHD_get_fdset and #MHD_get_timeout() if the * client-controlled select method is used. diff --git a/src/microhttpd/daemon.c b/src/microhttpd/daemon.c @@ -101,16 +101,17 @@ close_all_connections (struct MHD_Daemon *daemon); #ifdef EPOLL_SUPPORT /** - * Do epoll()-based processing (this function is allowed to - * block if @a may_block is set to #MHD_YES). + * Do epoll()-based processing. * * @param daemon daemon to run poll loop for - * @param may_block #MHD_YES if blocking, #MHD_NO if non-blocking + * @param millisec the maximum time in milliseconds to wait for events, + * set to '0' for non-blocking processing, + * set to '-1' to wait indefinitely. * @return #MHD_NO on serious errors, #MHD_YES on success */ static enum MHD_Result MHD_epoll (struct MHD_Daemon *daemon, - int may_block); + int32_t millisec); #endif /* EPOLL_SUPPORT */ @@ -3835,6 +3836,35 @@ MHD_get_timeout (struct MHD_Daemon *daemon, /** + * Obtain timeout value for polling function for this daemon. + * @remark To be called only from the thread that processes + * daemon's select()/poll()/etc. + * + * @param daemon the daemon to query for timeout + * @param max_timeout the maximum return value (in milliseconds), + * ignored if set to '-1' + * @return timeout value in milliseconds or -1 if no timeout is expected. + */ +static int +get_timeout_millisec_ (struct MHD_Daemon *daemon, + int32_t max_timeout) +{ + MHD_UNSIGNED_LONG_LONG ulltimeout; + if (0 == max_timeout) + return 0; + + if (MHD_NO == MHD_get_timeout (daemon, &ulltimeout)) + return (INT_MAX < max_timeout) ? INT_MAX : (int) max_timeout; + + if ( (0 > max_timeout) || + ((uint32_t) max_timeout > ulltimeout) ) + return (INT_MAX < ulltimeout) ? INT_MAX : (int) ulltimeout; + + return (INT_MAX < max_timeout) ? INT_MAX : (int) max_timeout; +} + + +/** * Internal version of #MHD_run_from_select(). * * @param daemon daemon to run select loop for @@ -3978,7 +4008,7 @@ MHD_run_from_select (struct MHD_Daemon *daemon, { #ifdef EPOLL_SUPPORT enum MHD_Result ret = MHD_epoll (daemon, - MHD_NO); + 0); MHD_cleanup_connections (daemon); return ret; @@ -4003,12 +4033,14 @@ MHD_run_from_select (struct MHD_Daemon *daemon, * and then #internal_run_from_select with the result. * * @param daemon daemon to run select() loop for - * @param may_block #MHD_YES if blocking, #MHD_NO if non-blocking + * @param millisec the maximum time in milliseconds to wait for events, + * set to '0' for non-blocking processing, + * set to '-1' to wait indefinitely. * @return #MHD_NO on serious errors, #MHD_YES on success */ static enum MHD_Result MHD_select (struct MHD_Daemon *daemon, - int may_block) + int32_t millisec) { int num_ready; fd_set rs; @@ -4017,7 +4049,6 @@ MHD_select (struct MHD_Daemon *daemon, MHD_socket maxsock; struct timeval timeout; struct timeval *tv; - MHD_UNSIGNED_LONG_LONG ltimeout; int err_state; MHD_socket ls; @@ -4033,7 +4064,7 @@ MHD_select (struct MHD_Daemon *daemon, if ( (0 != (daemon->options & MHD_TEST_ALLOW_SUSPEND_RESUME)) && (MHD_NO != resume_suspended_connections (daemon)) && (0 == (daemon->options & MHD_USE_THREAD_PER_CONNECTION)) ) - may_block = MHD_NO; + millisec = 0; if (0 == (daemon->options & MHD_USE_THREAD_PER_CONNECTION)) { @@ -4118,25 +4149,47 @@ MHD_select (struct MHD_Daemon *daemon, FD_CLR (ls, &rs); } - tv = NULL; + if (MHD_NO != err_state) - may_block = MHD_NO; - if (MHD_NO == may_block) + millisec = 0; + tv = NULL; + if (0 == millisec) { timeout.tv_usec = 0; timeout.tv_sec = 0; tv = &timeout; } - else if ( (0 == (daemon->options & MHD_USE_THREAD_PER_CONNECTION)) && - (MHD_NO != MHD_get_timeout (daemon, &ltimeout)) ) + else { - /* ltimeout is in ms */ - timeout.tv_usec = (ltimeout % 1000) * 1000; - if (ltimeout / 1000 > TIMEVAL_TV_SEC_MAX) - timeout.tv_sec = TIMEVAL_TV_SEC_MAX; - else - timeout.tv_sec = (_MHD_TIMEVAL_TV_SEC_TYPE) (ltimeout / 1000); - tv = &timeout; + MHD_UNSIGNED_LONG_LONG ltimeout; + + if ( (0 == (daemon->options & MHD_USE_THREAD_PER_CONNECTION)) && + (MHD_NO != MHD_get_timeout (daemon, &ltimeout)) ) + { + tv = &timeout; /* have timeout value */ + if ( (0 < millisec) && + (ltimeout > (MHD_UNSIGNED_LONG_LONG) millisec) ) + ltimeout = (MHD_UNSIGNED_LONG_LONG) millisec; + } + else if (0 < millisec) + { + tv = &timeout; /* have timeout value */ + ltimeout = (MHD_UNSIGNED_LONG_LONG) millisec; + } + + if (NULL != tv) + { /* have timeout value */ + if (ltimeout / 1000 > TIMEVAL_TV_SEC_MAX) + { + timeout.tv_sec = TIMEVAL_TV_SEC_MAX; + timeout.tv_usec = 0; + } + else + { + timeout.tv_sec = (_MHD_TIMEVAL_TV_SEC_TYPE) (ltimeout / 1000); + timeout.tv_usec = (ltimeout % 1000) * 1000; + } + } } num_ready = MHD_SYS_select_ (maxsock + 1, &rs, @@ -4172,12 +4225,14 @@ MHD_select (struct MHD_Daemon *daemon, * socket using poll(). * * @param daemon daemon to run poll loop for - * @param may_block #MHD_YES if blocking, #MHD_NO if non-blocking + * @param millisec the maximum time in milliseconds to wait for events, + * set to '0' for non-blocking processing, + * set to '-1' to wait indefinitely. * @return #MHD_NO on serious errors, #MHD_YES on success */ static enum MHD_Result MHD_poll_all (struct MHD_Daemon *daemon, - int may_block) + int32_t millisec) { unsigned int num_connections; struct MHD_Connection *pos; @@ -4189,7 +4244,7 @@ MHD_poll_all (struct MHD_Daemon *daemon, if ( (0 != (daemon->options & MHD_TEST_ALLOW_SUSPEND_RESUME)) && (MHD_NO != resume_suspended_connections (daemon)) ) - may_block = MHD_NO; + millisec = 0; /* count number of connections and thus determine poll set size */ num_connections = 0; @@ -4200,7 +4255,6 @@ MHD_poll_all (struct MHD_Daemon *daemon, num_connections += 2; #endif /* HTTPS_SUPPORT && UPGRADE_SUPPORT */ { - MHD_UNSIGNED_LONG_LONG ltimeout; unsigned int i; int timeout; unsigned int poll_server; @@ -4243,14 +4297,8 @@ MHD_poll_all (struct MHD_Daemon *daemon, poll_itc_idx = (int) poll_server; poll_server++; } - if (may_block == MHD_NO) - timeout = 0; - else if ( (0 != (daemon->options & MHD_USE_THREAD_PER_CONNECTION)) || - (MHD_NO == MHD_get_timeout (daemon, - &ltimeout)) ) - timeout = -1; - else - timeout = (ltimeout > INT_MAX) ? INT_MAX : (int) ltimeout; + + timeout = get_timeout_millisec_ (daemon, millisec); i = 0; for (pos = daemon->connections_tail; NULL != pos; pos = pos->prev) @@ -4494,7 +4542,7 @@ MHD_poll (struct MHD_Daemon *daemon, return MHD_NO; if (0 == (daemon->options & MHD_USE_THREAD_PER_CONNECTION)) return MHD_poll_all (daemon, - may_block); + may_block ? -1 : 0); return MHD_poll_listen_socket (daemon, may_block); #else @@ -4685,16 +4733,17 @@ static const char *const epoll_itc_marker = "itc_marker"; /** - * Do epoll()-based processing (this function is allowed to - * block if @a may_block is set to #MHD_YES). + * Do epoll()-based processing. * * @param daemon daemon to run poll loop for - * @param may_block #MHD_YES if blocking, #MHD_NO if non-blocking + * @param millisec the maximum time in milliseconds to wait for events, + * set to '0' for non-blocking processing, + * set to '-1' to wait indefinitely. * @return #MHD_NO on serious errors, #MHD_YES on success */ static enum MHD_Result MHD_epoll (struct MHD_Daemon *daemon, - int may_block) + int32_t millisec) { #if defined(HTTPS_SUPPORT) && defined(UPGRADE_SUPPORT) static const char *const upgrade_marker = "upgrade_ptr"; @@ -4704,7 +4753,6 @@ MHD_epoll (struct MHD_Daemon *daemon, struct epoll_event events[MAX_EVENTS]; struct epoll_event event; int timeout_ms; - MHD_UNSIGNED_LONG_LONG timeout_ll; int num_events; unsigned int i; MHD_socket ls; @@ -4790,23 +4838,9 @@ MHD_epoll (struct MHD_Daemon *daemon, if ( (0 != (daemon->options & MHD_TEST_ALLOW_SUSPEND_RESUME)) && (MHD_NO != resume_suspended_connections (daemon)) ) - may_block = MHD_NO; + millisec = 0; - if (MHD_NO != may_block) - { - if (MHD_NO != MHD_get_timeout (daemon, - &timeout_ll)) - { - if (timeout_ll >= (MHD_UNSIGNED_LONG_LONG) INT_MAX) - timeout_ms = INT_MAX; - else - timeout_ms = (int) timeout_ll; - } - else - timeout_ms = -1; - } - else - timeout_ms = 0; + timeout_ms = get_timeout_millisec_ (daemon, millisec); /* Reset. New value will be set when connections are processed. */ /* Note: Used mostly for uniformity here as same situation is @@ -5026,24 +5060,69 @@ MHD_run (struct MHD_Daemon *daemon) if ( (daemon->shutdown) || (0 != (daemon->options & MHD_USE_INTERNAL_POLLING_THREAD)) ) return MHD_NO; + + (void) MHD_run_wait (daemon, 0); + return MHD_YES; +} + + +/** + * Run websever operation with possible blocking. + * This function do the following: waits for any network event not more than + * specified number of milliseconds, processes all incoming and outgoing + * data, processes new connections, processes any timed-out connection, and + * do other things required to run webserver. + * Once all connections are processed, function returns. + * This function is useful for quick and simple webserver implementation if + * application needs to run a single thread only and does not have any other + * network activity. + * @param daemon the daemon to run + * @param millisec the maximum time in milliseconds to wait for network and + * other events. Note: there is no guarantee that function + * blocks for specified amount of time. The real processing + * time can be shorter (if some data comes earlier) or + * longer (if data processing requires more time, especially + * in the user callbacks). + * If set to '0' then function does not block and processes + * only already available data (if any). + * If set to '-1' then function waits for events + * indefinitely (blocks until next network activity). + * @return #MHD_YES on success, #MHD_NO if this + * daemon was not started with the right + * options for this call or some serious + * unrecoverable error occurs. + * @note Available since #MHD_VERSION 0x00097206 + * @ingroup event + */ +enum MHD_Result +MHD_run_wait (struct MHD_Daemon *daemon, + int32_t millisec) +{ + enum MHD_Result res; + if ( (daemon->shutdown) || + (0 != (daemon->options & MHD_USE_INTERNAL_POLLING_THREAD)) ) + return MHD_NO; + + if (0 > millisec) + millisec = -1; if (0 != (daemon->options & MHD_USE_POLL)) { - MHD_poll (daemon, MHD_NO); + res = MHD_poll_all (daemon, millisec); MHD_cleanup_connections (daemon); } #ifdef EPOLL_SUPPORT else if (0 != (daemon->options & MHD_USE_EPOLL)) { - MHD_epoll (daemon, MHD_NO); + res = MHD_epoll (daemon, millisec); MHD_cleanup_connections (daemon); } #endif else { - MHD_select (daemon, MHD_NO); + res = MHD_select (daemon, millisec); /* MHD_select does MHD_cleanup_connections already */ } - return MHD_YES; + return res; } @@ -5139,10 +5218,10 @@ MHD_polling_thread (void *cls) MHD_poll (daemon, MHD_YES); #ifdef EPOLL_SUPPORT else if (0 != (daemon->options & MHD_USE_EPOLL)) - MHD_epoll (daemon, MHD_YES); + MHD_epoll (daemon, -1); #endif else - MHD_select (daemon, MHD_YES); + MHD_select (daemon, -1); MHD_cleanup_connections (daemon); } diff --git a/src/testcurl/.gitignore b/src/testcurl/.gitignore @@ -118,3 +118,5 @@ test_patch11 core /test_get_iovec /test_get_iovec11 +/test_get_wait +/test_get_wait11 diff --git a/src/testcurl/Makefile.am b/src/testcurl/Makefile.am @@ -46,6 +46,8 @@ THREAD_ONLY_TESTS += \ endif THREAD_ONLY_TESTS += \ + test_get_wait \ + test_get_wait11 \ test_quiesce \ $(EMPTY_ITEM) @@ -232,6 +234,24 @@ test_get_sendfile_LDADD = \ $(top_builddir)/src/microhttpd/libmicrohttpd.la \ @LIBCURL@ +test_get_wait_SOURCES = \ + test_get_wait.c \ + mhd_has_in_name.h +test_get_wait_CFLAGS = \ + $(PTHREAD_CFLAGS) $(AM_CFLAGS) +test_get_wait_LDADD = \ + $(top_builddir)/src/microhttpd/libmicrohttpd.la \ + $(PTHREAD_LIBS) @LIBCURL@ + +test_get_wait11_SOURCES = \ + test_get_wait.c \ + mhd_has_in_name.h +test_get_wait11_CFLAGS = \ + $(PTHREAD_CFLAGS) $(AM_CFLAGS) +test_get_wait11_LDADD = \ + $(top_builddir)/src/microhttpd/libmicrohttpd.la \ + $(PTHREAD_LIBS) @LIBCURL@ + test_urlparse_SOURCES = \ test_urlparse.c mhd_has_in_name.h test_urlparse_LDADD = \ diff --git a/src/testcurl/test_get_wait.c b/src/testcurl/test_get_wait.c @@ -0,0 +1,238 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2007, 2009, 2011 Christian Grothoff + Copyright (C) 2016-2021 Karlson2k (Evgeny Grin) + + libmicrohttpd is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 2, or (at your + option) any later version. + + libmicrohttpd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libmicrohttpd; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +/** + * @file test_get_wait.c + * @brief Test 'MHD_run_wait()' function. + * @author Christian Grothoff + * @author Karlson2k (Evgeny Grin) + */ + +#include "MHD_config.h" +#include "platform.h" +#include <curl/curl.h> +#include <microhttpd.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <pthread.h> +#include "mhd_has_in_name.h" + +#if defined(MHD_CPU_COUNT) && (MHD_CPU_COUNT + 0) < 2 +#undef MHD_CPU_COUNT +#endif +#if ! defined(MHD_CPU_COUNT) +#define MHD_CPU_COUNT 2 +#endif + +/** + * How many rounds of operations do we do for each + * test. + * Check all three types of requests for HTTP/1.1: + * * first request, new connection; + * * "middle" request, existing connection with stay-alive; + * * final request, no data processed after. + */ +#define ROUNDS 3 + +/** + * Do we use HTTP 1.1? + */ +static int oneone; + +/** + * Response to return (re-used). + */ +static struct MHD_Response *response; + +/** + * Set to 1 if the worker threads are done. + */ +static volatile int signal_done; + + +static size_t +copyBuffer (void *ptr, + size_t size, size_t nmemb, + void *ctx) +{ + (void) ptr; (void) ctx; /* Unused. Silent compiler warning. */ + return size * nmemb; +} + + +static enum MHD_Result +ahc_echo (void *cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, size_t *upload_data_size, + void **unused) +{ + static int ptr; + const char *me = cls; + enum MHD_Result ret; + (void) url; (void) version; /* Unused. Silent compiler warning. */ + (void) upload_data; (void) upload_data_size; /* Unused. Silent compiler warning. */ + + if (0 != strcmp (me, method)) + return MHD_NO; /* unexpected method */ + if (&ptr != *unused) + { + *unused = &ptr; + return MHD_YES; + } + *unused = NULL; + ret = MHD_queue_response (connection, MHD_HTTP_OK, response); + if (ret == MHD_NO) + abort (); + return ret; +} + + +static void * +thread_gets (void *param) +{ + CURL *c; + CURLcode errornum; + unsigned int i; + char url[64]; + int port = (int) (intptr_t) param; + + snprintf (url, + sizeof (url), + "http://127.0.0.1:%d/hello_world", + port); + + c = curl_easy_init (); + if (NULL == c) + _exit (99); + curl_easy_setopt (c, CURLOPT_URL, url); + curl_easy_setopt (c, CURLOPT_WRITEFUNCTION, &copyBuffer); + curl_easy_setopt (c, CURLOPT_WRITEDATA, NULL); + curl_easy_setopt (c, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt (c, CURLOPT_TIMEOUT, 15L); + if (oneone) + curl_easy_setopt (c, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + else + curl_easy_setopt (c, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + curl_easy_setopt (c, CURLOPT_CONNECTTIMEOUT, 15L); + curl_easy_setopt (c, CURLOPT_NOSIGNAL, 1L); + for (i = 0; i < ROUNDS; i++) + { + if (CURLE_OK != (errornum = curl_easy_perform (c))) + { + signal_done = 1; + fprintf (stderr, + "curl_easy_perform failed: `%s'\n", + curl_easy_strerror (errornum)); + curl_easy_cleanup (c); + abort (); + } + } + curl_easy_cleanup (c); + signal_done = 1; + + return NULL; +} + + +static int +testRunWaitGet (int port, int poll_flag) +{ + pthread_t get_tid; + struct MHD_Daemon *d; + const char *const test_desc = ((poll_flag & MHD_USE_AUTO) ? + "MHD_USE_AUTO" : + (poll_flag & MHD_USE_POLL) ? + "MHD_USE_POLL" : + (poll_flag & MHD_USE_EPOLL) ? + "MHD_USE_EPOLL" : + "select()"); + + if (MHD_NO != MHD_is_feature_supported (MHD_FEATURE_AUTODETECT_BIND_PORT)) + port = 0; + + printf ("Starting MHD_run_wait() test with MHD in %s polling mode.\n", + test_desc); + signal_done = 0; + d = MHD_start_daemon (MHD_USE_ERROR_LOG | poll_flag, + port, NULL, NULL, &ahc_echo, "GET", MHD_OPTION_END); + if (d == NULL) + abort (); + if (0 == port) + { + const union MHD_DaemonInfo *dinfo; + dinfo = MHD_get_daemon_info (d, MHD_DAEMON_INFO_BIND_PORT); + if ((NULL == dinfo) || (0 == dinfo->port) ) + abort (); + port = (int) dinfo->port; + } + + if (0 != pthread_create (&get_tid, NULL, + &thread_gets, (void*) (intptr_t) port)) + _exit (99); + + /* As another thread sets "done" flag after ending of network + * activity, it's required to set positive timeout value for MHD_run_wait(). + * Alternatively, to use timeout value "-1" here, another thread should start + * additional connection to wake MHD after setting "done" flag. */ + do + { + if (MHD_NO == MHD_run_wait (d, 50)) + abort (); + } while (0 == signal_done); + + if (0 != pthread_join (get_tid, NULL)) + _exit (99); + + MHD_stop_daemon (d); + printf ("Test succeeded.\n"); + return 0; +} + + +int +main (int argc, char *const *argv) +{ + int port = 1675; + (void) argc; /* Unused. Silent compiler warning. */ + + if ((NULL == argv) || (0 == argv[0])) + return 99; + oneone = has_in_name (argv[0], "11"); + if (oneone) + port += 5; + if (0 != curl_global_init (CURL_GLOBAL_WIN32)) + return 2; + response = MHD_create_response_from_buffer (strlen ("/hello_world"), + "/hello_world", + MHD_RESPMEM_MUST_COPY); + testRunWaitGet (port++, 0); + if (MHD_YES == MHD_is_feature_supported (MHD_FEATURE_EPOLL)) + testRunWaitGet (port++, MHD_USE_EPOLL); + testRunWaitGet (port++, MHD_USE_AUTO); + + MHD_destroy_response (response); + curl_global_cleanup (); + return 0; /* Errors produce abort() or _exit() */ +}