Browse Source

[5189] Implemented ClientConnection to communicate over unix socket.

Marcin Siodelski 8 years ago
parent
commit
768815986d

+ 9 - 7
src/lib/asiolink/tests/unix_domain_socket_unittest.cc

@@ -95,7 +95,8 @@ TEST_F(UnixDomainSocketTest, sendReceive) {
     ASSERT_EQ(outbound_data.size(), sent_size);
 
     // Run IO service to generate server's response.
-    while (test_socket_.getResponseNum() < 1) {
+    while ((test_socket_->getResponseNum() < 1) &&
+           (!test_socket_->isStopped())) {
         io_service_.run_one();
     }
 
@@ -136,7 +137,7 @@ TEST_F(UnixDomainSocketTest, asyncSendReceive) {
         }
     ));
     // Run IO service until connect handler is invoked.
-    while (!connect_handler_invoked) {
+    while (!connect_handler_invoked && (!test_socket_->isStopped())) {
         io_service_.run_one();
     }
 
@@ -158,7 +159,8 @@ TEST_F(UnixDomainSocketTest, asyncSendReceive) {
     ));
 
     // Run IO service to generate server's response.
-    while (test_socket_->getResponseNum() < 1) {
+    while ((test_socket_->getResponseNum() < 1) &&
+           (!test_socket_->isStopped())) {
         io_service_.run_one();
     }
 
@@ -189,7 +191,7 @@ TEST_F(UnixDomainSocketTest, asyncSendReceive) {
 
     }));
     // Run IO service until we get some response from the server.
-    while (!receive_handler_invoked) {
+    while (!receive_handler_invoked && !test_socket_->isStopped()) {
         io_service_.run_one();
     }
 
@@ -230,7 +232,7 @@ TEST_F(UnixDomainSocketTest, asyncClientErrors) {
         connect_handler_invoked = true;
         EXPECT_TRUE(ec);
     });
-    while (!connect_handler_invoked) {
+    while (!connect_handler_invoked && !test_socket_->isStopped()) {
         io_service_.run_one();
     }
 
@@ -243,7 +245,7 @@ TEST_F(UnixDomainSocketTest, asyncClientErrors) {
         send_handler_invoked = true;
         EXPECT_TRUE(ec);
     });
-    while (!send_handler_invoked) {
+    while (!send_handler_invoked && !test_socket_->isStopped()) {
         io_service_.run_one();
     }
 
@@ -256,7 +258,7 @@ TEST_F(UnixDomainSocketTest, asyncClientErrors) {
         receive_handler_invoked = true;
         EXPECT_TRUE(ec);
     });
-    while (!receive_handler_invoked) {
+    while (!receive_handler_invoked && !test_socket_->isStopped()) {
         io_service_.run_one();
     }
 }

+ 23 - 5
src/lib/asiolink/testutils/test_server_unix_socket.cc

@@ -10,6 +10,7 @@
 #include <boost/shared_ptr.hpp>
 #include <functional>
 #include <set>
+#include <sstream>
 
 using namespace boost::asio::local;
 
@@ -200,16 +201,14 @@ private:
 
 TestServerUnixSocket::TestServerUnixSocket(IOService& io_service,
                                            const std::string& socket_file_path,
-                                           const long test_timeout,
                                            const std::string& custom_response)
     : io_service_(io_service),
       server_endpoint_(socket_file_path),
       server_acceptor_(io_service_.get_io_service()),
       test_timer_(io_service_),
       custom_response_(custom_response),
