changeset 128:2d8343b86e2e

Irccd: implement PluginService, #499
author David Demelier <markand@malikania.fr>
date Wed, 11 May 2016 13:58:42 +0200
parents 77f950caab35
children 49d1a5eeef6f
files irccd/main.cpp lib/irccd/CMakeSources.cmake lib/irccd/cmd-plugin-info.cpp lib/irccd/cmd-plugin-list.cpp lib/irccd/cmd-plugin-load.cpp lib/irccd/cmd-plugin-reload.cpp lib/irccd/cmd-plugin-unload.cpp lib/irccd/irccd.cpp lib/irccd/irccd.hpp lib/irccd/js-plugin.cpp lib/irccd/server-event.cpp lib/irccd/service-plugin.cpp lib/irccd/service-plugin.hpp lib/irccd/service-server.cpp tests/js-timer/main.cpp
diffstat 15 files changed, 365 insertions(+), 294 deletions(-) [+]
line wrap: on
line diff
--- a/irccd/main.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/irccd/main.cpp	Wed May 11 13:58:42 2016 +0200
@@ -37,6 +37,7 @@
 #include <irccd/logger.hpp>
 #include <irccd/options.hpp>
 #include <irccd/path.hpp>
+#include <irccd/service-plugin.hpp>
 #include <irccd/service-rule.hpp>
 #include <irccd/service-server.hpp>
 #include <irccd/service-transport.hpp>
@@ -245,7 +246,7 @@
 
 	// [plugin] section.
 	for (const auto &plugin : config.loadPlugins()) {
-		instance->addPlugin(plugin);
+		instance->pluginService().add(plugin);
 	}
 }
 
