libmicrohttpd

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

commit fd16f9d2cbd085e84bc903326d9434aff74ea890
parent d59f3045248ff848f3e3bb9698f1c4094fc9c8b8
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 28 Jul 2026 23:13:46 +0200

add test for suspend_resume_epoll example

Diffstat:
Msrc/examples/.gitignore | 5+++++
Msrc/examples/Makefile.am | 13++++++++++++-
Asrc/examples/test_suspend_resume_epoll.c | 698+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 715 insertions(+), 1 deletion(-)

diff --git a/src/examples/.gitignore b/src/examples/.gitignore @@ -40,3 +40,8 @@ minimal_example_empty minimal_example_empty_tls /digest_auth_example_adv json_echo +/suspend_resume_epoll +/test_suspend_resume_epoll +# Produced by the automake test harness +*.log +*.trs diff --git a/src/examples/Makefile.am b/src/examples/Makefile.am @@ -3,7 +3,7 @@ SUBDIRS = . AM_CPPFLAGS = \ -I$(top_srcdir)/src/include \ - $(CPPFLAGS_ac) \ + $(CPPFLAGS_ac) $(LIBCURL_CPPFLAGS) \ -DDATA_DIR=\"$(top_srcdir)/src/datadir/\" AM_CFLAGS = $(CFLAGS_ac) @LIBGCRYPT_CFLAGS@ @@ -42,6 +42,12 @@ noinst_PROGRAMS = \ if MHD_HAVE_EPOLL noinst_PROGRAMS += \ suspend_resume_epoll +if HAVE_FORK_WAITPID +if RUN_LIBCURL_TESTS +check_PROGRAMS = test_suspend_resume_epoll +TESTS = $(check_PROGRAMS) +endif +endif endif if HAVE_JANSSON @@ -175,6 +181,11 @@ suspend_resume_epoll_CPPFLAGS = \ suspend_resume_epoll_LDADD = \ $(top_builddir)/src/microhttpd/libmicrohttpd.la +test_suspend_resume_epoll_SOURCES = \ + test_suspend_resume_epoll.c +test_suspend_resume_epoll_LDADD = \ + @LIBCURL@ + benchmark_https_SOURCES = \ benchmark_https.c benchmark_https_CPPFLAGS = \ diff --git a/src/examples/test_suspend_resume_epoll.c b/src/examples/test_suspend_resume_epoll.c @@ -0,0 +1,698 @@ +/* + This file is part of libmicrohttpd + Copyright (C) 2026 Christian Grothoff (and other contributing authors) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ +/** + * @file test_suspend_resume_epoll.c + * @brief Test for the suspend_resume_epoll example. The example itself + * is started as a child process and then driven over HTTP, so + * what is tested is exactly the code that ships as the example. + * + * The example parks every request for one second on a timerfd + * that lives in the application's own epoll set, next to the + * daemon's epoll FD. Checking that the right bytes come back is + * the easy part; the checks that matter are that the process + * burns no CPU while a connection is suspended, and that two + * requests are parked at the same time rather than one after the + * other. Both would still deliver identical bytes if suspending + * were broken. + * @author Christian Grothoff + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <curl/curl.h> +#include <errno.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +/** + * How long the example parks a request, in milliseconds. This is the + * one-second timer hard-coded in suspend_resume_epoll.c. + */ +#define PARK_MS 1000 + +/** + * Upper bound for any single request, in seconds. + */ +#define REQUEST_TIMEOUT 30 + +/** + * How long to wait for the freshly started example to answer, in + * milliseconds. + */ +#define STARTUP_TIMEOUT_MS 5000 + +/** + * How often to retry with a different port if the one we picked turned + * out to be taken after all. + */ +#define PORT_ATTEMPTS 5 + +/** + * Largest response body we are prepared to keep. + */ +#define MAX_BODY 4096 + + +/** + * What one HTTP request collected. + */ +struct Fetch +{ + /** + * The body, as far as it was received. + */ + char body[MAX_BODY]; + + /** + * Number of valid bytes in @e body. + */ + size_t len; + + /** + * Result of the transfer. + */ + CURLcode res; +}; + + +/** + * The example's process ID, or -1 once it has been reaped. + */ +static pid_t server_pid = -1; + +/** + * The port the example was told to bind. + */ +static unsigned int server_port; + +/** + * Number of checks that failed. + */ +static unsigned int failures; + + +/** + * Report the outcome of one check. + * + * @param ok non-zero if the check passed + * @param what what was being checked + */ +static void +check (int ok, + const char *what) +{ + if (! ok) + failures++; + fprintf (stderr, + "%s: %s\n", + ok ? "PASS" : "FAIL", + what); +} + + +/** + * @return the current value of the monotonic clock, in milliseconds + */ +static long +now_ms (void) +{ + struct timespec ts; + + if (0 != clock_gettime (CLOCK_MONOTONIC, + &ts)) + return 0; + return (long) ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + + +/** + * Read the CPU time consumed by process @a pid so far. Only + * implemented for Linux; everywhere else the test simply skips the + * check that uses it. + * + * @param pid the process to look at + * @param[out] ms where to store the sum of user and system time in + * milliseconds + * @return non-zero on success + */ +static int +cpu_time_ms (pid_t pid, + long *ms) +{ +#ifdef __linux__ + char path[64]; + char buf[1024]; + char *p; + FILE *f; + size_t got; + unsigned long utime; + unsigned long stime; + long ticks; + + snprintf (path, + sizeof (path), + "/proc/%ld/stat", + (long) pid); + f = fopen (path, + "r"); + if (NULL == f) + return 0; + got = fread (buf, + 1, + sizeof (buf) - 1, + f); + fclose (f); + if (0 == got) + return 0; + buf[got] = '\0'; + /* The second field is the executable name and may contain spaces and + parentheses, so start parsing behind its closing one. */ + p = strrchr (buf, + ')'); + if (NULL == p) + return 0; + if (2 != sscanf (p + 1, + " %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu", + &utime, + &stime)) + return 0; + ticks = sysconf (_SC_CLK_TCK); + if (0 >= ticks) + return 0; + *ms = (long) ((utime + stime) * 1000 / (unsigned long) ticks); + return 1; +#else + (void) pid; + (void) ms; + return 0; +#endif +} + + +static size_t +write_cb (void *ptr, + size_t size, + size_t nmemb, + void *cls) +{ + struct Fetch *f = cls; + size_t total = size * nmemb; + + if (total > sizeof (f->body) - f->len - 1) + total = sizeof (f->body) - f->len - 1; + memcpy (&f->body[f->len], + ptr, + total); + f->len += total; + f->body[f->len] = '\0'; + return size * nmemb; +} + + +/** + * Create an easy handle that will GET @a path from the example. + * + * @param path the path to request + * @param[out] f where to store what is received; cleared here + * @return the handle, or NULL on error + */ +static CURL * +make_handle (const char *path, + struct Fetch *f) +{ + CURL *curl; + char url[128]; + + memset (f, + 0, + sizeof (struct Fetch)); + f->res = CURLE_FAILED_INIT; + snprintf (url, + sizeof (url), + "http://127.0.0.1:%u%s", + server_port, + path); + curl = curl_easy_init (); + if (NULL == curl) + return NULL; + curl_easy_setopt (curl, CURLOPT_URL, url); + curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, &write_cb); + curl_easy_setopt (curl, CURLOPT_WRITEDATA, f); + curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long) REQUEST_TIMEOUT); + curl_easy_setopt (curl, CURLOPT_NOSIGNAL, 1L); + return curl; +} + + +/** + * Perform one GET against the example. + * + * @param path the path to request + * @param[out] f where to store what was received + * @return milliseconds the request took + */ +static long +fetch (const char *path, + struct Fetch *f) +{ + CURL *curl; + long start; + + curl = make_handle (path, + f); + if (NULL == curl) + return 0; + start = now_ms (); + f->res = curl_easy_perform (curl); + curl_easy_cleanup (curl); + return now_ms () - start; +} + + +/** + * Perform two GETs against the example at the same time. + * + * @param path1 the path for the first request + * @param path2 the path for the second request + * @param[out] f1 where to store what the first request received + * @param[out] f2 where to store what the second request received + * @return milliseconds until both were done + */ +static long +fetch_both (const char *path1, + const char *path2, + struct Fetch *f1, + struct Fetch *f2) +{ + CURLM *multi; + CURL *c1; + CURL *c2; + CURLMsg *msg; + int running; + int left; + long start; + + c1 = make_handle (path1, + f1); + c2 = make_handle (path2, + f2); + multi = curl_multi_init (); + if ( (NULL == c1) || + (NULL == c2) || + (NULL == multi) ) + { + if (NULL != c1) + curl_easy_cleanup (c1); + if (NULL != c2) + curl_easy_cleanup (c2); + if (NULL != multi) + curl_multi_cleanup (multi); + return 0; + } + curl_multi_add_handle (multi, c1); + curl_multi_add_handle (multi, c2); + start = now_ms (); + running = 1; + while (0 != running) + { + if (CURLM_OK != curl_multi_perform (multi, + &running)) + break; + if (0 == running) + break; + if (CURLM_OK != curl_multi_wait (multi, + NULL, + 0, + 100, + NULL)) + break; + if (now_ms () - start > REQUEST_TIMEOUT * 1000) + break; + } + while (NULL != (msg = curl_multi_info_read (multi, + &left))) + { + if (CURLMSG_DONE != msg->msg) + continue; + if (msg->easy_handle == c1) + f1->res = msg->data.result; + else if (msg->easy_handle == c2) + f2->res = msg->data.result; + } + curl_multi_remove_handle (multi, c1); + curl_multi_remove_handle (multi, c2); + curl_easy_cleanup (c1); + curl_easy_cleanup (c2); + curl_multi_cleanup (multi); + return now_ms () - start; +} + + +/** + * Ask the operating system for a port that is currently free. The + * example insists on being given a port number, so the test has to pick + * one; if the guess goes stale between here and the child's bind() the + * child exits at once and the caller simply tries again. + * + * @param[out] port where to store the port number + * @return non-zero on success + */ +static int +pick_free_port (unsigned int *port) +{ + struct sockaddr_in addr; + socklen_t addrlen = sizeof (addr); + int sock; + + sock = socket (AF_INET, + SOCK_STREAM, + 0); + if (-1 == sock) + return 0; + memset (&addr, + 0, + sizeof (addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl (INADDR_ANY); + addr.sin_port = 0; + if ( (0 != bind (sock, + (struct sockaddr *) &addr, + sizeof (addr))) || + (0 != getsockname (sock, + (struct sockaddr *) &addr, + &addrlen)) ) + { + close (sock); + return 0; + } + close (sock); + *port = ntohs (addr.sin_port); + return (0 != *port); +} + + +/** + * Start the example and wait until it answers. + * + * @return 1 on success, 0 if it could not be started, -1 if the binary + * could not be executed at all (in which case the test skips) + */ +static int +start_server (void) +{ + unsigned int attempt; + + for (attempt = 0; attempt < PORT_ATTEMPTS; attempt++) + { + char port_arg[16]; + long deadline; + + if (! pick_free_port (&server_port)) + return 0; + snprintf (port_arg, + sizeof (port_arg), + "%u", + server_port); + server_pid = fork (); + if (-1 == server_pid) + return 0; + if (0 == server_pid) + { + execl ("./suspend_resume_epoll", + "suspend_resume_epoll", + port_arg, + (char *) NULL); + fprintf (stderr, + "Failed to exec ./suspend_resume_epoll: %s\n", + strerror (errno)); + _exit (77); + } + deadline = now_ms () + STARTUP_TIMEOUT_MS; + while (now_ms () < deadline) + { + struct Fetch f; + int status; + + if (server_pid == waitpid (server_pid, + &status, + WNOHANG)) + { + /* The example gave up, most likely because the port was taken + after all. Unless it could not even be started, try again. */ + if (WIFEXITED (status) && (77 == WEXITSTATUS (status))) + { + server_pid = -1; + return -1; + } + server_pid = -1; + break; + } + (void) fetch ("/startup", + &f); + if (CURLE_OK == f.res) + return 1; + usleep (100 * 1000); + } + if (-1 != server_pid) + { + kill (server_pid, + SIGKILL); + waitpid (server_pid, + NULL, + 0); + server_pid = -1; + return 0; /* it is running but not answering; retrying will not help */ + } + } + return 0; +} + + +/** + * The example echoes the requested URL back, but only after the timer + * it armed has fired. + */ +static void +test_echo (void) +{ + struct Fetch f; + long ms; + + ms = fetch ("/hello/world", + &f); + check (CURLE_OK == f.res, + "request completed"); + check (0 == strcmp (f.body, + "/hello/world"), + "example echoed the requested URL"); + /* Only a lower bound is checked: a loaded machine may take longer, + but nothing can make the one-second timer fire early. */ + fprintf (stderr, + "request was parked for %ld ms\n", + ms); + check (ms >= PARK_MS / 2, + "answer waited for the timer to fire"); +} + + +/** + * While the connection is parked the process must be asleep in + * epoll_wait(). This is the check that tells a suspended connection + * apart from an access handler that is polled in a loop --- both return + * the same bytes after the same delay. + */ +static void +test_idle_while_suspended (void) +{ + struct Fetch f; + long cpu_before; + long cpu_after; + long ms; + + if (! cpu_time_ms (server_pid, + &cpu_before)) + { + fprintf (stderr, + "SKIP: no way to read the server's CPU time on this " + "platform\n"); + return; + } + ms = fetch ("/idle", + &f); + check (CURLE_OK == f.res, + "request completed"); + if (! cpu_time_ms (server_pid, + &cpu_after)) + { + fprintf (stderr, + "SKIP: could not read the server's CPU time\n"); + return; + } + fprintf (stderr, + "server used %ld ms of CPU during %ld ms of waiting\n", + cpu_after - cpu_before, + ms); + /* "<=" rather than "<" so that a degenerate run in which nothing was + waited for at all is left for test_echo() to report, instead of + being blamed on CPU usage here. */ + check ((cpu_after - cpu_before) * 4 <= ms, + "server stayed idle while the connection was suspended"); +} + + +/** + * Suspending must not serialise requests: two connections parked at the + * same time have to come back after one timer period, not two. With a + * single-threaded daemon this only works because the connection is + * taken out of the event loop rather than blocking it. + */ +static void +test_two_at_once (void) +{ + struct Fetch f1; + struct Fetch f2; + long ms; + + ms = fetch_both ("/first", + "/second", + &f1, + &f2); + check ( (CURLE_OK == f1.res) && + (CURLE_OK == f2.res), + "both concurrent requests completed"); + check ( (0 == strcmp (f1.body, + "/first")) && + (0 == strcmp (f2.body, + "/second")), + "each concurrent request got its own answer"); + fprintf (stderr, + "two concurrent requests took %ld ms\n", + ms); + check (ms < 2 * PARK_MS - PARK_MS / 4, + "the two requests were parked at the same time"); +} + + +/** + * A client that hangs up while its connection is parked must not take + * the server down, and must not leak the timer either. + */ +static void +test_client_hangs_up (void) +{ + struct sockaddr_in addr; + struct Fetch f; + static const char *req = "GET /gone HTTP/1.1\r\nHost: localhost\r\n\r\n"; + int sock; + + sock = socket (AF_INET, + SOCK_STREAM, + 0); + if (-1 == sock) + { + check (0, + "could not create a socket"); + return; + } + memset (&addr, + 0, + sizeof (addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK); + addr.sin_port = htons ((uint16_t) server_port); + if (0 != connect (sock, + (struct sockaddr *) &addr, + sizeof (addr))) + { + close (sock); + check (0, + "could not connect to the example"); + return; + } + if (-1 == send (sock, + req, + strlen (req), + 0)) + { + close (sock); + check (0, + "could not send a request"); + return; + } + /* Hang up well before the timer fires, so that the connection is + still suspended when the FIN arrives. */ + usleep ((useconds_t) (PARK_MS / 4) * 1000); + close (sock); + + (void) fetch ("/still/here", + &f); + check ( (CURLE_OK == f.res) && + (0 == strcmp (f.body, + "/still/here")), + "server survived a client that hung up while suspended"); +} + + +int +main (void) +{ + int started; + + if (0 != curl_global_init (CURL_GLOBAL_ALL)) + return 77; + started = start_server (); + if (1 != started) + { + fprintf (stderr, + "Could not start ./suspend_resume_epoll\n"); + curl_global_cleanup (); + return (-1 == started) ? 77 : 1; + } + fprintf (stderr, + "example listening on port %u\n", + server_port); + + test_echo (); + test_idle_while_suspended (); + test_two_at_once (); + test_client_hangs_up (); + + /* The example runs an endless loop by design, so there is nothing to + ask it to do but die. */ + kill (server_pid, + SIGKILL); + waitpid (server_pid, + NULL, + 0); + server_pid = -1; + curl_global_cleanup (); + if (0 != failures) + fprintf (stderr, + "%u check(s) failed\n", + failures); + return (0 == failures) ? 0 : 1; +}