changeset 102:4777f7e18bf2

Irccd: several improvements in servers, #385 Make server state a class to manage data cleanly where they are needed and to understand better the transitions. Add a ping timer that defaults to 5 minutes before marking the server as disconnected.
author David Demelier <markand@malikania.fr>
date Tue, 26 Apr 2016 13:19:35 +0200
parents 113d909fdfe1
children 04d672ab41a4
files doc/html/guide/04-irccd/01-config.md doc/man/irccd.conf.5.in extern/libircclient/include/libirc_events.h extern/libircclient/src/libircclient.c lib/irccd/CMakeSources.cmake lib/irccd/config.cpp lib/irccd/server-state-connected.cpp lib/irccd/server-state-connected.hpp lib/irccd/server-state-connecting.cpp lib/irccd/server-state-connecting.hpp lib/irccd/server-state-disconnected.cpp lib/irccd/server-state-disconnected.hpp lib/irccd/server-state.cpp lib/irccd/server-state.hpp lib/irccd/server.cpp lib/irccd/server.hpp
diffstat 16 files changed, 501 insertions(+), 273 deletions(-) [+]
line wrap: on
line diff
--- a/doc/html/guide/04-irccd/01-config.md	Mon Apr 25 21:16:47 2016 +0200
+++ b/doc/html/guide/04-irccd/01-config.md	Tue Apr 26 13:19:35 2016 +0200
@@ -122,7 +122,8 @@
   - **ssl-verify**: (bool) verify the SSL certificates (Optional, default: true),
   - **reconnect**: (bool) enable reconnection after failure (Optional, default: true),
   - **reconnect-tries**: (int) number of tries before giving up. A value of -1 means indefinitely (Optional, default: -1),
-  - **reconnect-timeout**: (int) number of seconds to wait before retrying (Optional, default: 30).
+  - **reconnect-timeout**: (int) number of seconds to wait before retrying (Optional, default: 30),
+  - **ping-timeout** (int) number of seconds before ping timeout (Optional, default: 300).
 
 <div class="alert alert-info" role="alert">
 **Note:** if a channel requires a password, add it after a colon (e.g. "#channel:password").
--- a/doc/man/irccd.conf.5.in	Mon Apr 25 21:16:47 2016 +0200
+++ b/doc/man/irccd.conf.5.in	Tue Apr 26 13:19:35 2016 +0200
@@ -133,6 +133,8 @@
 (int) number of tries before giving up. A value of -1 means indefinitely (Optional, default: -1).
 .It reconnect-timeout
 (int) number of seconds to wait before retrying (Optional, default: 30).
+.It ping-timeout
+(int) number of seconds before ping timeout (Optional, default: 300).
 .El
 .\" PLUGINS
 .Ss plugins
