From: Jean Privat Date: Mon, 13 Aug 2018 19:24:43 +0000 (-0400) Subject: Merge: libevent: support UNIX domain sockets and add a test and an example X-Git-Url: http://nitlanguage.org?hp=dfad28c63380f652c2e5f10576c7b7fd212d4111 Merge: libevent: support UNIX domain sockets and add a test and an example Libevent server can now listen on UNIX domain sockets by calling `ConnectionFactory::bind_unix`. The method to listen on TCP ports was renamed to `bind_tcp` for clarity. This PR intro two new supporting programs in the form of a parallel test for a libevent server and a minimal usage example. If we already had these let me know, because otherwise these two were long overdue. This PR is built on the work of @matthmsl in #2708, thank you! Close #2679 Pull-Request: #2726 --- diff --git a/lib/libevent/libevent.nit b/lib/libevent/libevent.nit index ff10b05..ec7d290 100644 --- a/lib/libevent/libevent.nit +++ b/lib/libevent/libevent.nit @@ -2,6 +2,7 @@ # # Copyright 2013 Jean-Philippe Caissy # Copyright 2014 Alexis Laferrière +# Copyright 2018 Matthieu Le Guellaut # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -38,6 +39,8 @@ in "C" `{ #include #include #include + #include + #include // Protect callbacks for compatibility with light FFI #ifdef Connection_decr_ref @@ -145,8 +148,6 @@ interface EventCallback end # Spawned to manage a specific connection -# -# TODO, use polls class Connection super Writer @@ -418,31 +419,49 @@ end # A listener acting on an interface and port, spawns `Connection` on new connections extern class ConnectionListener `{ struct evconnlistener * `} - private new bind_to(base: NativeEventBase, address: CString, port: Int, factory: ConnectionFactory) + private new bind_tcp(base: NativeEventBase, address: CString, port: Int, factory: ConnectionFactory) import ConnectionFactory.accept_connection, error_callback `{ - struct sockaddr_in sin; - struct evconnlistener *listener; ConnectionFactory_incr_ref(factory); struct hostent *hostent = gethostbyname(address); - if (!hostent) { return NULL; } - memset(&sin, 0, sizeof(sin)); + struct sockaddr_in sin = {0}; sin.sin_family = hostent->h_addrtype; sin.sin_port = htons(port); memcpy( &(sin.sin_addr.s_addr), (const void*)hostent->h_addr, hostent->h_length ); - listener = evconnlistener_new_bind(base, + struct evconnlistener *listener = evconnlistener_new_bind(base, (evconnlistener_cb)accept_connection_cb, factory, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, (struct sockaddr*)&sin, sizeof(sin)); + if (listener != NULL) { + evconnlistener_set_error_cb(listener, + (evconnlistener_errorcb)ConnectionListener_error_callback); + } + + return listener; + `} + + private new bind_unix(base: NativeEventBase, file: CString, factory: ConnectionFactory) + import ConnectionFactory.accept_connection, error_callback `{ + + ConnectionFactory_incr_ref(factory); + + struct sockaddr_un sun = {0}; + sun.sun_family = AF_UNIX; + strncpy(sun.sun_path, file, sizeof(sun.sun_path) - 1); + struct evconnlistener *listener = evconnlistener_new_bind(base, + (evconnlistener_cb)accept_connection_cb, factory, + LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, + (struct sockaddr*)&sun, sizeof(sun)); if (listener != NULL) { - evconnlistener_set_error_cb(listener, (evconnlistener_errorcb)ConnectionListener_error_callback); + evconnlistener_set_error_cb(listener, + (evconnlistener_errorcb)ConnectionListener_error_callback); } return listener; @@ -451,15 +470,17 @@ extern class ConnectionListener `{ struct evconnlistener * `} # Get the `NativeEventBase` associated to `self` fun base: NativeEventBase `{ return evconnlistener_get_base(self); `} - # Callback method on listening error - fun error_callback do + # Callback on listening error + fun error_callback + do var cstr = evutil_socket_error_to_string(evutil_socket_error) - print_error "libevent error: '{cstr}'" + print_error "libevent error: {cstr}" end end # Factory to listen on sockets and create new `Connection` class ConnectionFactory + # The `NativeEventBase` for the dispatch loop of this factory var event_base: NativeEventBase @@ -490,17 +511,45 @@ class ConnectionFactory return new Connection(buffer_event) end - # Listen on `address`:`port` for new connection, which will callback `spawn_connection` - fun bind_to(address: String, port: Int): nullable ConnectionListener + # Listen on the TCP socket at `address`:`port` for new connections + # + # On new connections, libevent callbacks `spawn_connection`. + fun bind_tcp(address: String, port: Int): nullable ConnectionListener do - var listener = new ConnectionListener.bind_to(event_base, address.to_cstring, port, self) + var listener = new ConnectionListener.bind_tcp( + event_base, address.to_cstring, port, self) + if listener.address_is_null then - sys.stderr.write "libevent warning: Opening {address}:{port} failed\n" + print_error "libevent warning: Opening {address}:{port} failed, " + + evutil_socket_error_to_string(evutil_socket_error).to_s + return null end + return listener end - # Put string representation of source `address` into `buf` + # Listen on a UNIX domain socket for new connections + # + # On new connections, libevent callbacks `spawn_connection`. + fun bind_unix(path: String): nullable ConnectionListener + do + # Delete the socket if it already exists + var stat = path.file_stat + if stat != null and stat.is_sock then path.file_delete + + var listener = new ConnectionListener.bind_unix( + event_base, path.to_cstring, self) + + if listener.address_is_null then + print_error "libevent warning: Opening UNIX domain socket {path} failed, " + + evutil_socket_error_to_string(evutil_socket_error).to_s + return null + end + + return listener + end + + # Put a human readable string representation of `address` into `buf` private fun addrin_to_address(address: Pointer, buf: CString, buf_len: Int): CString `{ struct sockaddr *addrin = (struct sockaddr*)address; @@ -512,6 +561,14 @@ class ConnectionFactory struct in6_addr *src = &((struct sockaddr_in6*)addrin)->sin6_addr; return (char *)inet_ntop(addrin->sa_family, src, buf, buf_len); } + else if (addrin->sa_family == AF_UNIX) { + struct sockaddr_un *src = (struct sockaddr_un*)addrin; + char *path = src->sun_path; + if (path == NULL) return "Unnamed UNIX domain socket"; + if (path[0] == '\0') return "Abstract UNIX domain socket"; + return path; + } + return NULL; `} end diff --git a/lib/libevent/libevent_example.nit b/lib/libevent/libevent_example.nit new file mode 100644 index 0000000..c00917e --- /dev/null +++ b/lib/libevent/libevent_example.nit @@ -0,0 +1,57 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Minimal usage example of libevent +module libevent_example is example + +import libevent + +# Factory creating instances of `EchoConnection` to handle new connections +class MyFactory + super ConnectionFactory + + redef fun spawn_connection(buf, address) + do + return new EchoConnection(buf) + end +end + +# Connection echoing data received from clients back at them +class EchoConnection + super Connection + + redef fun read_callback(content) + do + print "Received: {content}" + write content + end +end + +# Skip the actual execution when testing +if "NIT_TESTING".environ == "true" then exit 0 + +# Setup libevent system +var event_base = new NativeEventBase +var factory = new MyFactory(event_base) + +# Open a TCP socket for listening +factory.bind_tcp("localhost", 8888) + +# Open a UNIX domain socket for listening +factory.bind_unix("/tmp/my.sck") + +# Launch event loop +event_base.dispatch + +event_base.free diff --git a/lib/libevent/libevent_test.nit b/lib/libevent/libevent_test.nit new file mode 100644 index 0000000..4c64b2e --- /dev/null +++ b/lib/libevent/libevent_test.nit @@ -0,0 +1,94 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module libevent_test + +import libevent +import pthreads + +redef class Sys + + var testing_id: Int is lazy do + var id = "NIT_TESTING_ID".environ + return if id.is_empty then 0 else id.to_i + end + + # Config for test sockets + var tcp_addr = "localhost" + var tcp_port: Int = 20000 + testing_id + var unix_socket_path = "/tmp/libevent_test{testing_id}.sck" +end + +class TestConnectionFactory + super ConnectionFactory + + redef fun spawn_connection(buf, address) + do + print "[Server] New client: {address}" + + var conn = new TestConnection(buf) + print "[Server] Write: Hi" + conn.write "Hi\n" + return conn + end +end + +class TestConnection + super Connection + + redef fun read_callback(content) + do + 0.2.sleep # Forcing the server output after the client output + printn "[Server] Read: {content}" + end +end + +class ServerThread + super Thread + + redef fun main + do + var event_base = new NativeEventBase + var factory = new TestConnectionFactory(event_base) + + # Bind TCP socket + factory.bind_tcp(tcp_addr, tcp_port) + + # Bind UNIX domain socket + factory.bind_unix unix_socket_path + + event_base.dispatch + event_base.free + + return null + end +end + +redef fun system(cmd) +do + if testing_id == 0 then print "[Client] {cmd}" + return super(cmd) +end + +# First, launch a server in the background +var server = new ServerThread +server.start +0.1.sleep + +# Test what should succeed +system "echo 'Hello TCP' | nc -N {tcp_addr} {tcp_port}" +system "echo 'Hello UNIX' | nc -NU {unix_socket_path}" + +1.0.sleep +exit 0 diff --git a/lib/libevent/package.ini b/lib/libevent/package.ini index c24d489..38f78e9 100644 --- a/lib/libevent/package.ini +++ b/lib/libevent/package.ini @@ -1,6 +1,6 @@ [package] name=libevent -tags=wrapper,lib +tags=network,wrapper,lib maintainer=Alexis Laferrière license=Apache-2.0 desc=Low-level wrapper around the libevent library to manage events on file descriptors diff --git a/lib/nitcorn/reactor.nit b/lib/nitcorn/reactor.nit index 85a2068..def3699 100644 --- a/lib/nitcorn/reactor.nit +++ b/lib/nitcorn/reactor.nit @@ -176,7 +176,7 @@ redef class Sys var listener = listeners[name, port] if listener == null then - listener = factory.bind_to(name, port) + listener = factory.bind_tcp(name, port) if listener != null then sys.listeners[name, port] = listener listeners_count[name, port] = 1 diff --git a/tests/sav/libevent_test.res b/tests/sav/libevent_test.res new file mode 100644 index 0000000..b0f29bd --- /dev/null +++ b/tests/sav/libevent_test.res @@ -0,0 +1,8 @@ +[Server] New client: 127.0.0.1 +[Server] Write: Hi +Hi +[Server] Read: Hello TCP +[Server] New client: Abstract UNIX domain socket +[Server] Write: Hi +Hi +[Server] Read: Hello UNIX diff --git a/tests/sav/simple_file_server.res b/tests/sav/simple_file_server.res index 12d09fe..4d7cf99 100644 --- a/tests/sav/simple_file_server.res +++ b/tests/sav/simple_file_server.res @@ -1 +1 @@ -libevent warning: Opening localhost:80 failed +libevent warning: Opening localhost:80 failed, Permission denied