-      connection_pool_(new ConnectionPool(io_service)) {
-    test_timer_.setup(boost::bind(&TestServerUnixSocket::timeoutHandler, this),
-                      test_timeout, IntervalTimer::ONE_SHOT);
+      connection_pool_(new ConnectionPool(io_service)),
+      stopped_(false) {
 }
 
 TestServerUnixSocket::~TestServerUnixSocket() {
@@ -217,6 +216,24 @@ TestServerUnixSocket::~TestServerUnixSocket() {
 }
 
 void
+TestServerUnixSocket::generateCustomResponse(const uint64_t response_size) {
+    std::ostringstream s;
+    s << "{";
+    while (s.tellp() < response_size) {
+        s << "\"param\": \"value\",";
+    }
+    s << "}";
+    custom_response_ = s.str();
+}
+
+void
+TestServerUnixSocket::startTimer(const long test_timeout) {
+    test_timer_.setup(boost::bind(&TestServerUnixSocket::timeoutHandler,
+                                  shared_from_this()),
+                      test_timeout, IntervalTimer::ONE_SHOT);
+}
+
+void
 TestServerUnixSocket::bindServerSocket() {
     server_acceptor_.open();
     server_acceptor_.bind(server_endpoint_);
@@ -233,13 +250,14 @@ TestServerUnixSocket::acceptHandler(const boost::system::error_code&) {
 void
 TestServerUnixSocket::accept() {
     server_acceptor_.async_accept(*(connection_pool_->getSocket()),
-        boost::bind(&TestServerUnixSocket::acceptHandler, this, _1));
+        boost::bind(&TestServerUnixSocket::acceptHandler, shared_from_this(), _1));
 }
 
 void
 TestServerUnixSocket::timeoutHandler() {
     ADD_FAILURE() << "Timeout occurred while running the test!";
     io_service_.stop();
+    stopped_ = true;
 }
 
 size_t

+ 21 - 2
src/lib/asiolink/testutils/test_server_unix_socket.h

@@ -13,6 +13,7 @@
 #include <boost/shared_ptr.hpp>
 #include <gtest/gtest.h>
 #include <list>
+#include <stdint.h>
 #include <string>
 
 namespace isc {
@@ -45,11 +46,9 @@ public:
     ///
     /// @param io_service IO service.
     /// @param socket_file_path Socket file path.
-    /// @param test_timeout Test timeout in milliseconds.
     /// @param custom_response Custom response to be sent to the client.
     TestServerUnixSocket(IOService& io_service,
                          const std::string& socket_file_path,
-                         const long test_timeout,
                          const std::string& custom_response = "");
 
     /// @brief Destructor.
@@ -57,6 +56,16 @@ public:
     /// Closes active connections.
     ~TestServerUnixSocket();
 
+    /// @brief Starts timer for detecting test timeout.
+    ///
+    /// @param test_timeout Test timeout in milliseconds.
+    void startTimer(const long test_timeout);
+
+    /// @brief Generates response of a given length.
+    ///
+    /// @param response_size Desired response size.
+    void generateCustomResponse(const uint64_t response_size);
+
     /// @brief Creates and binds server socket.
     void bindServerSocket();
 
@@ -73,6 +82,12 @@ public:
     /// @brief Return number of responses sent so far to the clients.
     size_t getResponseNum() const;
 
+    /// @brief Checks if IO service has been stopped as a result of the
+    /// timeout.
+    bool isStopped() const {
+        return (stopped_);
+    }
+
 private:
 
     /// @brief Asynchronously accept new connections.
@@ -94,6 +109,10 @@ private:
 
     /// @brief Pool of connections.
     boost::shared_ptr<ConnectionPool> connection_pool_;
+
+    /// @brief Indicates if IO service has been stopped as a result of
+    /// a timeout.
+    bool stopped_;
 };
 
 /// @brief Pointer to the @ref TestServerUnixSocket.

+ 0 - 1
src/lib/cc/json_feed.cc

@@ -7,7 +7,6 @@
 #include <cc/data.h>
 #include <cc/json_feed.h>
 #include <boost/bind.hpp>
-#include <iostream>
 
 using namespace isc::data;
 using namespace isc::util;

+ 13 - 1
src/lib/cc/json_feed.h

@@ -7,8 +7,10 @@
 #ifndef JSON_FEED_H
 #define JSON_FEED_H
 
+#include <cc/data.h>
 #include <exceptions/exceptions.h>
 #include <util/state_model.h>
+#include <boost/shared_ptr.hpp>
 #include <list>
 #include <stdint.h>
 #include <string>
@@ -16,6 +18,14 @@
 namespace isc {
 namespace config {
 
+class JSONFeed;
+
+/// @brief Pointer to the @ref JSONFeed.
+typedef boost::shared_ptr<JSONFeed> JSONFeedPtr;
+
+/// @brief Pointer to the const @ref JSONFeed.
+typedef boost::shared_ptr<const JSONFeed> ConstJSONFeedPtr;
+
 /// @brief A generic exception thrown upon an error in the @ref JSONFeed.
 class JSONFeedError : public Exception {
 public:
@@ -140,7 +150,9 @@ public:
     bool feedOk() const;
 
     /// @brief Returns error string when data processing has failed.
-    std::string getErrorMessage() const;
+    std::string getErrorMessage() const {
+        return (error_message_);
+    }
 
     /// @brief Returns processed data as a structure of @ref isc::data::Element
     /// objects.

+ 1 - 0
src/lib/config/Makefile.am

@@ -16,6 +16,7 @@ lib_LTLIBRARIES = libkea-cfgclient.la
 libkea_cfgclient_la_SOURCES = config_data.h config_data.cc
 libkea_cfgclient_la_SOURCES += module_spec.h module_spec.cc
 libkea_cfgclient_la_SOURCES += base_command_mgr.cc base_command_mgr.h
+libkea_cfgclient_la_SOURCES += client_connection.cc client_connection.h
 libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h
 libkea_cfgclient_la_SOURCES += command_socket.cc command_socket.h
 libkea_cfgclient_la_SOURCES += command_socket_factory.cc command_socket_factory.h

+ 275 - 0
src/lib/config/client_connection.cc

@@ -0,0 +1,275 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/interval_timer.h>
+#include <asiolink/unix_domain_socket.h>
+#include <cc/json_feed.h>
+#include <config/client_connection.h>
+#include <boost/bind.hpp>
+#include <boost/enable_shared_from_this.hpp>
+#include <array>
+
+using namespace isc::asiolink;
+
+namespace isc {
+namespace config {
+
+/// @brief Implementation of the @ref ClientConnection.
+class ClientConnectionImpl : public boost::enable_shared_from_this<ClientConnectionImpl> {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service Reference to the IO service.
+    explicit ClientConnectionImpl(IOService& io_service);
+
+    /// @brief Starts asynchronous transaction with a remote endpoint.
+    ///
+    /// See @ref ClientConnection::start documentation for the details.
+    ///
+    /// @param socket_path Path to the socket description that the server
+    /// is bound to.
+    /// @param command Control command to be sent to the server.
+    /// @param handler Pointer to the user suppiled callback function which
+    /// should be invoked when transaction completes or when an error has
+    /// occurred during the transaction.
+    /// @param timeout Connection timeout in milliseconds.
+    void start(const ClientConnection::SocketPath& socket_path,
+               const ClientConnection::ControlCommand& command,
+               const ClientConnection::Handler& handler,
+               const ClientConnection::Timeout& timeout);
+
+    /// @brief Closes the socket.
+    void stop();
+
+    /// @brief Starts asynchronous send.
+    ///
+    /// This method may be called multiple times internally when the command
+    /// is large and can't be sent all at once.
+    ///
+    /// @param buffer Pointer to the buffer holding input data.
+    /// @param length Length of the data in the input buffer.
+    /// @param handler User supplied callback.
+    void doSend(const void* buffer, const size_t length,
+                const ClientConnection::Handler& handler);
+
+    /// @brief Starts asynchronous receive from the server.
+    ///
+    /// This method may be called multiple times internally if the response
+    /// is large. The @ref JSONFeed instance is used to detect the boundaries
+    /// of the command within the stream. Once the entire command has been
+    /// received the user callback is invoked and the instance of the
+    /// @ref JSONFeed is returned.
+    ///
+    /// @param handler User supplied callback.
+    void doReceive(const ClientConnection::Handler& handler);
+
+    /// @brief Terminates the connection and invokes a user callback indicating
+    /// an error.
+    ///
+    /// @param ec Error code.
+    /// @param handler User callback.
+    void terminate(const boost::system::error_code& ec,
+                   const ClientConnection::Handler& handler);
+
+    /// @brief Callback invoked when the timeout occurs.
+    ///
+    /// It calls @ref terminate with the @c boost::asio::error::timed_out.
+    void timeoutCallback(const ClientConnection::Handler& handler);
+
+private:
+
+    /// @brief Unix domain socket used for communication with a server.
+    UnixDomainSocket socket_;
+
+    /// @brief Pointer to the @ref JSONFeed holding a response.
+    ///
+    ///It may be a null pointer until some part of a response has been received.
+    JSONFeedPtr feed_;
+
+    /// @brief Holds the entire command being transmitted over the unix
+    /// socket.
+    std::string current_command_;
+
+    /// @brief Buffer into which chunks of the response are received.
+    std::array<char, 1024> read_buf_;
+
+    /// @brief Instance of the interval timer protecting against timeouts.
+    IntervalTimer timer_;
+};
+
+ClientConnectionImpl::ClientConnectionImpl(IOService& io_service)
+    : socket_(io_service), feed_(), current_command_(), timer_(io_service) {
+}
+
+void
+ClientConnectionImpl::start(const ClientConnection::SocketPath& socket_path,
+                            const ClientConnection::ControlCommand& command,
+                            const ClientConnection::Handler& handler,
+                            const ClientConnection::Timeout& timeout) {
+    // Start the timer protecting against timeouts.
+    timer_.setup(boost::bind(&ClientConnectionImpl::timeoutCallback,
+                             shared_from_this(), handler),
+                 timeout.timeout_, IntervalTimer::ONE_SHOT);
+
+    // Store the command in the class member to make sure it is valid
+    // the entire time.
+    current_command_.assign(command.control_command_);
+
+    // Pass self to lambda to make sure that the instance of this class
+    // lives as long as the lambda is held for async connect.
+    auto self(shared_from_this());
+    // Start asynchronous connect. This will return immediatelly.
+    socket_.asyncConnect(socket_path.socket_path_,
+    [this, self, command, handler](const boost::system::error_code& ec) {
+        // We failed to connect so we can't proceed. Simply clean up
+        // and invoke the user callback to signal an error.
+        if (ec) {
+            // This doesn't throw.
+            terminate(ec, handler);
+
+        } else {
+            // Connection successful. Transmit the command to the remote
+            // endpoint asynchronously.
+            doSend(current_command_.c_str(), current_command_.length(),
+                   handler);
+        }
+    });
+}
+
+void
+ClientConnectionImpl::stop() {
+    try {
+        socket_.close();
+
+    } catch (...) {
+        // Suppress errors related to closing a socket. We can't really help
+        // if an error occurred.
+    }
+}
+
+void
+ClientConnectionImpl::doSend(const void* buffer, const size_t length,
+                             const ClientConnection::Handler& handler) {
+    // Pass self to lambda to make sure that the instance of this class
+    // lives as long as the lambda is held for async send.
+    auto self(shared_from_this());
+    // Start asynchronous transmission of the command. This will return
+    // immediatelly.
+    socket_.asyncSend(buffer, length,
+        [this, self, buffer, length, handler]
+        (const boost::system::error_code& ec, size_t bytes_transferred) {
+        // An error has occurred while sending. Close the connection and
+        // signal an error.
+        if (ec) {
+            // This doesn't throw.
+            terminate(ec, handler);
+
+        } else {
+            // If the number of bytes we have managed to send so far is
+            // lower than the amount of data we're trying to send, we
+            // have to schedule another send to deliver the rest of
+            // the data.
+            if (bytes_transferred < length) {
+                doSend(static_cast<const char*>(buffer) + bytes_transferred,
+                       length - bytes_transferred, handler);
+
+            } else {
+                // We have sent all the data. Start receiving a response.
+                doReceive(handler);
+            }
+        }
+    });
+}
+
+void
+ClientConnectionImpl::doReceive(const ClientConnection::Handler& handler) {
+    // Pass self to lambda to make sure that the instance of this class
+    // lives as long as the lambda is held for async receive.
+    auto self(shared_from_this());
+    socket_.asyncReceive(&read_buf_[0], read_buf_.size(),
+        [this, self, handler]
+        (const boost::system::error_code& ec, size_t length) {
+        // An error has occurred while receiving the data. Close the connection
+        // and signal an error.
+        if (ec) {
+            // This doesn't throw.
+            terminate(ec, handler);
+
+        } else {
+            // Lazy initialization of the JSONFeed. The feed will be "parsing"
+            // received JSON stream and will detect when the whole response
+            // has been received.
+            if (!feed_) {
+                feed_.reset(new JSONFeed());
+                feed_->initModel();
+            }
+            // Put everything we have received so far into the feed and process
+            // the data.
+            feed_->postBuffer(&read_buf_[0], length);
+            feed_->poll();
+            // If the feed indicates that only a part of the response has been
+            // received, schedule another receive to get more data.
+            if (feed_->needData()) {
+                doReceive(handler);
+
+            } else {
+                // We have received the entire response, let's call the handler
+                // and indicate success.
+                terminate(ec, handler);
+            }
+        }
+    });
+}
+
+void
+ClientConnectionImpl::terminate(const boost::system::error_code& ec,
+                                const ClientConnection::Handler& handler) {
+    try {
+        stop();
+        current_command_.clear();
+        handler(ec, feed_);
+
+    } catch (...) {
+        // None of these operations should throw. In particular, the handler
+        // should not throw but if it has been misimplemented, we want to make
+        // sure we don't emit any exceptions from here.
+    }
+}
+
+void
+ClientConnectionImpl::timeoutCallback(const ClientConnection::Handler& handler) {
+    // Timeout has occurred. The remote server didn't provide the entire
+    // response within the given time frame. Let's close the connection
+    // and signal the timeout.
+    terminate(boost::asio::error::timed_out, handler);
+}
+
+ClientConnection::ClientConnection(asiolink::IOService& io_service)
+    : impl_(new ClientConnectionImpl(io_service)) {
+}
+
+ClientConnection::~ClientConnection() {
+    stop();
+}
+
+void
+ClientConnection::start(const ClientConnection::SocketPath& socket_path,
+                        const ClientConnection::ControlCommand& command,
+                        const Handler& handler,
+                        const ClientConnection::Timeout& timeout) {
+    impl_->start(socket_path, command, handler, timeout);
+}
+
+void
+ClientConnection::stop() {
+    impl_->stop();
+}
+
+
+} // end of namespace config
+} // end of namespace isc

+ 165 - 0
src/lib/config/client_connection.h

@@ -0,0 +1,165 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef CLIENT_CONNECTION_H
+#define CLIENT_CONNECTION_H
+
+#include <asiolink/io_service.h>
+#include <cc/json_feed.h>
+#include <boost/shared_ptr.hpp>
+#include <functional>
+
+namespace isc {
+namespace config {
+
+class ClientConnectionImpl;
+
+/// @brief Represents client side connection over the unix domain socket.
+///
+/// This class represents a client side connection between the controlling
+/// client and the server exposing control API over a unix domain socket.
+/// In particular, this class is used by the Kea Control Agent to establish
+/// connections with respective Kea services to forward received commands.
+/// As of Kea 1.2 the servers can handle a single connection at the time.
+/// In the future, we're planning to support multiple simulatenous connections.
+/// In this case, each connection will be handled by a unique instance of the
+/// @ref ClientConnection class.
+///
+/// The @ref ClientConnection supports asynchronous connections. A caller
+/// creates an instance of the @ref ClientConnection and calls
+/// @ref ClientConnection::start to start asynchronous communication with
+/// a remote server. The caller provides a pointer to the callback function
+/// (handler) which will be called when the communication with the server
+/// completes, i.e. the command is sent to the server and the response
+/// from the server is received. If an error occurs, the callback is
+/// invoked with an error code indicating a reason for the failure.
+///
+/// The documentation of the @ref ClientConnection::start explains the
+/// sequence of operations performed by this class.
+///
+/// Even though the @ref ClientConnection is asynchronous in nature, it
+/// can also be used in cases requiring synchronous communication. As it
+/// has been already mentioned, the servers in Kea 1.2 do not support
+/// multiple concurrent connections. The following pseudo code demonstrate
+/// how to perform synchronous transaction using this class.
+///
+/// @code
+/// IOService io_service;
+/// ClientConnection conn(io_service);
+/// bool cb_invoked = false;
+/// conn.start(ClientConnection::SocketPath("/tmp/kea.sock"),
+///            ClientConnection::ControlCommand(command),
+///            [this, &cb_invoked](const boost::system::error_code& ec,
+///                   const ConstJSONFeedPtr& feed) {
+///                cb_invoked = true;
+///                if (ec) {
+///                    ... handle error here ...
+///                } else {
+///                    ... use feed to retrieve the response ...
+///                }
+///            }
+/// );
+/// while (!cb_invoked) {
+///     io_service.run_one();
+/// }
+/// @endcode
+///
+class ClientConnection {
+public:
+
+    /// @name Structures used for strong typing.
+    ///
+    //@{
+
+    /// @brief Encapsulates socket path.
+    struct SocketPath {
+        explicit SocketPath(const std::string& socket_path)
+            : socket_path_(socket_path) { }
+
+        std::string socket_path_;
+    };
+
+    /// @brief Encapsulates control command.
+    struct ControlCommand {
+        explicit ControlCommand(const std::string control_command)
+            : control_command_(control_command) { }
+
+        std::string control_command_;
+    };
+
+    /// @brief Encapsulates timeout value.
+    struct Timeout {
+        explicit Timeout(const long timeout)
+            : timeout_(timeout) { }
+
+        long timeout_;
+    };
+
+    //@}
+
+    /// @brief Type of the callback invoked when the communication with
+    /// the server is complete or an error has occurred.
+    typedef std::function<void(const boost::system::error_code& ec,
+                               const ConstJSONFeedPtr& feed)> Handler;
+
+    /// @brief Constructor.
+    ///
+    /// @param io_service Reference to the IO service.
+    explicit ClientConnection(asiolink::IOService& io_service);
+
+    /// @brief Destructor.
+    ///
+    /// Closes current connection.
+    ~ClientConnection();
+
+    /// @brief Starts asynchronous transaction with a remote endpoint.
+    ///
+    /// Starts asynchronous connection with the remote endpoint. If the
+    /// connection is successful, the control command is asynchronously
+    /// sent to the remote endpoint. When the entire command has been sent,
+    /// the response is read asynchronously, possibly in multiple chunks.
+    ///
+    /// The timeout is specified for the entire transaction in milliseconds.
+    /// If the transaction takes longer than the timeout value the connection
+    /// is closed and the callback is called with the error code of
+    /// @c boost::asio::error::timed_out.
+    ///
+    /// In other cases, the callback is called with the error code returned
+    /// by the boost asynchronous operations. If the transaction is successful
+    /// the 'success' status is indicated with the error code. In addition
+    /// the instance of the @ref JSONFeed is returned to the caller. It can
+    /// be used to retrieve parsed response from the server. Note that the
+    /// response may still be malformed, even if no error is signalled in
+    /// the handler. The @ref JSONFeed::toElement will return a parsing
+    /// error if the JSON appears to be malformed.
+    ///
+    /// @param socket_path Path to the socket description that the server
+    /// is bound to.
+    /// @param command Control command to be sent to the server.
+    /// @param handler Pointer to the user suppiled callback function which
+    /// should be invoked when transaction completes or when an error has
+    /// occurred during the transaction.
+    /// @param timeout Connection timeout in milliseconds.
+    void start(const SocketPath& socket_path, const ControlCommand& command,
+               const Handler& handler, const Timeout& timeout = Timeout(10000));
+
+    /// @brief Closes the connection.
+    void stop();
+
+private:
+
+    /// @brief Pointer to the implementation.
+    boost::shared_ptr<ClientConnectionImpl> impl_;
+
+};
+
+/// @brief Type of the pointer to the @ref ClientConnection object.
+typedef boost::shared_ptr<ClientConnection> ClientConnectionPtr;
+
+} // end of namespace config
+} // end of namespace isc
+
+#endif // CLIENT_CONNECTION_H

+ 3 - 1
src/lib/config/tests/Makefile.am

@@ -19,6 +19,7 @@ TESTS =
 if HAVE_GTEST
 TESTS += run_unittests
 run_unittests_SOURCES = module_spec_unittests.cc
+run_unittests_SOURCES += client_connection_unittests.cc
 run_unittests_SOURCES += command_socket_factory_unittests.cc
 run_unittests_SOURCES += config_data_unittests.cc run_unittests.cc
 run_unittests_SOURCES += command_mgr_unittests.cc
@@ -26,7 +27,8 @@ run_unittests_SOURCES += command_mgr_unittests.cc
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS)
 
-run_unittests_LDADD = $(top_builddir)/src/lib/config/libkea-cfgclient.la
+run_unittests_LDADD  = $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la
+run_unittests_LDADD += $(top_builddir)/src/lib/config/libkea-cfgclient.la
 run_unittests_LDADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
 run_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la
 run_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la

+ 177 - 0
src/lib/config/tests/client_connection_unittests.cc

@@ -0,0 +1,177 @@
+// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+#include <asiolink/asio_wrapper.h>
+#include <asiolink/io_service.h>
+#include <asiolink/testutils/test_server_unix_socket.h>
+#include <cc/json_feed.h>
+#include <config/client_connection.h>
+#include <gtest/gtest.h>
+#include <cstdlib>
+#include <sstream>
+#include <string>
+
+using namespace isc::asiolink;
+using namespace isc::config;
+
+namespace {
+
+/// @brief Test unix socket file name.
+const std::string TEST_SOCKET = "test-socket";
+
+/// @brief Test timeout in ms.
+const long TEST_TIMEOUT = 10000;
+
+class ClientConnectionTest : public ::testing::Test {
+public:
+
+    /// @brief Constructor.
+    ///
+    /// Removes unix socket descriptor before the test.
+    ClientConnectionTest() :
+        io_service_(),
+        test_socket_(new test::TestServerUnixSocket(io_service_,
+                                                    unixSocketFilePath())) {
+        removeUnixSocketFile();
+    }
+
+    /// @brief Destructor.
+    ///
+    /// Removes unix socket descriptor after the test.
+    virtual ~ClientConnectionTest() {
+        removeUnixSocketFile();
+    }
+
+    /// @brief Returns socket file path.
+    ///
+    /// If the KEA_SOCKET_TEST_DIR environment variable is specified, the
+    /// socket file is created in the location pointed to by this variable.
+    /// Otherwise, it is created in the build directory.
+    static std::string unixSocketFilePath() {
+        std::ostringstream s;
+        const char* env = getenv("KEA_SOCKET_TEST_DIR");
+        if (env) {
+            s << std::string(env);
+        } else {
+            s << TEST_DATA_BUILDDIR;
+        }
+
+        s << "/" << TEST_SOCKET;
+        return (s.str());
+    }
+
+    /// @brief Removes unix socket descriptor.
+    void removeUnixSocketFile() {
+        static_cast<void>(remove(unixSocketFilePath().c_str()));
+    }
+
+    /// @brief IO service used by the tests.
+    IOService io_service_;
+
+    /// @brief Server side unix socket used in these tests.
+    test::TestServerUnixSocketPtr test_socket_;
+};
+
+// Tests successful transaction: connect, send command and receive a
+// response.
+TEST_F(ClientConnectionTest, success) {
+    // Start timer protecting against test timeouts.
+    test_socket_->startTimer(TEST_TIMEOUT);
+
+    // Start the server.
+    test_socket_->bindServerSocket();
+    test_socket_->generateCustomResponse(2048);
+
+    // Create some valid command.
+    std::string command = "{ \"command\": \"list-commands\" }";
+
+    ClientConnection conn(io_service_);
+
+    // This boolean value will indicate when the callback function is invoked
+    // at the end of the transaction (whether it is successful or unsuccessful).
+    bool handler_invoked = false;
+    conn.start(ClientConnection::SocketPath(unixSocketFilePath()),
+               ClientConnection::ControlCommand(command),
+        [this, &handler_invoked](const boost::system::error_code& ec,
+                                 const ConstJSONFeedPtr& feed) {
+        // Indicate that the handler has been called to break from the
+        // while loop below.
+        handler_invoked = true;
+        // The ec should contain no error.
+        ASSERT_FALSE(ec);
+        // The JSONFeed should be present and it should contain a valid
+        // response.
+        ASSERT_TRUE(feed);
+        EXPECT_TRUE(feed->feedOk()) << feed->getErrorMessage();
+    });
+    // Run the connection.
+    while (!handler_invoked && !test_socket_->isStopped()) {
+        io_service_.run_one();
+    }
+}
+
+// This test checks that a timeout is signalled when the communication
+// takes too long.
+TEST_F(ClientConnectionTest, timeout) {
+    // The server will return only partial JSON response (lacking closing
+    // brace). The client will wait for closing brace and eventually the
+    // connection should time out.
+    test_socket_.reset(new test::TestServerUnixSocket(io_service_,
+                                                      unixSocketFilePath(),
+                                                      "{ \"command\": \"foo\""));
+    test_socket_->startTimer(TEST_TIMEOUT);
+
+    // Start the server.
+    test_socket_->bindServerSocket();
+
+    // Command to be sent to the server.
+    std::string command = "{ \"command\": \"list-commands\" }";
+
+    ClientConnection conn(io_service_);
+
+    // This boolean value will be set to true when the callback is invoked.
+    bool handler_invoked = false;
+    conn.start(ClientConnection::SocketPath(unixSocketFilePath()),
+              ClientConnection::ControlCommand(command),
+    [this, &handler_invoked](const boost::system::error_code& ec,
+                             const ConstJSONFeedPtr& feed) {
+        // Indicate that the callback has been invoked to break the loop
+        // below.
+        handler_invoked = true;
+        ASSERT_TRUE(ec);
+        EXPECT_TRUE(ec.value() == boost::asio::error::timed_out);
+    }, ClientConnection::Timeout(1000));
+
+    while (!handler_invoked && !test_socket_->isStopped()) {
+        io_service_.run_one();
+    }
+}
+
+// This test checks that an error is returned when the client is unable
+// to connect to the server.
+TEST_F(ClientConnectionTest, connectionError) {
+    // Create the new connection but do not bind the server socket.
+    // The connection should be refused and an error returned.
+    ClientConnection conn(io_service_);
+
+    std::string command = "{ \"command\": \"list-commands\" }";
+
+    bool handler_invoked = false;
+    conn.start(ClientConnection::SocketPath(unixSocketFilePath()),
+               ClientConnection::ControlCommand(command),
+    [this, &handler_invoked](const boost::system::error_code& ec,
+           const ConstJSONFeedPtr& feed) {
+        handler_invoked = true;
+        ASSERT_TRUE(ec);
+    });
+
+    while (!handler_invoked && !test_socket_->isStopped()) {
+        io_service_.run_one();
+    }
+}
+
+} // end of anonymous namespace