--- a/extern/libircclient/include/libirc_events.h	Mon Apr 25 21:16:47 2016 +0200
+++ b/extern/libircclient/include/libirc_events.h	Tue Apr 26 13:19:35 2016 +0200
@@ -158,6 +158,11 @@
 	irc_event_callback_t	event_connect;
 
 	/*!
+	 * The "ping" event is triggered when the client receives PING requests.
+	 */
+	irc_event_callback_t	event_ping;
+
+	/*!
 	 * The "nick" event is triggered when the client receives a NICK message,
 	 * meaning that someone (including you) on a channel with the client has 
 	 * changed their nickname. 
--- a/extern/libircclient/src/libircclient.c	Mon Apr 25 21:16:47 2016 +0200
+++ b/extern/libircclient/src/libircclient.c	Tue Apr 26 13:19:35 2016 +0200
@@ -593,7 +593,11 @@
 	// Handle PING/PONG
 	if ( command && !strncmp (command, "PING", buf_end - command) && params[0] )
 	{
-		irc_send_raw (session, "PONG %s", params[0]);
+		if (session->callbacks.event_ping)
+			(*session->callbacks.event_ping)(session, "PING", prefix, params, paramindex);
+		else
+			irc_send_raw (session, "PONG %s", params[0]);
+
 		return;
 	}
 
--- a/lib/irccd/CMakeSources.cmake	Mon Apr 25 21:16:47 2016 +0200
+++ b/lib/irccd/CMakeSources.cmake	Tue Apr 26 13:19:35 2016 +0200
@@ -58,6 +58,9 @@
 	${CMAKE_CURRENT_LIST_DIR}/server.hpp
 	${CMAKE_CURRENT_LIST_DIR}/server-private.hpp
 	${CMAKE_CURRENT_LIST_DIR}/server-state.hpp
+	${CMAKE_CURRENT_LIST_DIR}/server-state-connected.hpp
+	${CMAKE_CURRENT_LIST_DIR}/server-state-connecting.hpp
+	${CMAKE_CURRENT_LIST_DIR}/server-state-disconnected.hpp
 	${CMAKE_CURRENT_LIST_DIR}/sockets.hpp
 	${CMAKE_CURRENT_LIST_DIR}/system.hpp
 	${CMAKE_CURRENT_LIST_DIR}/timer.hpp
@@ -121,7 +124,9 @@
 	${CMAKE_CURRENT_LIST_DIR}/plugin.cpp
 	${CMAKE_CURRENT_LIST_DIR}/rule.cpp
 	${CMAKE_CURRENT_LIST_DIR}/server.cpp
-	${CMAKE_CURRENT_LIST_DIR}/server-state.cpp
+	${CMAKE_CURRENT_LIST_DIR}/server-state-connected.cpp
+	${CMAKE_CURRENT_LIST_DIR}/server-state-connecting.cpp
+	${CMAKE_CURRENT_LIST_DIR}/server-state-disconnected.cpp
 	${CMAKE_CURRENT_LIST_DIR}/sockets.cpp
 	${CMAKE_CURRENT_LIST_DIR}/system.cpp
 	${CMAKE_CURRENT_LIST_DIR}/timer.cpp
--- a/lib/irccd/config.cpp	Mon Apr 25 21:16:47 2016 +0200
+++ b/lib/irccd/config.cpp	Tue Apr 26 13:19:35 2016 +0200
@@ -303,6 +303,8 @@
 			settings.recotries = std::stoi(it->value());
 		if ((it = sc.find("reconnect-timeout")) != sc.end())
 			settings.recotimeout = std::stoi(it->value());
+		if ((it = sc.find("ping-timeout")) != sc.end())
+			settings.ping_timeout = std::stoi(it->value());
 	} catch (const std::exception &) {
 		throw std::invalid_argument("server " + info.name + ": invalid number for " + it->key() + ": " + it->value());
 	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server-state-connected.cpp	Tue Apr 26 13:19:35 2016 +0200
@@ -0,0 +1,58 @@
+/*
+ * server-state-connected.cpp -- connected state
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "logger.hpp"
+#include "server-state-connected.hpp"
+#include "server-state-disconnected.hpp"
+#include "server-private.hpp"
+
+namespace irccd {
+
+namespace state {
+
+void Connected::prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd)
+{
+	const ServerInfo &info = server.info();
+	const ServerSettings &settings = server.settings();
+
+	if (!irc_is_connected(server.session())) {
+		log::warning() << "server " << info.name << ": disconnected" << std::endl;
+
+		if (settings.recotimeout > 0) {
+			log::warning() << "server " << info.name << ": retrying in "
+				       << settings.recotimeout << " seconds" << std::endl;
+		}
+
+		server.next(std::make_unique<state::Disconnected>());
+	} else if (server.pingTimer().elapsed() >= settings.ping_timeout * 1000) {
+		log::warning() << "server " << info.name << ": ping timeout after "
+			       << (server.pingTimer().elapsed() / 1000) << " seconds" << std::endl;
+		server.next(std::make_unique<state::Disconnected>());
+	} else {
+		irc_add_select_descriptors(server.session(), &setinput, &setoutput, reinterpret_cast<int *>(&maxfd));
+	}
+}
+
+std::string Connected::ident() const
+{
+	return "Connected";
+}
+
+} // !state
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server-state-connected.hpp	Tue Apr 26 13:19:35 2016 +0200
@@ -0,0 +1,53 @@
+/*
+ * server-state-connected.hpp -- connected state
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_SERVER_STATE_CONNECTED_HPP
+#define IRCCD_SERVER_STATE_CONNECTED_HPP
+
+/**
+ * \file server-state-connected.hpp
+ * \brief Connected state.
+ */
+
+#include "server-state.hpp"
+
+namespace irccd {
+
+namespace state {
+
+/**
+ * \brief Connected state.
+ */
+class Connected : public ServerState {
+public:
+	/**
+	 * \copydoc ServerState::prepare
+	 */
+	void prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd) override;
+
+	/**
+	 * \copydoc ServerState::ident
+	 */
+	std::string ident() const override;
+};
+
+} // !state
+
+} // !irccd
+
+#endif // !IRCCD_SERVER_STATE_CONNECTED_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server-state-connecting.cpp	Tue Apr 26 13:19:35 2016 +0200
@@ -0,0 +1,133 @@
+/*
+ * server-state-connecting.cpp -- connecting state
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "server-state-connecting.hpp"
+#include "server-state-connected.hpp"
+#include "server-state-disconnected.hpp"
+#include "server-private.hpp"
+#include "sysconfig.hpp"
+
+#if !defined(IRCCD_SYSTEM_WINDOWS)
+#  include <sys/types.h>
+#  include <netinet/in.h>
+#  include <arpa/nameser.h>
+#  include <resolv.h>
+#endif
+
+namespace irccd {
+
+namespace state {
+
+namespace {
+
+bool connect(Server &server)
+{
+	const ServerInfo &info = server.info();
+	const ServerIdentity &identity = server.identity();
+	const char *password = info.password.empty() ? nullptr : info.password.c_str();
+	std::string host = info.host;
+	int code;
+
+	/* libircclient requires # for SSL connection */
+#if defined(WITH_SSL)
+	if (info.flags & ServerInfo::Ssl)
+		host.insert(0, 1, '#');
+	if (!(info.flags & ServerInfo::SslVerify))
+		irc_option_set(server.session(), LIBIRC_OPTION_SSL_NO_VERIFY);
+#endif
+
+	if (info.flags & ServerInfo::Ipv6) {
+		code = irc_connect6(server.session(), host.c_str(), info.port, password,
+				    identity.nickname.c_str(),
+				    identity.username.c_str(),
+				    identity.realname.c_str());
+	} else {
+		code = irc_connect(server.session(), host.c_str(), info.port, password,
+				   identity.nickname.c_str(),
+				   identity.username.c_str(),
+				   identity.realname.c_str());
+	}
+
+	return code == 0;
+}
+
+} // !namespace
+
+void Connecting::prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd)
+{
+	/*
+	 * The connect function will either fail if the hostname wasn't resolved
+	 * or if any of the internal functions fail.
+	 *
+	 * It returns success if the connection was successful but it does not
+	 * mean that connection is established.
+	 *
+	 * Because this function will be called repeatidly, the connection was started and we're still not
+	 * connected in the specified timeout time, we mark the server as disconnected.
+	 *
+	 * Otherwise, the libircclient event_connect will change the state.
+	 */
+	const ServerInfo &info = server.info();
+
+	if (m_started) {
+		const ServerSettings &settings = server.settings();
+
+		if (m_timer.elapsed() > static_cast<unsigned>(settings.recotimeout * 1000)) {
+			log::warning() << "server " << info.name << ": timeout while connecting" << std::endl;
+			server.next(std::make_unique<state::Disconnected>());
+		} else if (!irc_is_connected(server.session())) {
+			log::warning() << "server " << info.name << ": error while connecting: ";
+			log::warning() << irc_strerror(irc_errno(server.session())) << std::endl;
+
+			if (settings.recotries != 0)
+				log::warning() << "server " << info.name << ": retrying in " << settings.recotimeout << " seconds" << std::endl;
+
+			server.next(std::make_unique<state::Disconnected>());
+		} else {
+			irc_add_select_descriptors(server.session(), &setinput, &setoutput, reinterpret_cast<int *>(&maxfd));
+		}
+	} else {
+		/*
+		 * This is needed if irccd is started before DHCP or if
+		 * DNS cache is outdated.
+		 *
+		 * For more information see bug #190.
+		 */
+#if !defined(IRCCD_SYSTEM_WINDOWS)
+		(void)res_init();
+#endif
+		log::info() << "server " << info.name << ": trying to connect to " << info.host << ", port " << info.port << std::endl;
+
+		if (!connect(server)) {
+			log::warning() << "server " << info.name << ": disconnected while connecting: ";
+			log::warning() << irc_strerror(irc_errno(server.session())) << std::endl;
+			server.next(std::make_unique<state::Disconnected>());
+		} else {
+			m_started = true;
+		}
+	}
+}
+
+std::string Connecting::ident() const
+{
+	return "Connecting";
+}
+
+} // !state
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server-state-connecting.hpp	Tue Apr 26 13:19:35 2016 +0200
@@ -0,0 +1,58 @@
+/*
+ * server-state-connecting.hpp -- connecting state
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_SERVER_STATE_CONNECTING_HPP
+#define IRCCD_SERVER_STATE_CONNECTING_HPP
+
+/**
+ * \file server-state-connecting.hpp
+ * \brief Connecting state.
+ */
+
+#include "elapsed-timer.hpp"
+#include "server-state.hpp"
+
+namespace irccd {
+
+namespace state {
+
+/**
+ * \brief Connecting state.
+ */
+class Connecting : public ServerState {
+private:
+	bool m_started{false};
+	ElapsedTimer m_timer;
+
+public:
+	/**
+	 * \copydoc ServerState::prepare
+	 */
+	void prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd) override;
+
+	/**
+	 * \copydoc ServerState::ident
+	 */
+	std::string ident() const override;
+};
+
+} // !state
+
+} // !irccd
+
+#endif // !IRCCD_SERVER_STATE_CONNECTING_HPP
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server-state-disconnected.cpp	Tue Apr 26 13:19:35 2016 +0200
@@ -0,0 +1,56 @@
+/*
+ * server-state-disconnected.cpp -- disconnected state
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "logger.hpp"
+#include "server-state-connecting.hpp"
+#include "server-state-disconnected.hpp"
+#include "server-private.hpp"
+
+namespace irccd {
+
+namespace state {
+
+void Disconnected::prepare(Server &server, fd_set &, fd_set &, net::Handle &)
+{
+	const ServerInfo &info = server.info();
+	ServerSettings &settings = server.settings();
+
+	if (settings.recotries == 0) {
+		log::warning() << "server " << info.name << ": reconnection disabled, skipping" << std::endl;
+		server.onDie();
+	} else if (settings.recotries > 0 && settings.recocurrent > settings.recotries) {
+		log::warning() << "server " << info.name << ": giving up" << std::endl;
+		server.onDie();
+	} else {
+		if (m_timer.elapsed() > static_cast<unsigned>(settings.recotimeout * 1000)) {
+			irc_disconnect(server.session());
+
+			settings.recocurrent ++;
+			server.next(std::make_unique<state::Connecting>());
+		}
+	}
+}
+
+std::string Disconnected::ident() const
+{
+	return "Disconnected";
+}
+
+} // !state
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/server-state-disconnected.hpp	Tue Apr 26 13:19:35 2016 +0200
@@ -0,0 +1,57 @@
+/*
+ * server-state-disconnected.hpp -- disconnected state
+ *
+ * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef IRCCD_SERVER_STATE_DISCONNECTED_HPP
+#define IRCCD_SERVER_STATE_DISCONNECTED_HPP
+
+/**
+ * \file server-state-disconnected.hpp
+ * \brief Connecting state.
+ */
+
+#include "elapsed-timer.hpp"
+#include "server-state.hpp"
+
+namespace irccd {
+
+namespace state {
+
+/**
+ * \brief Disconnected state.
+ */
+class Disconnected : public ServerState {
+private:
+	ElapsedTimer m_timer;
+
+public:
+	/**
+	 * \copydoc ServerState::prepare
+	 */
+	void prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd) override;
+
+	/**
+	 * \copydoc ServerState::ident
+	 */
+	std::string ident() const override;
+};
+
+} // !state
+
+} // !irccd
+
+#endif // !IRCCD_SERVER_STATE_DISCONNECTED_HPP
--- a/lib/irccd/server-state.cpp	Mon Apr 25 21:16:47 2016 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +0,0 @@
-/*
- * server-state.cpp -- server current state
- *
- * Copyright (c) 2013-2016 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <cassert>
-
-#include "sysconfig.hpp"
-
-#if !defined(_WIN32)
-#  include <sys/types.h>
-#  include <netinet/in.h>
-#  include <arpa/nameser.h>
-#  include <resolv.h>
-#endif
-
-#include "server-state.hpp"
-#include "server-private.hpp"
-
-namespace irccd {
-
-bool ServerState::connect(Server &server)
-{
-	const ServerInfo &info = server.info();
-	const ServerIdentity &identity = server.identity();
-	const char *password = info.password.empty() ? nullptr : info.password.c_str();
-	std::string host = info.host;
-	int code;
-
-	/* libircclient requires # for SSL connection */
-#if defined(WITH_SSL)
-	if (info.flags & ServerInfo::Ssl)
-		host.insert(0, 1, '#');
-	if (!(info.flags & ServerInfo::SslVerify))
-		irc_option_set(server.session(), LIBIRC_OPTION_SSL_NO_VERIFY);
-#endif
-
-	if (info.flags & ServerInfo::Ipv6) {
-		code = irc_connect6(server.session(), host.c_str(), info.port, password,
-				    identity.nickname.c_str(),
-				    identity.username.c_str(),
-				    identity.realname.c_str());
-	} else {
-		code = irc_connect(server.session(), host.c_str(), info.port, password,
-				   identity.nickname.c_str(),
-				   identity.username.c_str(),
-				   identity.realname.c_str());
-	}
-
-	return code == 0;
-}
-
-void ServerState::prepareConnected(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd)
-{
-	if (!irc_is_connected(server.session())) {
-		const ServerSettings &settings = server.settings();
-
-		log::warning() << "server " << server.info().name << ": disconnected" << std::endl;
-
-		if (settings.recotimeout > 0) {
-			log::warning() << "server " << server.info().name << ": retrying in "
-					  << settings.recotimeout << " seconds" << std::endl;
-		}
-
-		server.next(ServerState::Disconnected);
-	} else {
-		irc_add_select_descriptors(server.session(), &setinput, &setoutput, reinterpret_cast<int *>(&maxfd));
-	}
-}
-
-void ServerState::prepareConnecting(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd)
-{
-	/*
-	 * The connect function will either fail if the hostname wasn't resolved
-	 * or if any of the internal functions fail.
-	 *
-	 * It returns success if the connection was successful but it does not
-	 * mean that connection is established.
-	 *
-	 * Because this function will be called repeatidly, the connection was started and we're still not
-	 * connected in the specified timeout time, we mark the server as disconnected.
-	 *
-	 * Otherwise, the libircclient event_connect will change the state.
-	 */
-	const ServerInfo &info = server.info();
-
-	if (m_started) {
-		const ServerSettings &settings = server.settings();
-
-		if (m_timer.elapsed() > static_cast<unsigned>(settings.recotimeout * 1000)) {
-			log::warning() << "server " << info.name << ": timeout while connecting" << std::endl;
-			server.next(ServerState::Disconnected);
-		} else if (!irc_is_connected(server.session())) {
-			log::warning() << "server " << info.name << ": error while connecting: ";
-			log::warning() << irc_strerror(irc_errno(server.session())) << std::endl;
-
-			if (settings.recotries != 0)
-				log::warning() << "server " << info.name << ": retrying in " << settings.recotimeout << " seconds" << std::endl;
-
-			server.next(ServerState::Disconnected);
-		} else {
-			irc_add_select_descriptors(server.session(), &setinput, &setoutput, reinterpret_cast<int *>(&maxfd));
-		}
-	} else {
-		/*
-		 * This is needed if irccd is started before DHCP or if
-		 * DNS cache is outdated.
-		 *
-		 * For more information see bug #190.
-		 */
-#if !defined(_WIN32)
-		(void)res_init();
-#endif
-		log::info() << "server " << info.name << ": trying to connect to " << info.host << ", port " << info.port << std::endl;
-
-		if (!connect(server)) {
-			log::warning() << "server " << info.name << ": disconnected while connecting: ";
-			log::warning() << irc_strerror(irc_errno(server.session())) << std::endl;
-			server.next(ServerState::Disconnected);
-		} else {
-			m_started = true;
-		}
-	}
-}
-
-void ServerState::prepareDisconnected(Server &server, fd_set &, fd_set &, net::Handle &)
-{
-	const ServerInfo &info = server.info();
-	ServerSettings &settings = server.settings();
-
-	if (settings.recotries == 0) {
-		log::warning() << "server " << info.name << ": reconnection disabled, skipping" << std::endl;
-		server.onDie();
-	} else if (settings.recotries > 0 && settings.recocurrent > settings.recotries) {
-		log::warning() << "server " << info.name << ": giving up" << std::endl;
-		server.onDie();
-	} else {
-		if (m_timer.elapsed() > static_cast<unsigned>(settings.recotimeout * 1000)) {
-			irc_disconnect(server.session());
-
-			settings.recocurrent ++;
-			server.next(ServerState::Connecting);
-		}
-	}
-}
-
-ServerState::ServerState(Type type)
-	: m_type(type)
-{
-	assert(static_cast<int>(m_type) >= static_cast<int>(ServerState::Undefined));
-	assert(static_cast<int>(m_type) <= static_cast<int>(ServerState::Disconnected));
-}
-
-void ServerState::prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd)
-{
-	switch (m_type) {
-	case Connecting:
-		prepareConnecting(server, setinput, setoutput, maxfd);
-		break;
-	case Connected:
-		prepareConnected(server, setinput, setoutput, maxfd);
-		break;
-	case Disconnected:
-		prepareDisconnected(server, setinput, setoutput, maxfd);
-		break;
-	default:
-		break;
-	}
-}
-
-} // !irccd
--- a/lib/irccd/server-state.hpp	Mon Apr 25 21:16:47 2016 +0200
+++ b/lib/irccd/server-state.hpp	Tue Apr 26 13:19:35 2016 +0200
@@ -39,39 +39,14 @@
 class ServerState {
 public:
 	/**
-	 * \enum Type
-	 * \brief Server state
+	 * Default constructor.
 	 */
-	enum Type {
-		Undefined,	//!< Not defined yet
-		Connecting,	//!< Connecting to the server
-		Connected,	//!< Connected and running
-		Disconnected,	//!< Disconnected and waiting before retrying
-	};
-
-private:
-	Type m_type;
-
-	/* For ServerState::Connecting */
-	bool m_started{false};
-	ElapsedTimer m_timer;
+	ServerState() = default;
 
-	/* Private helpers */
-	bool connect(Server &server);
-
-	/* Different preparation */
-	void prepareConnected(Server &, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd);
-	void prepareConnecting(Server &, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd);
-	void prepareDisconnected(Server &, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd);
-
-public:
 	/**
-	 * Create the server state.
-	 *
-	 * \pre type must be valid
-	 * \param type the type
+	 * Virtual default destructor.
 	 */
-	ServerState(Type type);
+	virtual ~ServerState() = default;
 
 	/**
 	 * Prepare the state.
@@ -81,17 +56,14 @@
 	 * \param setoutput the write set
 	 * \param maxfd the maximum fd
 	 */
-	void prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd);
+	virtual void prepare(Server &server, fd_set &setinput, fd_set &setoutput, net::Handle &maxfd) = 0;
 
 	/**
-	 * Get the state type.
+	 * Return the state identifier, only for information purposes.
 	 *
-	 * \return the type
+	 * \return the identifier
 	 */
