aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Bühler <stbuehler@web.de>2008-09-20 11:43:01 +0200
committerStefan Bühler <stbuehler@web.de>2008-09-20 11:43:01 +0200
commitc81c68f300a359241f3288543d7aa48bbc38da2d (patch)
treef9e2e58f14f1c383e6590036af1e7325315c0bc6
downloadfcgi-debug-c81c68f300a359241f3288543d7aa48bbc38da2d.tar.gz
fcgi-debug-c81c68f300a359241f3288543d7aa48bbc38da2d.zip
Initial commit
-rw-r--r--.gitignore4
-rw-r--r--CMakeLists.txt20
-rw-r--r--cmake/LighttpdMacros.cmake14
-rw-r--r--src/CMakeLists.txt65
-rw-r--r--src/config.h.cmake0
-rw-r--r--src/connection.c174
-rw-r--r--src/fcgi-debug.c187
-rw-r--r--src/fcgi-debug.h70
-rw-r--r--src/tools.c51
9 files changed, 585 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b287b2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*build
+Doxyfile
+*kdev*
+*~
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..9bd4eae
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,20 @@
+PROJECT(fcgi-debug C)
+
+CMAKE_MINIMUM_REQUIRED(VERSION 2.4.0 FATAL_ERROR)
+
+SET(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
+
+SET(CPACK_PACKAGE_VERSION_MAJOR 2)
+SET(CPACK_PACKAGE_VERSION_MINOR 0)
+SET(CPACK_PACKAGE_VERSION_PATCH 0)
+
+#SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/COPYING")
+#SET(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README")
+SET(CPACK_PACKAGE_VENDOR "lighttpd@stbuehler.de")
+
+SET(CPACK_SOURCE_GENERATOR "TGZ")
+SET(CPACK_SOURCE_IGNORE_FILES "/\\\\.;~$;/_;build/;CMakeFiles/;CMakeCache;gz$;Makefile\\\\.;trace;Testing/;foo;autom4te;cmake_install;CPack;\\\\.pem;ltmain.sh;configure;libtool;/config\\\\.;missing;autogen.sh;install-sh;Dart;aclocal;log$;Makefile$")
+
+SET(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
+
+ADD_SUBDIRECTORY(src build)
diff --git a/cmake/LighttpdMacros.cmake b/cmake/LighttpdMacros.cmake
new file mode 100644
index 0000000..68b5f20
--- /dev/null
+++ b/cmake/LighttpdMacros.cmake
@@ -0,0 +1,14 @@
+
+MACRO(ADD_TARGET_PROPERTIES _target _name)
+ SET(_properties)
+ FOREACH(_prop ${ARGN})
+ SET(_properties "${_properties} ${_prop}")
+ ENDFOREACH(_prop)
+ GET_TARGET_PROPERTY(_old_properties ${_target} ${_name})
+ MESSAGE("adding property to ${_target} ${_name}:" ${_properties})
+ IF(NOT _old_properties)
+ # in case it's NOTFOUND
+ SET(_old_properties)
+ ENDIF(NOT _old_properties)
+ SET_TARGET_PROPERTIES(${_target} PROPERTIES ${_name} "${_old_properties} ${_properties}")
+ENDMACRO(ADD_TARGET_PROPERTIES)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..b3ce8ea
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,65 @@
+INCLUDE(CheckIncludeFiles)
+INCLUDE(CheckFunctionExists)
+INCLUDE(CheckVariableExists)
+INCLUDE(CheckTypeSize)
+INCLUDE(CheckLibraryExists)
+INCLUDE(CMakeDetermineCCompiler)
+INCLUDE(FindThreads)
+INCLUDE(CPack)
+INCLUDE(FindPkgConfig)
+
+INCLUDE(LighttpdMacros)
+cmake_policy(SET CMP0005 OLD)
+
+ADD_DEFINITIONS(-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGE_FILES)
+
+# libev
+CHECK_INCLUDE_FILES(ev.h HAVE_EV_H)
+IF(HAVE_EV_H)
+ CHECK_LIBRARY_EXISTS(ev ev_loop "" HAVE_LIBEV)
+ IF(HAVE_LIBEV)
+ SET(EV_LDFLAGS -lev)
+ ELSE(HAVE_LIBEV)
+ MESSAGE(FATAL_ERROR "Couldn't find lib ev")
+ ENDIF(HAVE_LIBEV)
+ELSE(HAVE_EV_H)
+ MESSAGE(FATAL_ERROR "Couldn't find <ev.h>")
+ENDIF(HAVE_EV_H)
+
+# glib/gthread
+pkg_check_modules(GTHREAD REQUIRED gthread-2.0)
+#INCLUDE_DIRECTORIES(${GTHREAD_INCLUDE_DIRS})
+
+ADD_DEFINITIONS(
+ -DPACKAGE_NAME="\\"${CMAKE_PROJECT_NAME}\\""
+ -DPACKAGE_VERSION="\\"${CPACK_PACKAGE_VERSION}\\""
+ )
+
+## Write out config.h
+CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h)
+
+ADD_DEFINITIONS(-DHAVE_CONFIG_H)
+
+INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
+
+ADD_EXECUTABLE(fcgi-debug
+ fcgi-debug.c
+ connection.c
+ tools.c
+)
+
+ADD_TARGET_PROPERTIES(fcgi-debug LINK_FLAGS ${EV_LDFLAGS})
+ADD_TARGET_PROPERTIES(fcgi-debug COMPILE_FLAGS ${EV_CFLAGS})
+
+IF(CMAKE_COMPILER_IS_GNUCC)
+ SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Wall -g -Wshadow -W -pedantic")
+ SET(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
+ SET(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0")
+ SET(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_WITHDEBINFO} -O2")
+ ADD_DEFINITIONS(-D_GNU_SOURCE)
+ENDIF(CMAKE_COMPILER_IS_GNUCC)
+
+ADD_TARGET_PROPERTIES(fcgi-debug LINK_FLAGS ${GTHREAD_LDFLAGS})
+ADD_TARGET_PROPERTIES(fcgi-debug COMPILE_FLAGS ${GTHREAD_CFLAGS})
+
+SET_TARGET_PROPERTIES(fcgi-debug PROPERTIES CMAKE_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX})
diff --git a/src/config.h.cmake b/src/config.h.cmake
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/config.h.cmake
diff --git a/src/connection.c b/src/connection.c
new file mode 100644
index 0000000..82fa50a
--- /dev/null
+++ b/src/connection.c
@@ -0,0 +1,174 @@
+#include "fcgi-debug.h"
+
+static void connection_close(connection *con) {
+ ev_io_stop(con->srv->loop, &con->w_server);
+ ev_io_stop(con->srv->loop, &con->w_client);
+ if (-1 != con->fd_server) {
+ shutdown(con->fd_server, SHUT_RDWR);
+ close(con->fd_server);
+ }
+ if (-1 != con->fd_client) {
+ shutdown(con->fd_client, SHUT_RDWR);
+ close(con->fd_client);
+ }
+ g_string_free(con->send_client_buf, TRUE);
+ g_string_free(con->send_server_buf, TRUE);
+ g_slice_free(connection, con);
+}
+
+static gboolean connection_connect(connection *con) {
+ server *srv = con->srv;
+ if (con->client_connected) return TRUE;
+ ev_io_start(srv->loop, &srv->w_accept);
+ if (-1 == connect(con->fd_client, srv->client.saddr, srv->client.addr_len)) {
+ switch (errno) {
+ case EALREADY:
+ case EINPROGRESS:
+ case EINTR:
+ ev_io_stop(srv->loop, &srv->w_accept); /* no new connections until we have a new connection to the client */
+ ev_io_set_events(srv->loop, &con->w_client, EV_WRITE | EV_READ);
+ break;
+ default:
+ g_warning("couldn't connect: %s", g_strerror(errno));
+ connection_close(con);
+ }
+ return FALSE;
+ } else {
+ con->client_connected = TRUE;
+ ev_io_set_events(srv->loop, &con->w_client, EV_READ);
+ ev_io_set_events(srv->loop, &con->w_server, EV_READ);
+ return TRUE;
+ }
+}
+
+static char readbuf[4096];
+
+static void fd_server_cb(struct ev_loop *loop, struct ev_io *w, int revents) {
+ connection *con = (connection*) w->data;
+ server *srv = con->srv;
+ UNUSED(loop);
+
+ if (revents & EV_READ) {
+ int l = read(w->fd, readbuf, sizeof(readbuf));
+ switch (l) {
+ case -1:
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ g_warning("couldn't read from server: %s", g_strerror(errno));
+ connection_close(con);
+ }
+ return;
+ case 0:
+ /* end of file */
+ connection_close(con);
+ return;
+ default:
+ break;
+ }
+ g_string_append_len(con->send_client_buf, readbuf, l);
+ if (con->send_client_buf->len > 4*4096) ev_io_rem_events(srv->loop, w, EV_READ);
+ ev_io_add_events(srv->loop, &con->w_client, EV_WRITE);
+ }
+ if (revents & EV_WRITE) {
+ if (con->send_server_buf->len > 0) {
+ int l = write(w->fd, con->send_server_buf->str, con->send_server_buf->len);
+ switch (l) {
+ case -1:
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ g_warning("couldn't write to server: %s", g_strerror(errno));
+ connection_close(con);
+ }
+ return;
+ }
+ g_string_erase(con->send_server_buf, 0, l);
+ if (con->send_server_buf->len < 4*4096) ev_io_add_events(srv->loop, &con->w_server, EV_READ);
+ }
+ if (con->send_server_buf->len == 0) ev_io_rem_events(srv->loop, w, EV_WRITE);
+ }
+}
+
+static void fd_client_cb(struct ev_loop *loop, struct ev_io *w, int revents) {
+ connection *con = (connection*) w->data;
+ server *srv = con->srv;
+ UNUSED(loop);
+ if (!connection_connect(con)) return;
+
+ if (revents & EV_READ) {
+ int l = read(w->fd, readbuf, sizeof(readbuf));
+ switch (l) {
+ case -1:
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ g_warning("couldn't read from client: %s", g_strerror(errno));
+ connection_close(con);
+ }
+ return;
+ case 0:
+ /* end of file */
+ connection_close(con);
+ return;
+ default:
+ break;
+ }
+ g_string_append_len(con->send_server_buf, readbuf, l);
+ if (con->send_server_buf->len > 4*4096) ev_io_rem_events(srv->loop, w, EV_READ);
+ ev_io_add_events(srv->loop, &con->w_server, EV_WRITE);
+ }
+ if (revents & EV_WRITE) {
+ if (con->send_client_buf->len > 0) {
+ int l = write(w->fd, con->send_client_buf->str, con->send_client_buf->len);
+ switch (l) {
+ case -1:
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ g_warning("couldn't write to client: %s", g_strerror(errno));
+ connection_close(con);
+ }
+ return;
+ }
+ g_string_erase(con->send_client_buf, 0, l);
+ if (con->send_client_buf->len < 4*4096) ev_io_add_events(srv->loop, &con->w_client, EV_READ);
+ }
+ if (con->send_client_buf->len == 0) ev_io_rem_events(srv->loop, w, EV_WRITE);
+ }
+}
+
+void connection_new(server *srv, int fd_server) {
+ connection *con;
+ int fd_client;
+
+ fd_init(fd_server);
+ con = g_slice_new0(connection);
+ con->srv = srv;
+ con->fd_server = fd_server;
+ con->fd_client = -1;
+ ev_io_init(&con->w_server, fd_server_cb, fd_server, 0);
+ con->w_server.data = con;
+ ev_io_init(&con->w_client, fd_client_cb, -1, 0);
+ con->w_client.data = con;
+ con->send_client_buf = g_string_sized_new(0);
+ con->send_server_buf = g_string_sized_new(0);
+
+ if (-1 == (fd_client = socket(AF_UNIX, SOCK_STREAM, 0))) {
+ g_warning("couldn't create socket: %s", g_strerror(errno));
+ connection_close(con);
+ }
+ fd_init(fd_client);
+ con->fd_client = fd_client;
+ ev_io_set(&con->w_client, fd_client, EV_WRITE | EV_READ);
+ con->client_connected = FALSE;
+ connection_connect(con);
+}
diff --git a/src/fcgi-debug.c b/src/fcgi-debug.c
new file mode 100644
index 0000000..a2fbeea
--- /dev/null
+++ b/src/fcgi-debug.c
@@ -0,0 +1,187 @@
+
+#include "fcgi-debug.h"
+
+static void accept_cb(struct ev_loop *loop, struct ev_io *w, int revents) {
+ server *srv = (server*) w->data;
+ int fd;
+ UNUSED(loop);
+ UNUSED(revents);
+
+ if (-1 == (fd = accept(w->fd, NULL, NULL))) {
+ g_error("Couldn't accept: %s", g_strerror(errno));
+ }
+
+ connection_new(srv, fd);
+}
+
+#define CATCH_SIGNAL(loop, cb, n) do {\
+ ev_init(&srv->sig_w_##n, cb); \
+ ev_signal_set(&srv->sig_w_##n, SIG##n); \
+ ev_signal_start(loop, &srv->sig_w_##n); \
+ srv->sig_w_##n.data = srv; \
+ ev_unref(loop); /* Signal watchers shouldn't keep loop alive */ \
+} while (0)
+
+#define UNCATCH_SIGNAL(loop, n) do {\
+ ev_ref(loop); \
+ ev_signal_stop(loop, &srv->sig_w_##n); \
+} while (0)
+
+void server_stop(server *srv) {
+ if (srv->tmpfile_name) {
+ unlink(srv->tmpfile_name);
+ g_free(srv->tmpfile_name);
+ srv->tmpfile_name = NULL;
+ }
+ if (srv->sockfile_name) {
+ unlink(srv->sockfile_name);
+ g_free(srv->sockfile_name);
+ srv->sockfile_name = NULL;
+ }
+ ev_io_stop(srv->loop, &srv->w_accept);
+ close(0);
+ if (!srv->exiting) {
+ if (-1 != srv->child) kill(srv->child, SIGINT);
+ } else {
+ if (-1 != srv->child) kill(srv->child, SIGTERM);
+
+ if (!srv->stopped_signals) {
+ srv->stopped_signals = TRUE;
+ /* reset default behaviour which will kill us next time */
+ UNCATCH_SIGNAL(srv->loop, INT);
+ UNCATCH_SIGNAL(srv->loop, TERM);
+ UNCATCH_SIGNAL(srv->loop, PIPE);
+ UNCATCH_SIGNAL(srv->loop, HUP);
+ }
+ }
+}
+
+static void child_cb(struct ev_loop *loop, struct ev_child *w, int revents) {
+ server *srv = (server*) w->data;
+ UNUSED(revents);
+
+ ev_child_stop(loop, w);
+ g_message ("process %d exited with status %d", w->rpid, w->rstatus);
+ srv->child = -1;
+ server_stop(srv);
+}
+
+static void sigint_cb(struct ev_loop *loop, struct ev_signal *w, int revents) {
+ server *srv = (server*) w->data;
+ UNUSED(loop);
+ UNUSED(revents);
+
+ if (!srv->exiting) {
+ g_message("Got signal, shutdown");
+ } else if (w->signum != SIGINT && w->signum != SIGTERM) {
+ return; /* ignore */
+ } else {
+ g_message("Got second signal, force shutdown");
+ }
+ server_stop(srv);
+ if (w->signum == SIGINT || w->signum == SIGTERM) srv->exiting = TRUE;
+}
+
+static void sigpipe_cb(struct ev_loop *loop, struct ev_signal *w, int revents) {
+ /* ignore */
+ UNUSED(loop); UNUSED(w); UNUSED(revents);
+}
+
+void create_tmp_addr(server *srv) {
+ gchar *fn = g_strdup("/tmp/fcgi-debug.XXXXXX");
+ int fd;
+
+ struct sockaddr_un *sun;
+ gsize slen = strlen(fn) + sizeof(".sock") - 1, len = 1 + slen + (gsize) (((struct sockaddr_un *) 0)->sun_path);
+ sun = (struct sockaddr_un*) g_malloc0(len);
+ sun->sun_family = AF_UNIX;
+
+ if (-1 == (fd = mkstemp(fn))) {
+ g_error("Couldn't make a tmpfile name");
+ }
+ close(fd);
+ strcpy(sun->sun_path, fn);
+ strcat(sun->sun_path, ".sock");
+ srv->sockfile_name = g_strdup(sun->sun_path);
+ srv->tmpfile_name = fn;
+
+ srv->client.saddr = (struct sockaddr*) sun;
+ srv->client.addr_len = len;
+}
+
+int client_bind(server *srv) {
+ int s;
+
+ if (-1 == (s = socket(AF_UNIX, SOCK_STREAM, 0))) {
+ g_error("Couldn't create socket: %s", g_strerror(errno));
+ }
+
+ if (-1 == bind(s, srv->client.saddr, srv->client.addr_len)) {
+ g_error("Couldn't bind socket: %s", g_strerror(errno));
+ }
+
+ if (-1 == listen(s, 1024)) {
+ g_error("Couldn't listen on socket: %s", g_strerror(errno));
+ }
+
+ return s;
+}
+
+pid_t spawn(char **argv, int s) {
+ pid_t child;
+
+ switch (child = fork()) {
+ case -1:
+ g_error("Fork failed: %s", g_strerror(errno));
+ break;
+ case 0: /* child */
+ setsid();
+ move2fd(s, 0);
+ execv(argv[1], argv+1);
+ g_error("execv failed: %s", g_strerror(errno));
+ break;
+ default:
+ close(s);
+ break;
+ }
+
+ return child;
+}
+
+int main(int argc, char **argv) {
+ server *srv;
+ int s;
+ pid_t ch;
+ UNUSED(argc);
+
+ srv = g_slice_new0(server);
+ srv->exiting = FALSE;
+ srv->child = -1;
+ srv->loop = ev_default_loop (0);
+
+ CATCH_SIGNAL(srv->loop, sigint_cb, INT);
+ CATCH_SIGNAL(srv->loop, sigint_cb, TERM);
+ CATCH_SIGNAL(srv->loop, sigint_cb, HUP);
+ CATCH_SIGNAL(srv->loop, sigpipe_cb, PIPE);
+
+ create_tmp_addr(srv);
+ s = client_bind(srv);
+
+ ch = spawn(argv, s);
+
+ srv->child = ch;
+ ev_child_init(&srv->w_child, child_cb, ch, 0);
+ srv->w_child.data = srv;
+ ev_child_start(srv->loop, &srv->w_child);
+
+ fd_init(0);
+ ev_io_init(&srv->w_accept, accept_cb, 0, EV_READ);
+ srv->w_accept.data = srv;
+ ev_io_start(srv->loop, &srv->w_accept);
+
+ ev_loop(srv->loop, 0);
+
+ g_message("exit fcgi-debug");
+ server_stop(srv);
+ return 0;
+}
diff --git a/src/fcgi-debug.h b/src/fcgi-debug.h
new file mode 100644
index 0000000..9ae91f5
--- /dev/null
+++ b/src/fcgi-debug.h
@@ -0,0 +1,70 @@
+
+#include <ev.h>
+#include <glib.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#define UNUSED(x) ( (void)(x) )
+
+struct addr;
+typedef struct addr addr;
+
+struct server;
+typedef struct server server;
+
+struct connection;
+typedef struct connection connection;
+
+struct addr {
+ struct sockaddr *saddr;
+ socklen_t addr_len;
+};
+
+struct server {
+ struct ev_loop *loop;
+
+ ev_signal
+ sig_w_INT,
+ sig_w_TERM,
+ sig_w_PIPE,
+ sig_w_HUP
+ ;
+
+ ev_child w_child;
+ ev_io w_accept;
+ int child;
+
+ gchar *tmpfile_name, *sockfile_name;
+ addr client;
+
+ gboolean exiting, stopped_signals;
+};
+
+struct connection {
+ server *srv;
+ int fd_server, fd_client;
+ ev_io w_server, w_client;
+ gboolean client_connected;
+
+ GString *send_client_buf, *send_server_buf;
+};
+
+
+/* tools.c */
+void move2fd(int srcfd, int dstfd);
+void move2devnull(int fd);
+void fd_init(int fd);
+
+void ev_io_add_events(struct ev_loop *loop, ev_io *watcher, int events);
+void ev_io_rem_events(struct ev_loop *loop, ev_io *watcher, int events);
+void ev_io_set_events(struct ev_loop *loop, ev_io *watcher, int events);
+
+/* connection.c */
+void connection_new(server *srv, int fd_server);
diff --git a/src/tools.c b/src/tools.c
new file mode 100644
index 0000000..70bff25
--- /dev/null
+++ b/src/tools.c
@@ -0,0 +1,51 @@
+#include "fcgi-debug.h"
+
+/* move a fd to another and close the old one */
+void move2fd(int srcfd, int dstfd) {
+ if (srcfd != dstfd) {
+ close(dstfd);
+ dup2(srcfd, dstfd);
+ close(srcfd);
+ }
+}
+
+/* replace an fd with /dev/null */
+void move2devnull(int fd) {
+ move2fd(open("/dev/null", O_RDWR), fd);
+}
+
+void fd_init(int fd) {
+#ifdef _WIN32
+ int i = 1;
+#endif
+#ifdef FD_CLOEXEC
+ /* close fd on exec (cgi) */
+ fcntl(fd, F_SETFD, FD_CLOEXEC);
+#endif
+#ifdef O_NONBLOCK
+ fcntl(fd, F_SETFL, O_NONBLOCK | O_RDWR);
+#elif defined _WIN32
+ ioctlsocket(fd, FIONBIO, &i);
+#endif
+}
+
+void ev_io_add_events(struct ev_loop *loop, ev_io *watcher, int events) {
+ if ((watcher->events & events) == events) return;
+ ev_io_stop(loop, watcher);
+ ev_io_set(watcher, watcher->fd, watcher->events | events);
+ ev_io_start(loop, watcher);
+}
+
+void ev_io_rem_events(struct ev_loop *loop, ev_io *watcher, int events) {
+ if (0 == (watcher->events & events)) return;
+ ev_io_stop(loop, watcher);
+ ev_io_set(watcher, watcher->fd, watcher->events & ~events);
+ ev_io_start(loop, watcher);
+}
+
+void ev_io_set_events(struct ev_loop *loop, ev_io *watcher, int events) {
+ if (events == (watcher->events & (EV_READ | EV_WRITE))) return;
+ ev_io_stop(loop, watcher);
+ ev_io_set(watcher, watcher->fd, (watcher->events & ~(EV_READ | EV_WRITE)) | events);
+ ev_io_start(loop, watcher);
+}