Merge: libevent: support UNIX domain sockets and add a test and an example
authorJean Privat <jean@pryen.org>
Mon, 13 Aug 2018 19:24:43 +0000 (15:24 -0400)
committerJean Privat <jean@pryen.org>
Mon, 13 Aug 2018 19:24:43 +0000 (15:24 -0400)
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

lib/libevent/libevent.nit
lib/libevent/libevent_example.nit [new file with mode: 0644]
lib/libevent/libevent_test.nit [new file with mode: 0644]
lib/libevent/package.ini
lib/nitcorn/reactor.nit
tests/sav/libevent_test.res [new file with mode: 0644]
tests/sav/simple_file_server.res

index ff10b05..ec7d290 100644 (file)
@@ -2,6 +2,7 @@
 #
 # Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+# Copyright 2018 Matthieu Le Guellaut <leguellaut.matthieu@gmail.com>
 #
 # 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 <arpa/inet.h>
        #include <netinet/in.h>
        #include <netinet/ip.h>
+       #include <sys/un.h>
+       #include <unistd.h>
 
 // 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 (file)
index 0000000..c00917e
--- /dev/null
@@ -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 (file)
index 0000000..4c64b2e
--- /dev/null
@@ -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
index c24d489..38f78e9 100644 (file)
@@ -1,6 +1,6 @@
 [package]
 name=libevent
-tags=wrapper,lib
+tags=network,wrapper,lib
 maintainer=Alexis Laferrière <alexis.laf@xymus.net>
 license=Apache-2.0
 desc=Low-level wrapper around the libevent library to manage events on file descriptors
index 85a2068..def3699 100644 (file)
@@ -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 (file)
index 0000000..b0f29bd
--- /dev/null
@@ -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
index 12d09fe..4d7cf99 100644 (file)
@@ -1 +1 @@
-libevent warning: Opening localhost:80 failed
+libevent warning: Opening localhost:80 failed, Permission denied