-	inline Type type() const noexcept
-	{
-		return m_type;
-	}
+	virtual std::string ident() const = 0;
 };
 
 } // !irccd
--- a/lib/irccd/server.cpp	Mon Apr 25 21:16:47 2016 +0200
+++ b/lib/irccd/server.cpp	Tue Apr 26 13:19:35 2016 +0200
@@ -25,6 +25,8 @@
 
 #include "logger.hpp"
 #include "server-private.hpp"
+#include "server-state-connected.hpp"
+#include "server-state-connecting.hpp"
 #include "util.hpp"
 
 namespace irccd {
@@ -87,8 +89,11 @@
 	/* Reset the number of tried reconnection. */
 	m_settings.recocurrent = 0;
 
+	/* Reset the timer. */
+	m_ping_timer.reset();
+
 	/* Don't forget to change state and notify. */
-	next(ServerState::Connected);
+	next(std::make_unique<state::Connected>());
 	onConnect();
 
 	/* Auto join listed channels. */
@@ -286,6 +291,15 @@
 	onPart(strify(orig), strify(params[0]), strify(params[1]));
 }
 
+void Server::handlePing(const char *, const char **params) noexcept
+{
+	/* Reset the timer to detect disconnection. */
+	m_ping_timer.reset();
+
+	/* Don't forget to respond */
+	send(params[0]);
+}
+
 void Server::handleQuery(const char *orig, const char **params) noexcept
 {
 	onQuery(strify(orig), strify(params[1]));
@@ -311,8 +325,7 @@
 	, m_settings(std::move(settings))
 	, m_identity(std::move(identity))
 	, m_session(std::make_unique<Session>())
-	, m_state(ServerState::Connecting)
-	, m_next(ServerState::Undefined)
+	, m_state(std::make_unique<state::Connecting>())
 {
 	irc_callbacks_t callbacks;
 
@@ -364,6 +377,9 @@
 	callbacks.event_part = [] (irc_session_t *session, const char *, const char *orig, const char **params, unsigned) {
 		static_cast<Server *>(irc_get_ctx(session))->handlePart(orig, params);
 	};
+	callbacks.event_ping = [] (irc_session_t *session, const char *, const char *orig, const char **params, unsigned) {
+		static_cast<Server *>(irc_get_ctx(session))->handlePing(orig, params);
+	};
 	callbacks.event_privmsg = [] (irc_session_t *session, const char *, const char *orig, const char **params, unsigned) {
 		static_cast<Server *>(irc_get_ctx(session))->handleQuery(orig, params);
 	};
@@ -388,25 +404,12 @@
 
 void Server::update() noexcept
 {
-	if (m_next.type() != ServerState::Undefined) {
-		log::debug() << "server " << m_info.name << ": switching to state ";
+	if (m_state_next) {
+		log::debug() << "server " << m_info.name << ": switching state "
+			     << m_state->ident() << " -> " << m_state_next->ident() << std::endl;
 
-		switch (m_next.type()) {
-		case ServerState::Connecting:
-			log::debug() << "\"Connecting\"" << std::endl;
-			break;
-		case ServerState::Connected:
-			log::debug() << "\"Connected\"" << std::endl;
-			break;
-		case ServerState::Disconnected:
-			log::debug() << "\"Disconnected\"" << std::endl;
-			break;
-		default:
-			break;
-		}
-
-		m_state = std::move(m_next);
-		m_next = ServerState::Undefined;
+		m_state = std::move(m_state_next);
+		m_state_next = nullptr;
 	}
 }
 
@@ -421,14 +424,14 @@
 void Server::reconnect() noexcept
 {
 	irc_disconnect(*m_session);
-	next(ServerState::Type::Connecting);
+	next(std::make_unique<state::Connecting>());
 }
 
 void Server::sync(fd_set &setinput, fd_set &setoutput) noexcept
 {
 	/*
-	 * 1. Send maximum of command possible if available for write */
-	/*
+	 * 1. Send maximum of command possible if available for write
+	 *
 	 * Break on the first failure to avoid changing the order of the
 	 * commands if any of them fails.
 	 */
--- a/lib/irccd/server.hpp	Mon Apr 25 21:16:47 2016 +0200
+++ b/lib/irccd/server.hpp	Tue Apr 26 13:19:35 2016 +0200
@@ -35,6 +35,7 @@
 #include <utility>
 #include <vector>
 
+#include "elapsed-timer.hpp"
 #include "logger.hpp"
 #include "server-state.hpp"
 #include "signals.hpp"
@@ -139,6 +140,8 @@
 	std::uint16_t recotimeout{30};		//!< Number of seconds to wait before trying to connect
 	std::uint8_t flags{0};			//!< Optional flags
 
+	std::uint16_t ping_timeout{300};	//!< Ping timeout in milliseconds
+
 	/* Private */
 	std::int8_t recocurrent{1};		//!< number of tries tested
 };
@@ -399,10 +402,17 @@
 	ServerSettings m_settings;
 	ServerIdentity m_identity;
 	SessionPtr m_session;
-	ServerState m_state;
-	ServerState m_next;
 	Queue m_queue;
 
+	/* States */
+	std::unique_ptr<ServerState> m_state;
+	std::unique_ptr<ServerState> m_state_next;
+
+	/*
+	 * Detect timeout from server.
+	 */
+	ElapsedTimer m_ping_timer;
+
 	/*
 	 * The names map is being built by a successive call to handleNumeric so we need to store a temporary
 	 * map by channels to list of names. Then, when we receive the end of names listing, we remove the
@@ -433,6 +443,7 @@
 	void handleNotice(const char *, const char **) noexcept;
 	void handleNumeric(unsigned int, const char **, unsigned int) noexcept;
 	void handlePart(const char *, const char **) noexcept;
+	void handlePing(const char *, const char **) noexcept;
 	void handleQuery(const char *, const char **) noexcept;
 	void handleTopic(const char *, const char **) noexcept;
 
@@ -460,18 +471,23 @@
 	virtual ~Server();
 
 	/**
-	 * Set the next state to be used. This function is thread safe because
-	 * the server manager may set the next state to the current state.
-	 *
-	 * If the server is installed into the ServerManager, it is called
-	 * automatically.
+	 * Set the next state, it is not changed immediately but on next iteration.
 	 *
-	 * \param type the new state type
-	 * \warning Not thread-safe
+	 * \param state the new state
 	 */
-	inline void next(ServerState::Type type)
+	inline void next(std::unique_ptr<ServerState> state)
 	{
-		m_next = ServerState(type);
+		m_state_next = std::move(state);
+	}
+
+	/**
+	 * Get the ping timer.
+	 *
+	 * \return the ping timer
+	 */
+	inline ElapsedTimer &pingTimer() noexcept
+	{
+		return m_ping_timer;
 	}
 
 	/**
@@ -513,16 +529,13 @@
 	void flush() noexcept;
 
 	/**
-	 * Prepare the IRC Session to the socket.
-	 *
-	 * If the server is installed into the ServerManager, it is called
-	 * automatically.
+	 * Prepare the IRC session.
 	 *
 	 * \warning Not thread-safe
 	 */
 	inline void prepare(fd_set &setinput, fd_set &setoutput, net::Handle &maxfd) noexcept
 	{
-		m_state.prepare(*this, setinput, setoutput, maxfd);
+		m_state->prepare(*this, setinput, setoutput, maxfd);
 	}
 
 	/**
@@ -602,16 +615,6 @@
 	}
 
 	/**
-	 * Get the current state identifier. Should not be used by user code.
-	 *
-	 * \note Thread-safe but the state may change just after the call
-	 */
-	inline ServerState::Type type() const noexcept
-	{
-		return m_state.type();
-	}
-
-	/**
 	 * Get the private session.
 	 *
 	 * \return the session