--- a/lib/irccd/CMakeSources.cmake	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/CMakeSources.cmake	Wed May 11 13:58:42 2016 +0200
@@ -1,8 +1,5 @@
 set(
 	HEADERS
-	${COMMAND_HEADERS}
-	${JS_HEADERS}
-	${PRIVATE_HEADERS}
 	${CMAKE_CURRENT_LIST_DIR}/alias.hpp
 	${CMAKE_CURRENT_LIST_DIR}/application.hpp
 	${CMAKE_CURRENT_LIST_DIR}/connection.hpp
@@ -64,6 +61,7 @@
 	${CMAKE_CURRENT_LIST_DIR}/server-state-disconnected.hpp
 	${CMAKE_CURRENT_LIST_DIR}/service.hpp
 	${CMAKE_CURRENT_LIST_DIR}/service-interrupt.hpp
+	${CMAKE_CURRENT_LIST_DIR}/service-plugin.hpp
 	${CMAKE_CURRENT_LIST_DIR}/service-rule.hpp
 	${CMAKE_CURRENT_LIST_DIR}/service-server.hpp
 	${CMAKE_CURRENT_LIST_DIR}/service-transport.hpp
@@ -135,6 +133,7 @@
 	${CMAKE_CURRENT_LIST_DIR}/server-state-connecting.cpp
 	${CMAKE_CURRENT_LIST_DIR}/server-state-disconnected.cpp
 	${CMAKE_CURRENT_LIST_DIR}/service-interrupt.cpp
+	${CMAKE_CURRENT_LIST_DIR}/service-plugin.cpp
 	${CMAKE_CURRENT_LIST_DIR}/service-rule.cpp
 	${CMAKE_CURRENT_LIST_DIR}/service-server.cpp
 	${CMAKE_CURRENT_LIST_DIR}/service-transport.cpp
--- a/lib/irccd/cmd-plugin-info.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/cmd-plugin-info.cpp	Wed May 11 13:58:42 2016 +0200
@@ -20,6 +20,8 @@
 
 #include "cmd-plugin-info.hpp"
 #include "irccd.hpp"
+#include "plugin.hpp"
+#include "service-plugin.hpp"
 #include "sysconfig.hpp"
 
 namespace irccd {
@@ -49,7 +51,7 @@
 json::Value PluginInfo::exec(Irccd &irccd, const json::Value &request) const
 {
 #if defined(WITH_JS)
-	auto plugin = irccd.requirePlugin(request.at("plugin").toString());
+	auto plugin = irccd.pluginService().require(request.at("plugin").toString());
 	
 	return json::object({
 		{ "author",	plugin->author()	},
--- a/lib/irccd/cmd-plugin-list.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/cmd-plugin-list.cpp	Wed May 11 13:58:42 2016 +0200
@@ -20,6 +20,8 @@
 
 #include "cmd-plugin-list.hpp"
 #include "irccd.hpp"
+#include "plugin.hpp"
+#include "service-plugin.hpp"
 #include "sysconfig.hpp"
 
 namespace irccd {
@@ -42,7 +44,7 @@
 	json::Value response = RemoteCommand::exec(irccd, request);
 	json::Value list = json::array({});
 
-	for (const auto &plugin : irccd.plugins()) {
+	for (const auto &plugin : irccd.pluginService().plugins()) {
 		list.append(plugin->name());
 	}
 
--- a/lib/irccd/cmd-plugin-load.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/cmd-plugin-load.cpp	Wed May 11 13:58:42 2016 +0200
@@ -18,6 +18,7 @@
 
 #include "cmd-plugin-load.hpp"
 #include "irccd.hpp"
+#include "service-plugin.hpp"
 #include "sysconfig.hpp"
 
 namespace irccd {
@@ -44,7 +45,7 @@
 #if defined(WITH_JS)
 	auto name = request.at("plugin").toString();
 
-	irccd.loadPlugin(name, name, true);
+	irccd.pluginService().load(name, name, true);
 
 	return RemoteCommand::exec(irccd, request);
 #else
--- a/lib/irccd/cmd-plugin-reload.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/cmd-plugin-reload.cpp	Wed May 11 13:58:42 2016 +0200
@@ -18,6 +18,8 @@
 
 #include "cmd-plugin-reload.hpp"
 #include "irccd.hpp"
+#include "plugin.hpp"
+#include "service-plugin.hpp"
 #include "sysconfig.hpp"
 
 namespace irccd {
@@ -42,7 +44,7 @@
 json::Value PluginReload::exec(Irccd &irccd, const json::Value &request) const
 {
 #if defined(WITH_JS)
-	irccd.requirePlugin(request.at("plugin").toString())->onReload();
+	irccd.pluginService().require(request.at("plugin").toString())->onReload();
 
 	return RemoteCommand::exec(irccd, request);
 #else
--- a/lib/irccd/cmd-plugin-unload.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/cmd-plugin-unload.cpp	Wed May 11 13:58:42 2016 +0200
@@ -18,6 +18,7 @@
 
 #include "cmd-plugin-unload.hpp"
 #include "irccd.hpp"
+#include "service-plugin.hpp"
 #include "sysconfig.hpp"
 
 namespace irccd {
@@ -42,7 +43,7 @@
 json::Value PluginUnload::exec(Irccd &irccd, const json::Value &request) const
 {
 #if defined(WITH_JS)
-	irccd.unloadPlugin(request.at("plugin").toString());
+	irccd.pluginService().unload(request.at("plugin").toString());
 
 	return RemoteCommand::exec(irccd, request);
 #else
--- a/lib/irccd/irccd.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/irccd.cpp	Wed May 11 13:58:42 2016 +0200
@@ -16,27 +16,19 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <algorithm>
-#include <stdexcept>
-
-#include <format.h>
-
-#include "fs.hpp"
 #include "irccd.hpp"
 #include "logger.hpp"
-#include "path.hpp"
 #include "service-interrupt.hpp"
+#include "service-plugin.hpp"
 #include "service-rule.hpp"
 #include "service-server.hpp"
 #include "service-transport.hpp"
-#include "util.hpp"
+#include "sockets.hpp"
 
 using namespace std;
 using namespace std::placeholders;
 using namespace std::string_literals;
 
-using namespace fmt::literals;
-
 namespace irccd {
 
 Irccd::Irccd()
@@ -44,6 +36,7 @@
 	, m_serverService(std::make_shared<ServerService>(*this))
 	, m_transportService(std::make_shared<TransportService>(*this))
 	, m_ruleService(std::make_shared<RuleService>())
+	, m_pluginService(std::make_shared<PluginService>(*this))
 {
 	m_services.push_back(m_interruptService);
 	m_services.push_back(m_serverService);
@@ -58,163 +51,6 @@
 	m_interruptService->interrupt();
 }
 
-#if defined(WITH_JS)
-
-std::shared_ptr<Plugin> Irccd::getPlugin(const std::string &name) const noexcept
-{
-	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
-		return plugin->name() == name;
-	});
-
-	if (it == m_plugins.end()) {
-		return nullptr;
-	}
-
-	return *it;
-}
-
-std::shared_ptr<Plugin> Irccd::requirePlugin(const std::string &name) const
-{
-	auto plugin = getPlugin(name);
-
-	if (!plugin) {
-		throw std::invalid_argument("plugin {} not found"_format(name));
-	}
-
-	return plugin;
-}
-
-void Irccd::addPlugin(std::shared_ptr<Plugin> plugin)
-{
-	std::weak_ptr<Plugin> ptr(plugin);
-
-	plugin->onTimerSignal.connect(std::bind(&Irccd::handleTimerSignal, this, ptr, _1));
-	plugin->onTimerEnd.connect(std::bind(&Irccd::handleTimerEnd, this, ptr, _1));
-
-	/* Store reference to irccd */
-	duk::putGlobal(plugin->context(), "\xff""\xff""irccd", duk::RawPointer<Irccd>{this});
-
-	/* Initial load now */
-	try {
-		plugin->onLoad();
-		m_plugins.push_back(std::move(plugin));
-	} catch (const std::exception &ex) {
-		log::warning("plugin {}: {}"_format(plugin->name(), ex.what()));
-	}
-}
-
-void Irccd::loadPlugin(std::string name, const std::string &source, bool find)
-{
-	// TODO: change with Plugin::find
-	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
-		return plugin->name() == name;
-	});
-
-	if (it != m_plugins.end()) {
-		throw std::invalid_argument("plugin already loaded");
-	}
-
-	std::vector<string> paths;
-	std::shared_ptr<Plugin> plugin;
-
-	if (find) {
-		for (const std::string &dir : path::list(path::PathPlugins)) {
-			paths.push_back(dir + source + ".js");
-		}
-	} else {
-		paths.push_back(source);
-	}
-
-	/* Iterate over all paths */
-	log::info("plugin {}: trying to load:"_format(name));
-
-	for (const auto &path : paths) {
-		log::info() << "  from " << path << std::endl;
-
-		try {
-			plugin = std::make_shared<Plugin>(name, path /*, m_pluginConf[name] */);
-			break;
-		} catch (const std::exception &ex) {
-			log::info(fmt::format("    error: {}", ex.what()));
-		}
-	}
-
-	if (plugin) {
-		addPlugin(std::move(plugin));
-	} else {
-		throw std::runtime_error("no suitable plugin found");
-	}
-}
-
-void Irccd::reloadPlugin(const std::string &name)
-{
-	auto plugin = getPlugin(name);
-
-	if (plugin) {
-		plugin->onReload();
-	}
-}
-
-void Irccd::unloadPlugin(const std::string &name)
-{
-	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
-		return plugin->name() == name;
-	});
-
-	if (it != m_plugins.end()) {
-		(*it)->onUnload();
-		m_plugins.erase(it);
-	}
-}
-
-#endif // !WITH_JS
-
-/*
- * Timer slots
- * ------------------------------------------------------------------
- *
- * These slots are called from timer threads.
- */
-
-#if defined(WITH_JS)
-
-void Irccd::handleTimerSignal(std::weak_ptr<Plugin> ptr, std::shared_ptr<Timer> timer)
-{
-	post([this, ptr, timer] (Irccd &) {
-		auto plugin = ptr.lock();
-
-		if (!plugin) {
-			return;
-		}
-
-		auto &ctx = plugin->context();
-
-		duk::StackAssert sa(ctx);
-
-		// TODO: improve this
-		try {
-			duk::getGlobal<void>(ctx, "\xff""\xff""timer-" + std::to_string(reinterpret_cast<std::intptr_t>(timer.get())));
-			duk::pcall(ctx, 0);
-			duk::pop(ctx);
-		} catch (const std::exception &) {
-		}
-	});
-}
-
-void Irccd::handleTimerEnd(std::weak_ptr<Plugin> ptr, std::shared_ptr<Timer> timer)
-{
-	post([this, ptr, timer] (Irccd &) {
-		auto plugin = ptr.lock();
-
-		if (plugin) {
-			log::debug() << "timer: finished, removing from plugin `" << plugin->name() << "'" << std::endl;
-			plugin->removeTimer(timer);
-		}
-	});
-}
-
-#endif
-
 void Irccd::run()
 {
 	while (m_running) {
--- a/lib/irccd/irccd.hpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/irccd.hpp	Wed May 11 13:58:42 2016 +0200
@@ -31,19 +31,13 @@
 #include <mutex>
 #include <vector>
 
-#include "sysconfig.hpp"
-
-#if defined(WITH_JS)
-#  include "plugin.hpp"
-#endif
-
 #include "application.hpp"
 
 namespace irccd {
 
 class InterruptService;
 class Irccd;
-class Plugin;
+class PluginService;
 class RuleService;
 class ServerService;
 class Service;
@@ -60,30 +54,14 @@
 	std::mutex m_mutex;
 	std::vector<std::function<void (Irccd &)>> m_events;
 
-	// Optional plugins.
-#if defined(WITH_JS)
-	std::vector<std::shared_ptr<Plugin>> m_plugins;
-#endif
-
 	// Services.
 	std::shared_ptr<InterruptService> m_interruptService;
 	std::shared_ptr<ServerService> m_serverService;
 	std::shared_ptr<TransportService> m_transportService;
 	std::shared_ptr<RuleService> m_ruleService;
+	std::shared_ptr<PluginService> m_pluginService;
 	std::vector<std::shared_ptr<Service>> m_services;
 
-	/*
-	 * Plugin timers slots
-	 * ----------------------------------------------------------
-	 *
-	 * These handlers catch the timer signals and call the plugin function or remove the timer from the plugin.
-	 */
-
-#if defined(WITH_JS)
-	void handleTimerSignal(std::weak_ptr<Plugin>, std::shared_ptr<Timer>);
-	void handleTimerEnd(std::weak_ptr<Plugin>, std::shared_ptr<Timer>);
-#endif
-
 	// Not copyable and not movable because services has references to irccd.
 	Irccd(const Irccd &) = delete;
 	Irccd(Irccd &&) = delete;
@@ -138,6 +116,16 @@
 	}
 
 	/**
+	 * Access the plugin service.
+	 *
+	 * \return the service
+	 */
+	inline PluginService &pluginService() noexcept
+	{
+		return *m_pluginService;
+	}
+
+	/**
 	 * Add an event to the queue. This will immediately signals the event loop to interrupt itself to dispatch
 	 * the pending events.
 	 *
@@ -146,92 +134,6 @@
 	 */
 	void post(std::function<void (Irccd &)> ev) noexcept;
 
-	/*
-	 * Plugin management
-	 * ----------------------------------------------------------
-	 *
-	 * Functions for loading JavaScript plugins.
-	 */
-
-#if defined(WITH_JS)
-	/**
-	 * Check if a plugin is loaded.
-	 *
-	 * \param name the plugin id
-	 * \return true if has plugin
-	 */
-	inline bool hasPlugin(const std::string &name) const noexcept
-	{
-		return std::count_if(m_plugins.cbegin(), m_plugins.cend(), [&] (const auto &plugin) {
-			return plugin->name() == name;
-		}) > 0;
-	}
-
-	/**
-	 * Get a plugin or empty one if not found.
-	 *
-	 * \param name the plugin id
-	 * \return the plugin or empty one if not found
-	 */
-	std::shared_ptr<Plugin> getPlugin(const std::string &name) const noexcept;
-
-	/**
-	 * Find a plugin.
-	 *
-	 * \param name the plugin id
-	 * \return the plugin
-	 * \throws std::out_of_range if not found
-	 */
-	std::shared_ptr<Plugin> requirePlugin(const std::string &name) const;
-
-	/**
-	 * Add a loaded plugin.
-	 *
-	 * Plugins signals will be connected to the irccd main loop. The onLoad function will also be called and the
-	 * plugin is not added on errors.
-	 *
-	 * \pre plugin must not be empty
-	 * \param plugin the plugin
-	 */
-	void addPlugin(std::shared_ptr<Plugin> plugin);
-
-	/**
-	 * Load a plugin by path or by searching through directories.
-	 *
-	 * TODO: Move this somewhere else (e.g. Plugin::find).
-	 *
-	 * \param source the path or the plugin id to search
-	 * \param find set to true for searching by id
-	 */
-	void loadPlugin(std::string name, const std::string &source, bool find);
-
-	/**
-	 * Unload a plugin and remove it.
-	 *
-	 * \param name the plugin id
-	 */
-	void unloadPlugin(const std::string &name);
-
-	/**
-	 * Reload a plugin by calling onReload.
-	 *
-	 * \param name the plugin name
-	 * \throw std::exception on failures
-	 */
-	void reloadPlugin(const std::string &name);
-
-	/**
-	 * Get the map of plugins.
-	 *
-	 * \return the map of plugins
-	 */
-	inline const std::vector<std::shared_ptr<Plugin>> &plugins() const noexcept
-	{
-		return m_plugins;
-	}
-
-#endif // !WITH_JS
-
 	/**
 	 * Loop forever by calling poll() and dispatch() indefinitely.
 	 */
--- a/lib/irccd/js-plugin.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/js-plugin.cpp	Wed May 11 13:58:42 2016 +0200
@@ -17,6 +17,8 @@
  */
 
 #include "irccd.hpp"
+#include "plugin.hpp"
+#include "service-plugin.hpp"
 #include "js-plugin.hpp"
 
 namespace irccd {
@@ -70,7 +72,7 @@
 	Plugin *plugin = nullptr;
 
 	if (duk::top(ctx) >= 1) {
-		plugin = duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->getPlugin(duk::require<std::string>(ctx, 0)).get();
+		plugin = duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->pluginService().get(duk::require<std::string>(ctx, 0)).get();
 	} else {
 		plugin = duk::getGlobal<duk::RawPointer<Plugin>>(ctx, "\xff""\xff""plugin");
 	}
@@ -103,7 +105,7 @@
 	duk::push(ctx, duk::Array{});
 
 	int i = 0;
-	for (const auto &plugin : duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->plugins()) {
+	for (const auto &plugin : duk::getGlobal<duk::RawPointer<Irccd>>(ctx, "\xff""\xff""irccd")->pluginService().plugins()) {
 		duk::putProperty(ctx, -1, i++, plugin->name());
 	}
 
@@ -125,7 +127,7 @@
 duk::Ret load(duk::ContextPtr ctx)
 {
 	return wrap(ctx, 0, [&] (Irccd &irccd, const std::string &name) {
-		irccd.loadPlugin(name, name, true);
+		irccd.pluginService().load(name, name, true);
 	});
 }
 
@@ -144,7 +146,7 @@
 duk::Ret reload(duk::ContextPtr ctx)
 {
 	return wrap(ctx, 0, [&] (Irccd &irccd, const std::string &name) {
-		irccd.reloadPlugin(name);
+		irccd.pluginService().reload(name);
 	});
 }
 
@@ -163,7 +165,7 @@
 duk::Ret unload(duk::ContextPtr ctx)
 {
 	return wrap(ctx, 0, [&] (Irccd &irccd, const std::string &name) {
-		irccd.unloadPlugin(name);
+		irccd.pluginService().unload(name);
 	});
 }
 
--- a/lib/irccd/server-event.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/server-event.cpp	Wed May 11 13:58:42 2016 +0200
@@ -18,7 +18,9 @@
 
 #include "irccd.hpp"
 #include "logger.hpp"
+#include "plugin.hpp"
 #include "server-event.hpp"
+#include "service-plugin.hpp"
 #include "service-rule.hpp"
 
 namespace irccd {
@@ -38,7 +40,7 @@
 
 void ServerEvent::operator()(Irccd &irccd) const
 {
-	for (auto &plugin : irccd.plugins()) {
+	for (auto &plugin : irccd.pluginService().plugins()) {
 		auto eventname = m_plugin_function_name(*plugin);
 		auto allowed = irccd.ruleService().solve(m_server, m_target, m_origin, plugin->name(), eventname);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/service-plugin.cpp	Wed May 11 13:58:42 2016 +0200
@@ -0,0 +1,190 @@
+/*
+ * service-plugin.cpp -- manage plugins
+ *
+ * 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 <algorithm>
+#include <functional>
+#include <stdexcept>
+
+#include <format.h>
+
+#include "irccd.hpp"
+#include "logger.hpp"
+#include "plugin.hpp"
+#include "service-plugin.hpp"
+
+using namespace fmt::literals;
+
+namespace irccd {
+
+PluginService::PluginService(Irccd &irccd) noexcept
+	: m_irccd(irccd)
+{
+}
+
+bool PluginService::has(const std::string &name) const noexcept
+{
+	return std::count_if(m_plugins.cbegin(), m_plugins.cend(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	}) > 0;
+}
+
+std::shared_ptr<Plugin> PluginService::get(const std::string &name) const noexcept
+{
+	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	});
+
+	if (it == m_plugins.end()) {
+		return nullptr;
+	}
+
+	return *it;
+}
+
+std::shared_ptr<Plugin> PluginService::require(const std::string &name) const
+{
+	auto plugin = get(name);
+
+	if (!plugin) {
+		throw std::invalid_argument("plugin {} not found"_format(name));
+	}
+
+	return plugin;
+}
+
+void PluginService::add(std::shared_ptr<Plugin> plugin)
+{
+	using namespace std::placeholders;
+
+	std::weak_ptr<Plugin> ptr(plugin);
+
+	plugin->onTimerSignal.connect(std::bind(&PluginService::handleTimerSignal, this, ptr, _1));
+	plugin->onTimerEnd.connect(std::bind(&PluginService::handleTimerEnd, this, ptr, _1));
+
+	// Store reference to irccd.
+	duk::putGlobal(plugin->context(), "\xff""\xff""irccd", duk::RawPointer<Irccd>{&m_irccd});
+
+	// Initial load now.
+	try {
+		plugin->onLoad();
+		m_plugins.push_back(std::move(plugin));
+	} catch (const std::exception &ex) {
+		log::warning("plugin {}: {}"_format(plugin->name(), ex.what()));
+	}
+}
+
+void PluginService::load(std::string name, const std::string &source, bool find)
+{
+	// TODO: change with Plugin::find
+	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	});
+
+	if (it != m_plugins.end()) {
+		throw std::invalid_argument("plugin already loaded");
+	}
+
+	std::vector<std::string> paths;
+	std::shared_ptr<Plugin> plugin;
+
+	if (find) {
+		for (const std::string &dir : path::list(path::PathPlugins)) {
+			paths.push_back(dir + source + ".js");
+		}
+	} else {
+		paths.push_back(source);
+	}
+
+	// Iterate over all paths.
+	log::info("plugin {}: trying to load:"_format(name));
+
+	for (const auto &path : paths) {
+		log::info() << "  from " << path << std::endl;
+
+		try {
+			plugin = std::make_shared<Plugin>(name, path /*, m_pluginConf[name] */);
+			break;
+		} catch (const std::exception &ex) {
+			log::info(fmt::format("    error: {}", ex.what()));
+		}
+	}
+
+	if (plugin) {
+		add(std::move(plugin));
+	} else {
+		throw std::runtime_error("no suitable plugin found");
+	}
+}
+
+void PluginService::reload(const std::string &name)
+{
+	auto plugin = get(name);
+
+	if (plugin) {
+		plugin->onReload();
+	}
+}
+
+void PluginService::unload(const std::string &name)
+{
+	auto it = std::find_if(m_plugins.begin(), m_plugins.end(), [&] (const auto &plugin) {
+		return plugin->name() == name;
+	});
+
+	if (it != m_plugins.end()) {
+		(*it)->onUnload();
+		m_plugins.erase(it);
+	}
+}
+
+void PluginService::handleTimerSignal(std::weak_ptr<Plugin> ptr, std::shared_ptr<Timer> timer)
+{
+	m_irccd.post([this, ptr, timer] (Irccd &) {
+		auto plugin = ptr.lock();
+
+		if (!plugin) {
+			return;
+		}
+
+		auto &ctx = plugin->context();
+
+		duk::StackAssert sa(ctx);
+
+		// TODO: improve this
+		try {
+			duk::getGlobal<void>(ctx, "\xff""\xff""timer-" + std::to_string(reinterpret_cast<std::intptr_t>(timer.get())));
+			duk::pcall(ctx, 0);
+			duk::pop(ctx);
+		} catch (const std::exception &) {
+		}
+	});
+}
+
+void PluginService::handleTimerEnd(std::weak_ptr<Plugin> ptr, std::shared_ptr<Timer> timer)
+{
+	m_irccd.post([this, ptr, timer] (Irccd &) {
+		auto plugin = ptr.lock();
+
+		if (plugin) {
+			log::debug() << "timer: finished, removing from plugin `" << plugin->name() << "'" << std::endl;
+			plugin->removeTimer(timer);
+		}
+	});
+}
+
+} // !irccd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/irccd/service-plugin.hpp	Wed May 11 13:58:42 2016 +0200
@@ -0,0 +1,128 @@
+/*
+ * service-plugin.hpp -- manage plugins
+ *
+ * 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_SERVICE_PLUGIN_HPP
+#define IRCCD_SERVICE_PLUGIN_HPP
+
+/**
+ * \file service-plugin.hpp
+ * \brief Manage plugins.
+ */
+
+#include <memory>
+#include <vector>
+
+namespace irccd {
+
+class Irccd;
+class Plugin;
+class Timer;
+
+/**
+ * \brief Manage plugins.
+ */
+class PluginService {
+private:
+	Irccd &m_irccd;
+	std::vector<std::shared_ptr<Plugin>> m_plugins;
+
+	// TODO: get rid of this with future JavaScript modules.
+	void handleTimerSignal(std::weak_ptr<Plugin>, std::shared_ptr<Timer>);
+	void handleTimerEnd(std::weak_ptr<Plugin>, std::shared_ptr<Timer>);
+
+public:
+	/**
+	 * Create the plugin service.
+	 *
+	 * \param irccd the irccd instance
+	 */
+	PluginService(Irccd &irccd) noexcept;
+
+	/**
+	 * Get the list of plugins.
+	 *
+	 * \return the list of plugins
+	 */
+	inline const std::vector<std::shared_ptr<Plugin>> &plugins() const noexcept
+	{
+		return m_plugins;
+	}
+
+	/**
+	 * Check if a plugin is loaded.
+	 *
+	 * \param name the plugin id
+	 * \return true if has plugin
+	 */
+	bool has(const std::string &name) const noexcept;
+
+	/**
+	 * Get a plugin or empty one if not found.
+	 *
+	 * \param name the plugin id
+	 * \return the plugin or empty one if not found
+	 */
+	std::shared_ptr<Plugin> get(const std::string &name) const noexcept;
+
+	/**
+	 * Find a plugin.
+	 *
+	 * \param name the plugin id
+	 * \return the plugin
+	 * \throws std::out_of_range if not found
+	 */
+	std::shared_ptr<Plugin> require(const std::string &name) const;
+
+	/**
+	 * Add a loaded plugin.
+	 *
+	 * Plugins signals will be connected to the irccd main loop. The onLoad function will also be called and the
+	 * plugin is not added on errors.
+	 *
+	 * \pre plugin must not be empty
+	 * \param plugin the plugin
+	 */
+	void add(std::shared_ptr<Plugin> plugin);
+
+	/**
+	 * Load a plugin by path or by searching through directories.
+	 *
+	 * \param source the path or the plugin id to search
+	 * \param find set to true for searching by id
+	 */
+	void load(std::string name, const std::string &source, bool find);
+
+	/**
+	 * Unload a plugin and remove it.
+	 *
+	 * \param name the plugin id
+	 */
+	void unload(const std::string &name);
+
+	/**
+	 * Reload a plugin by calling onReload.
+	 *
+	 * \param name the plugin name
+	 * \throw std::exception on failures
+	 */
+	void reload(const std::string &name);
+};
+
+} // !irccd
+
+#endif // !IRCCD_SERVICE_PLUGIN_HPP
--- a/lib/irccd/service-server.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/lib/irccd/service-server.cpp	Wed May 11 13:58:42 2016 +0200
@@ -22,6 +22,7 @@
 
 #include "irccd.hpp"
 #include "logger.hpp"
+#include "plugin.hpp"
 #include "server.hpp"
 #include "server-event.hpp"
 #include "service-server.hpp"
--- a/tests/js-timer/main.cpp	Wed May 11 13:27:39 2016 +0200
+++ b/tests/js-timer/main.cpp	Wed May 11 13:58:42 2016 +0200
@@ -21,6 +21,8 @@
 #include <irccd/elapsed-timer.hpp>
 #include <irccd/irccd.hpp>
 #include <irccd/logger.hpp>
+#include <irccd/plugin.hpp>
+#include <irccd/service-plugin.hpp>
 #include <irccd/system.hpp>
 
 using namespace irccd;
@@ -32,7 +34,7 @@
 
 	auto plugin = std::make_shared<Plugin>("timer", IRCCD_TESTS_DIRECTORY "/timer-single.js");
 
-	irccd.addPlugin(plugin);
+	irccd.pluginService().add(plugin);
 
 	while (timer.elapsed() < 3000) {
 		irccd.poll();
@@ -49,7 +51,7 @@
 
 	auto plugin = std::make_shared<Plugin>("timer", IRCCD_TESTS_DIRECTORY "/timer-repeat.js");
 
-	irccd.addPlugin(plugin);
+	irccd.pluginService().add(plugin);
 
 	while (timer.elapsed() < 3000) {
 		irccd.poll();