From 4de3380d96672552d8b29fba1b3b0df51bdcf5cb Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 25 Apr 2016 22:27:04 +0800 Subject: [PATCH 001/116] emqplus backends --- .gitmodules | 15 +++++++++++++++ plugins/emqplus_backend_cassa | 1 + plugins/emqplus_backend_mongo | 1 + plugins/emqplus_backend_mysql | 1 + plugins/emqplus_backend_pgsql | 1 + plugins/emqplus_backend_redis | 1 + 6 files changed, 20 insertions(+) create mode 160000 plugins/emqplus_backend_cassa create mode 160000 plugins/emqplus_backend_mongo create mode 160000 plugins/emqplus_backend_mysql create mode 160000 plugins/emqplus_backend_pgsql create mode 160000 plugins/emqplus_backend_redis diff --git a/.gitmodules b/.gitmodules index 17e1be420..fd65dc85a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,18 @@ [submodule "plugins/emqttd_plugin_redis"] path = plugins/emqttd_plugin_redis url = https://github.com/emqtt/emqttd_plugin_redis.git +[submodule "plugins/emqplus_backend_redis"] + path = plugins/emqplus_backend_redis + url = git@github.com:emqplus/emqplus_backend_redis.git +[submodule "plugins/emqplus_backend_mongo"] + path = plugins/emqplus_backend_mongo + url = git@github.com:emqplus/emqplus_backend_mongo.git +[submodule "plugins/emqplus_backend_mysql"] + path = plugins/emqplus_backend_mysql + url = git@github.com:emqplus/emqplus_backend_mysql.git +[submodule "plugins/emqplus_backend_pgsql"] + path = plugins/emqplus_backend_pgsql + url = git@github.com:emqplus/emqplus_backend_pgsql.git +[submodule "plugins/emqplus_backend_cassa"] + path = plugins/emqplus_backend_cassa + url = git@github.com:emqplus/emqplus_backend_cassa.git diff --git a/plugins/emqplus_backend_cassa b/plugins/emqplus_backend_cassa new file mode 160000 index 000000000..62c52536a --- /dev/null +++ b/plugins/emqplus_backend_cassa @@ -0,0 +1 @@ +Subproject commit 62c52536a4263fa2735478420b8e78b8f494be2a diff --git a/plugins/emqplus_backend_mongo b/plugins/emqplus_backend_mongo new file mode 160000 index 000000000..8a7b5cc03 --- /dev/null +++ b/plugins/emqplus_backend_mongo @@ -0,0 +1 @@ +Subproject commit 8a7b5cc03f1e55cfd6d50ed1a7161ecd8a4a14be diff --git a/plugins/emqplus_backend_mysql b/plugins/emqplus_backend_mysql new file mode 160000 index 000000000..c7bebfe62 --- /dev/null +++ b/plugins/emqplus_backend_mysql @@ -0,0 +1 @@ +Subproject commit c7bebfe62a208284cb38cab89f2fd6d73968aa33 diff --git a/plugins/emqplus_backend_pgsql b/plugins/emqplus_backend_pgsql new file mode 160000 index 000000000..92825dea5 --- /dev/null +++ b/plugins/emqplus_backend_pgsql @@ -0,0 +1 @@ +Subproject commit 92825dea51e3debf68a2387e66bae27d375975f8 diff --git a/plugins/emqplus_backend_redis b/plugins/emqplus_backend_redis new file mode 160000 index 000000000..4e9c1a234 --- /dev/null +++ b/plugins/emqplus_backend_redis @@ -0,0 +1 @@ +Subproject commit 4e9c1a234d35561c6227f0e6ff98a612fab50ec4 From 23525f9ca03740946852bbe76ce11ad60c4dac7e Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 30 Apr 2016 10:18:00 +0800 Subject: [PATCH 002/116] remove submodules --- plugins/emqplus_backend_cassa | 1 - plugins/emqplus_backend_mongo | 1 - plugins/emqplus_backend_mysql | 1 - plugins/emqplus_backend_pgsql | 1 - plugins/emqplus_backend_redis | 1 - plugins/emqttd_dashboard | 1 - plugins/emqttd_plugin_mysql | 1 - plugins/emqttd_plugin_pgsql | 1 - plugins/emqttd_plugin_redis | 1 - plugins/emqttd_plugin_template | 1 - plugins/emqttd_recon | 1 - plugins/emqttd_reloader | 1 - plugins/emqttd_stomp | 1 - 13 files changed, 13 deletions(-) delete mode 160000 plugins/emqplus_backend_cassa delete mode 160000 plugins/emqplus_backend_mongo delete mode 160000 plugins/emqplus_backend_mysql delete mode 160000 plugins/emqplus_backend_pgsql delete mode 160000 plugins/emqplus_backend_redis delete mode 160000 plugins/emqttd_dashboard delete mode 160000 plugins/emqttd_plugin_mysql delete mode 160000 plugins/emqttd_plugin_pgsql delete mode 160000 plugins/emqttd_plugin_redis delete mode 160000 plugins/emqttd_plugin_template delete mode 160000 plugins/emqttd_recon delete mode 160000 plugins/emqttd_reloader delete mode 160000 plugins/emqttd_stomp diff --git a/plugins/emqplus_backend_cassa b/plugins/emqplus_backend_cassa deleted file mode 160000 index 62c52536a..000000000 --- a/plugins/emqplus_backend_cassa +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 62c52536a4263fa2735478420b8e78b8f494be2a diff --git a/plugins/emqplus_backend_mongo b/plugins/emqplus_backend_mongo deleted file mode 160000 index 8a7b5cc03..000000000 --- a/plugins/emqplus_backend_mongo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8a7b5cc03f1e55cfd6d50ed1a7161ecd8a4a14be diff --git a/plugins/emqplus_backend_mysql b/plugins/emqplus_backend_mysql deleted file mode 160000 index c7bebfe62..000000000 --- a/plugins/emqplus_backend_mysql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7bebfe62a208284cb38cab89f2fd6d73968aa33 diff --git a/plugins/emqplus_backend_pgsql b/plugins/emqplus_backend_pgsql deleted file mode 160000 index 92825dea5..000000000 --- a/plugins/emqplus_backend_pgsql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 92825dea51e3debf68a2387e66bae27d375975f8 diff --git a/plugins/emqplus_backend_redis b/plugins/emqplus_backend_redis deleted file mode 160000 index 4e9c1a234..000000000 --- a/plugins/emqplus_backend_redis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e9c1a234d35561c6227f0e6ff98a612fab50ec4 diff --git a/plugins/emqttd_dashboard b/plugins/emqttd_dashboard deleted file mode 160000 index 0a5983510..000000000 --- a/plugins/emqttd_dashboard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a59835106c7672699da7c2f9deb576678fdb37e diff --git a/plugins/emqttd_plugin_mysql b/plugins/emqttd_plugin_mysql deleted file mode 160000 index d714c7598..000000000 --- a/plugins/emqttd_plugin_mysql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d714c759804f9528f187fadd544912e37fd664b0 diff --git a/plugins/emqttd_plugin_pgsql b/plugins/emqttd_plugin_pgsql deleted file mode 160000 index 81f0164a3..000000000 --- a/plugins/emqttd_plugin_pgsql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 81f0164a339044d329c3179f922df332ae102696 diff --git a/plugins/emqttd_plugin_redis b/plugins/emqttd_plugin_redis deleted file mode 160000 index 9fef7a1b7..000000000 --- a/plugins/emqttd_plugin_redis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9fef7a1b762dfd8ad27190ebe31dd3afc784c6fa diff --git a/plugins/emqttd_plugin_template b/plugins/emqttd_plugin_template deleted file mode 160000 index 6e1382b63..000000000 --- a/plugins/emqttd_plugin_template +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e1382b63096cc3259f7edc6c26adc571ee53d74 diff --git a/plugins/emqttd_recon b/plugins/emqttd_recon deleted file mode 160000 index eb4a03d90..000000000 --- a/plugins/emqttd_recon +++ /dev/null @@ -1 +0,0 @@ -Subproject commit eb4a03d90f932839bdad9aa5703de7731169ce66 diff --git a/plugins/emqttd_reloader b/plugins/emqttd_reloader deleted file mode 160000 index d7ff2a70b..000000000 --- a/plugins/emqttd_reloader +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d7ff2a70bc259432c9cf38e7c55781a474b4d2eb diff --git a/plugins/emqttd_stomp b/plugins/emqttd_stomp deleted file mode 160000 index 745871d6a..000000000 --- a/plugins/emqttd_stomp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 745871d6ae6f80de49dc4350772dd0f91d6a4408 From a471f6a3e21fffb5ff495a2ae083daa7a8d0425f Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 30 Apr 2016 10:19:23 +0800 Subject: [PATCH 003/116] rm submodules --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 659126c0d..b0b1b1c37 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,10 @@ BASE_DIR = $(shell pwd) REBAR = $(BASE_DIR)/rebar DIST = $(BASE_DIR)/rel/$(APP) -all: submods compile +all: compile -submods: - @git submodule update --init +# submods: +# @git submodule update --init compile: deps @$(REBAR) compile From 0d84c7474aa9aba893f5bfaef51e75756da1e8f1 Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 30 Apr 2016 10:21:53 +0800 Subject: [PATCH 004/116] rm plugins --- Makefile | 14 +------------- rel/files/loaded_plugins | 1 - 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/Makefile b/Makefile index b0b1b1c37..2e02db60e 100644 --- a/Makefile +++ b/Makefile @@ -35,18 +35,7 @@ edoc: rel: compile @cd rel && $(REBAR) generate -f -plugins: - @for plugin in ./plugins/* ; do \ - if [ -d $${plugin} ]; then \ - mkdir -p $(DIST)/$${plugin}/ ; \ - cp -R $${plugin}/ebin $(DIST)/$${plugin}/ ; \ - [ -d "$${plugin}/priv" ] && cp -R $${plugin}/priv $(DIST)/$${plugin}/ ; \ - [ -d "$${plugin}/etc" ] && cp -R $${plugin}/etc $(DIST)/$${plugin}/ ; \ - echo "$${plugin} copied" ; \ - fi \ - done - -dist: rel plugins +dist: rel PLT = $(BASE_DIR)/.emqttd_dialyzer.plt APPS = erts kernel stdlib sasl crypto ssl os_mon syntax_tools \ @@ -63,4 +52,3 @@ build_plt: compile dialyzer: compile dialyzer -Wno_return --plt $(PLT) deps/*/ebin ./ebin plugins/*/ebin - diff --git a/rel/files/loaded_plugins b/rel/files/loaded_plugins index 68ba6a41d..e69de29bb 100644 --- a/rel/files/loaded_plugins +++ b/rel/files/loaded_plugins @@ -1 +0,0 @@ -emqttd_dashboard. From 4900641a893de4088ab24aaf56d81d775bb99c4b Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 30 Apr 2016 14:18:16 +0800 Subject: [PATCH 005/116] extra --- include/emqttd.hrl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index b68b583f0..43f9e36b4 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -124,7 +124,8 @@ dup = false :: boolean(), %% Dup flag sys = false :: boolean(), %% $SYS flag payload :: binary(), %% Payload - timestamp :: erlang:timestamp() %% os:timestamp + timestamp :: erlang:timestamp(), %% os:timestamp + extra = [] :: list() }). -type mqtt_message() :: #mqtt_message{}. From f2163c7dbbc2fefcdc4a58ee9c125c1dad203a40 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 1 May 2016 18:02:51 +0800 Subject: [PATCH 006/116] rm submods, add 'extra' field --- Makefile | 2 +- include/emqttd.hrl | 3 ++- plugins/emqplus_backend_cassa | 1 - plugins/emqplus_backend_mongo | 1 - plugins/emqplus_backend_mysql | 1 - plugins/emqplus_backend_pgsql | 1 - plugins/emqplus_backend_redis | 1 - plugins/emqttd_dashboard | 1 - plugins/emqttd_plugin_mysql | 1 - plugins/emqttd_plugin_pgsql | 1 - plugins/emqttd_plugin_redis | 1 - plugins/emqttd_plugin_template | 1 - plugins/emqttd_recon | 1 - plugins/emqttd_stomp | 1 - 14 files changed, 3 insertions(+), 14 deletions(-) delete mode 160000 plugins/emqplus_backend_cassa delete mode 160000 plugins/emqplus_backend_mongo delete mode 160000 plugins/emqplus_backend_mysql delete mode 160000 plugins/emqplus_backend_pgsql delete mode 160000 plugins/emqplus_backend_redis delete mode 160000 plugins/emqttd_dashboard delete mode 160000 plugins/emqttd_plugin_mysql delete mode 160000 plugins/emqttd_plugin_pgsql delete mode 160000 plugins/emqttd_plugin_redis delete mode 160000 plugins/emqttd_plugin_template delete mode 160000 plugins/emqttd_recon delete mode 160000 plugins/emqttd_stomp diff --git a/Makefile b/Makefile index 659126c0d..c1a173c07 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BASE_DIR = $(shell pwd) REBAR = $(BASE_DIR)/rebar DIST = $(BASE_DIR)/rel/$(APP) -all: submods compile +all: compile submods: @git submodule update --init diff --git a/include/emqttd.hrl b/include/emqttd.hrl index b68b583f0..cb849d246 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -124,7 +124,8 @@ dup = false :: boolean(), %% Dup flag sys = false :: boolean(), %% $SYS flag payload :: binary(), %% Payload - timestamp :: erlang:timestamp() %% os:timestamp + timestamp :: erlang:timestamp(), %% os:timestamp + extra = [] :: list() }). -type mqtt_message() :: #mqtt_message{}. diff --git a/plugins/emqplus_backend_cassa b/plugins/emqplus_backend_cassa deleted file mode 160000 index 62c52536a..000000000 --- a/plugins/emqplus_backend_cassa +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 62c52536a4263fa2735478420b8e78b8f494be2a diff --git a/plugins/emqplus_backend_mongo b/plugins/emqplus_backend_mongo deleted file mode 160000 index 8a7b5cc03..000000000 --- a/plugins/emqplus_backend_mongo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8a7b5cc03f1e55cfd6d50ed1a7161ecd8a4a14be diff --git a/plugins/emqplus_backend_mysql b/plugins/emqplus_backend_mysql deleted file mode 160000 index c7bebfe62..000000000 --- a/plugins/emqplus_backend_mysql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7bebfe62a208284cb38cab89f2fd6d73968aa33 diff --git a/plugins/emqplus_backend_pgsql b/plugins/emqplus_backend_pgsql deleted file mode 160000 index 92825dea5..000000000 --- a/plugins/emqplus_backend_pgsql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 92825dea51e3debf68a2387e66bae27d375975f8 diff --git a/plugins/emqplus_backend_redis b/plugins/emqplus_backend_redis deleted file mode 160000 index 4e9c1a234..000000000 --- a/plugins/emqplus_backend_redis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e9c1a234d35561c6227f0e6ff98a612fab50ec4 diff --git a/plugins/emqttd_dashboard b/plugins/emqttd_dashboard deleted file mode 160000 index 0a5983510..000000000 --- a/plugins/emqttd_dashboard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a59835106c7672699da7c2f9deb576678fdb37e diff --git a/plugins/emqttd_plugin_mysql b/plugins/emqttd_plugin_mysql deleted file mode 160000 index d714c7598..000000000 --- a/plugins/emqttd_plugin_mysql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d714c759804f9528f187fadd544912e37fd664b0 diff --git a/plugins/emqttd_plugin_pgsql b/plugins/emqttd_plugin_pgsql deleted file mode 160000 index 81f0164a3..000000000 --- a/plugins/emqttd_plugin_pgsql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 81f0164a339044d329c3179f922df332ae102696 diff --git a/plugins/emqttd_plugin_redis b/plugins/emqttd_plugin_redis deleted file mode 160000 index 9fef7a1b7..000000000 --- a/plugins/emqttd_plugin_redis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9fef7a1b762dfd8ad27190ebe31dd3afc784c6fa diff --git a/plugins/emqttd_plugin_template b/plugins/emqttd_plugin_template deleted file mode 160000 index 6e1382b63..000000000 --- a/plugins/emqttd_plugin_template +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6e1382b63096cc3259f7edc6c26adc571ee53d74 diff --git a/plugins/emqttd_recon b/plugins/emqttd_recon deleted file mode 160000 index eb4a03d90..000000000 --- a/plugins/emqttd_recon +++ /dev/null @@ -1 +0,0 @@ -Subproject commit eb4a03d90f932839bdad9aa5703de7731169ce66 diff --git a/plugins/emqttd_stomp b/plugins/emqttd_stomp deleted file mode 160000 index b8b5393ec..000000000 --- a/plugins/emqttd_stomp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b8b5393ecda9617a2b724dae0ab431d368963ca4 From f96f209b4a0039dec55250db5d6ffe192c28f6ce Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 2 May 2016 00:55:59 +0800 Subject: [PATCH 007/116] esockd .* --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index fc6762faf..1ee966c4f 100644 --- a/rebar.config +++ b/rebar.config @@ -41,7 +41,7 @@ {deps, [ {gproc, ".*", {git, "git://github.com/uwiger/gproc.git", {branch, "master"}}}, {lager, ".*", {git, "git://github.com/basho/lager.git", {branch, "master"}}}, - {esockd, "3.*", {git, "git://github.com/emqtt/esockd.git", {branch, "master"}}}, + {esockd, ".*", {git, "git://github.com/emqtt/esockd.git", {branch, "master"}}}, {mochiweb, "4.*", {git, "git://github.com/emqtt/mochiweb.git", {branch, "master"}}} ]}. From c73d299942c25a1b5f5aa31f73c0e98a27150514 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 2 May 2016 01:05:55 +0800 Subject: [PATCH 008/116] gen_logger --- rebar.config | 1 + 1 file changed, 1 insertion(+) diff --git a/rebar.config b/rebar.config index 1ee966c4f..5d90eb17e 100644 --- a/rebar.config +++ b/rebar.config @@ -41,6 +41,7 @@ {deps, [ {gproc, ".*", {git, "git://github.com/uwiger/gproc.git", {branch, "master"}}}, {lager, ".*", {git, "git://github.com/basho/lager.git", {branch, "master"}}}, + {gen_logger, ".*", {git, "git://github.com/emqtt/gen_logger.git", {branch, "master"}}}, {esockd, ".*", {git, "git://github.com/emqtt/esockd.git", {branch, "master"}}}, {mochiweb, "4.*", {git, "git://github.com/emqtt/mochiweb.git", {branch, "master"}}} ]}. From 3aef962a07afcff628fe27b1e448c07273db5dd4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 2 May 2016 22:31:28 +0800 Subject: [PATCH 009/116] 2.0 --- src/emqttd.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd.app.src b/src/emqttd.app.src index 8e102ccc6..9b58ce3df 100644 --- a/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "1.0.1"}, + {vsn, "2.0"}, {id, "emqttd"}, {modules, []}, {registered, []}, From 9b71f9f6c09fbac3e5d2ea742ae9e6d5545b09ff Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 3 May 2016 18:02:41 +0800 Subject: [PATCH 010/116] new plugin mechanism --- src/emqttd_plugins.erl | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/emqttd_plugins.erl b/src/emqttd_plugins.erl index 271b9bd08..f0d31be0a 100644 --- a/src/emqttd_plugins.erl +++ b/src/emqttd_plugins.erl @@ -70,10 +70,10 @@ stop_plugins(Names) -> %% @doc List all available plugins -spec(list() -> [mqtt_plugin()]). list() -> - case env(plugins_dir) of - {ok, PluginsDir} -> - AppFiles = filelib:wildcard("*/ebin/*.app", PluginsDir), - Plugins = [plugin(PluginsDir, AppFile) || AppFile <- AppFiles], + case env(plugins_etc) of + {ok, PluginsEtc} -> + CfgFiles = filelib:wildcard("*.config", PluginsEtc), + Plugins = [plugin(PluginsEtc, CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), lists:map(fun(Plugin = #mqtt_plugin{name = Name}) -> case lists:member(Name, StartedApps) of @@ -85,21 +85,13 @@ list() -> [] end. -plugin(PluginsDir, AppFile0) -> - AppFile = filename:join(PluginsDir, AppFile0), - {ok, [{application, Name, Attrs}]} = file:consult(AppFile), - CfgFile = filename:join([PluginsDir, Name, "etc/plugin.config"]), - AppsEnv1 = - case filelib:is_file(CfgFile) of - true -> - {ok, [AppsEnv]} = file:consult(CfgFile), - AppsEnv; - false -> - [] - end, +plugin(PluginsEtc, CfgFile0) -> + CfgFile = filename:join(PluginsEtc, CfgFile0), + {ok, [[{AppName, AppEnv} | _]]} = file:consult(CfgFile), + {ok, Attrs} = application:get_all_key(AppName), Ver = proplists:get_value(vsn, Attrs, "0"), Descr = proplists:get_value(description, Attrs, ""), - #mqtt_plugin{name = Name, version = Ver, config = AppsEnv1, descr = Descr}. + #mqtt_plugin{name = AppName, version = Ver, config = AppEnv, descr = Descr}. %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, any()}). @@ -126,23 +118,16 @@ load_plugin(#mqtt_plugin{name = Name, config = Config}, Persistent) -> {error, Error} end. -load_app(App, Config) -> +load_app(App, _Config) -> case application:load(App) of ok -> - set_config(Config); + ok; {error, {already_loaded, App}} -> - set_config(Config); + ok; {error, Error} -> {error, Error} end. -%% This trick is awesome:) -set_config([]) -> - ok; -set_config([{AppName, Envs} | Config]) -> - [application:set_env(AppName, Par, Val) || {Par, Val} <- Envs], - set_config(Config). - start_app(App, SuccFun) -> case application:ensure_all_started(App) of {ok, Started} -> From 3ef099389aca7c0d718bb0ecc18eb941a5b153a5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 5 May 2016 18:18:50 +0800 Subject: [PATCH 011/116] 1.1 --- src/emqttd.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd.app.src b/src/emqttd.app.src index 9b58ce3df..5485a8d46 100644 --- a/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "2.0"}, + {vsn, "1.1"}, {id, "emqttd"}, {modules, []}, {registered, []}, From a398a74ed5c84e550b212b52ac04f15480e2e86a Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 5 Jun 2016 12:52:36 +0800 Subject: [PATCH 012/116] rm .gimodules --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3e6105d27..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "plugins/emqttd_sockjs"] - path = plugins/emqttd_sockjs - url = https://github.com/emqtt/emqttd_sockjs.git From 83b25fe84a9555f3d0ecde2521ccd5a113af0a83 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 20 Jun 2016 12:18:49 +0800 Subject: [PATCH 013/116] 2.0 --- .gitignore | 2 ++ src/emqttd.app.src | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 613802978..8e6271dba 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ eunit.coverdata test/ct.cover.spec logs ct.coverdata +.idea/ +emqttd.iml diff --git a/src/emqttd.app.src b/src/emqttd.app.src index 2f3f75de0..9b58ce3df 100644 --- a/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "1.1.1"}, + {vsn, "2.0"}, {id, "emqttd"}, {modules, []}, {registered, []}, From 61a64ea0b0f629642080494b0652e18a527c1025 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 28 Jun 2016 16:44:52 +0800 Subject: [PATCH 014/116] send(Packet) --- src/emqttd_client.erl | 9 ++++++++- src/emqttd_protocol.erl | 9 +++------ src/emqttd_ws_client.erl | 6 +++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/emqttd_client.erl b/src/emqttd_client.erl index f3169339a..89cc6a4a7 100644 --- a/src/emqttd_client.erl +++ b/src/emqttd_client.erl @@ -80,13 +80,20 @@ init([OriginConn, MqttEnv]) -> end, ConnName = esockd_net:format(PeerName), Self = self(), - SendFun = fun(Data) -> + + %%TODO: Send packet... + SendFun = fun(Packet) -> + Data = emqttd_serializer:serialize(Packet), + %%TODO: How to Log??? + ?LOG(debug, "SEND ~p", [Data], #client_state{connname = ConnName}), + emqttd_metrics:inc('bytes/sent', size(Data)), try Connection:async_send(Data) of true -> ok catch error:Error -> Self ! {shutdown, Error} end end, + PktOpts = proplists:get_value(packet, MqttEnv), ParserFun = emqttd_parser:new(PktOpts), ProtoState = emqttd_protocol:init(PeerName, SendFun, PktOpts), diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 7c8a87146..64594866e 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -236,8 +236,8 @@ publish(Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), State = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> + username = Username, + session = Session}) -> Msg = emqttd_message:from_packet(Username, ClientId, Packet), case emqttd_session:publish(Session, Msg) of ok -> @@ -256,10 +256,7 @@ send(Packet, State = #proto_state{sendfun = SendFun}) when is_record(Packet, mqtt_packet) -> trace(send, Packet, State), emqttd_metrics:sent(Packet), - Data = emqttd_serializer:serialize(Packet), - ?LOG(debug, "SEND ~p", [Data], State), - emqttd_metrics:inc('bytes/sent', size(Data)), - SendFun(Data), + SendFun(Packet), {ok, State}. trace(recv, Packet, ProtoState) -> diff --git a/src/emqttd_ws_client.erl b/src/emqttd_ws_client.erl index 776dc4ce5..7466148eb 100644 --- a/src/emqttd_ws_client.erl +++ b/src/emqttd_ws_client.erl @@ -108,7 +108,11 @@ init([WsPid, Req, ReplyChannel, PktOpts]) -> %%issue#413: trap_exit is unnecessary %%process_flag(trap_exit, true), {ok, Peername} = Req:get(peername), - SendFun = fun(Payload) -> ReplyChannel({binary, Payload}) end, + SendFun = fun(Packet) -> + Data = emqttd_serializer:serialize(Packet), + emqttd_metrics:inc('bytes/sent', size(Data)), + ReplyChannel({binary, Data}) + end, Headers = mochiweb_request:get(headers, Req), HeadersList = mochiweb_headers:to_list(Headers), ProtoState = emqttd_protocol:init(Peername, SendFun, From 03d6710b60c1d7591d57a468ca7b8142b1ae2301 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 19 Jul 2016 16:49:30 +0800 Subject: [PATCH 015/116] gen_conf to improve the configuration of emqttd broker --- etc/acl.conf | 28 +++ etc/client.conf | 3 + etc/emqttd.conf | 250 +++++++++++++++++++++++++ etc/passwd.conf | 2 + {rel/files => etc}/ssl/ssl.crt | 0 {rel/files => etc}/ssl/ssl.key | 0 rel/files/emqttd | 323 --------------------------------- rel/files/emqttd.cmd | 108 ----------- rel/files/emqttd.test.config | 300 ------------------------------ rel/files/rewrite.config | 14 -- rel/reltool.config | 98 ---------- rel/reltool.config.script | 18 -- rel/vars.config | 28 --- src/emqttd_auth_ldap.erl | 77 -------- 14 files changed, 283 insertions(+), 966 deletions(-) create mode 100644 etc/acl.conf create mode 100644 etc/client.conf create mode 100644 etc/emqttd.conf create mode 100644 etc/passwd.conf rename {rel/files => etc}/ssl/ssl.crt (100%) rename {rel/files => etc}/ssl/ssl.key (100%) delete mode 100755 rel/files/emqttd delete mode 100644 rel/files/emqttd.cmd delete mode 100644 rel/files/emqttd.test.config delete mode 100644 rel/files/rewrite.config delete mode 100644 rel/reltool.config delete mode 100644 rel/reltool.config.script delete mode 100644 rel/vars.config delete mode 100644 src/emqttd_auth_ldap.erl diff --git a/etc/acl.conf b/etc/acl.conf new file mode 100644 index 000000000..c818c64f0 --- /dev/null +++ b/etc/acl.conf @@ -0,0 +1,28 @@ +%%-------------------------------------------------------------------- +%% +%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL) +%% +%% -type who() :: all | binary() | +%% {ipaddr, esockd_access:cidr()} | +%% {client, binary()} | +%% {user, binary()}. +%% +%% -type access() :: subscribe | publish | pubsub. +%% +%% -type topic() :: binary(). +%% +%% -type rule() :: {allow, all} | +%% {allow, who(), access(), list(topic())} | +%% {deny, all} | +%% {deny, who(), access(), list(topic())}. +%% +%%-------------------------------------------------------------------- + +{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. + +{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. + +{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. + +{allow, all}. + diff --git a/etc/client.conf b/etc/client.conf new file mode 100644 index 000000000..2c880c365 --- /dev/null +++ b/etc/client.conf @@ -0,0 +1,3 @@ +testclientid0 +testclientid1 127.0.0.1 +testclientid2 192.168.0.1/24 diff --git a/etc/emqttd.conf b/etc/emqttd.conf new file mode 100644 index 000000000..92a575795 --- /dev/null +++ b/etc/emqttd.conf @@ -0,0 +1,250 @@ + +%%-------------------------------------------------------------------- +%% Authentication +%%-------------------------------------------------------------------- + +%% Anonymous: Allow all +{auth, anonymous, []}. + +%% Authentication with username, password +%% Passwd Hash: plain | md5 | sha | sha256 +{auth, username, [{passwd, "etc/passwd.conf"}, {passwd_hash, plain}]}. + +%% Authentication with clientId +{auth, clientid, [{clients, "etc/client.config"}, {password, no}]}. + +%%-------------------------------------------------------------------- +%% ACL +%%-------------------------------------------------------------------- + +{acl, anonymous, []}. + +{acl, internal, [{config, "etc/acl.conf"}, {nomatch, allow}]}. + +%%-------------------------------------------------------------------- +%% Broker +%%-------------------------------------------------------------------- + +%% System interval of publishing broker $SYS messages +{broker_sys_interval, 60}. + +%%-------------------------------------------------------------------- +%% Retained message +%%-------------------------------------------------------------------- + +%% Expired after seconds, never expired if 0 +{retained_expired_after, 0}. + +%% Max number of retained messages +{retained_max_message_num, 100000}. + +%% Max Payload Size of retained message +{retained_max_playload_size, 65536}. + +%%-------------------------------------------------------------------- +%% MQTT Protocol +%%-------------------------------------------------------------------- + +%% Max ClientId Length Allowed. +{mqtt_max_clientid_len, 512}. + +%% Max Packet Size Allowed, 64K by default. +{mqtt_max_packet_size, 65536}. + +%% Socket Idle Timeout. +{mqtt_client_idle_timeout, 30}. % Seconds + +%%-------------------------------------------------------------------- +%% MQTT Session +%%-------------------------------------------------------------------- + +%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. +%% 0 means no limit +{session_max_inflight, 100}. + +%% Retry interval for redelivering QoS1/2 messages. +{session_unack_retry_interval, 60}. + +%% Awaiting PUBREL Timeout +{session_await_rel_timeout, 20}. + +%% Max Packets that Awaiting PUBREL, 0 means no limit +{session_max_awaiting_rel, 0}. + +%% Statistics Collection Interval(seconds) +{session_collect_interval, 0}. + +%% Expired after 2 day (unit: minute) +{session_expired_after, 2880}. + +%%-------------------------------------------------------------------- +%% Queue +%%-------------------------------------------------------------------- + +%% Type: simple | priority +{queue_type, simple}. + +%% Topic Priority: 0~255, Default is 0 +%% {queue_priority, [{"topic/1", 10}, {"topic/2", 8}]}. + +%% Max queue length. Enqueued messages when persistent client disconnected, +%% or inflight window is full. +{queue_max_length, infinity}. + +%% Low-water mark of queued messages +{queue_low_watermark, 0.2}. + +%% High-water mark of queued messages +{queue_high_watermark, 0.6}. + +%% Queue Qos0 messages? +{queue_qos0, true}. + +%%-------------------------------------------------------------------- +%% Listeners +%%-------------------------------------------------------------------- + +%% Plain MQTT +{listener, mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 16}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + + %% Socket Options + {sockopts, [ + %Set buffer if hight thoughtput + %{recbuf, 4096}, + %{sndbuf, 4096}, + %{buffer, 4096}, + %{nodelay, true}, + {backlog, 1024} + ]} +]}. + +%% MQTT SSL +{listener, mqtts, 8883, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% SSL certificate and key files + {ssl, [{certfile, "etc/ssl/ssl.crt"}, + {keyfile, "etc/ssl/ssl.key"}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} +]}. + +%% HTTP and WebSocket Listener +{listener, http, 8083, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 64}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} +]}. + +%%-------------------------------------------------------------------- +%% PubSub +%%-------------------------------------------------------------------- + +%% PubSub and Router. Default should be scheduler numbers. +{pubsub_pool_size, 8}. + +%% Route aging time(seconds) +{pubsub_routing_age, 5}. + +%%-------------------------------------------------------------------- +%% Bridge +%%-------------------------------------------------------------------- + +%% TODO: Bridge Queue Size +{bridge_max_queue_len, 10000}. + +%% Ping Interval of bridge node +{bridge_ping_down_interval, 1} %seconds + +%%------------------------------------------------------------------- +%% Plugins +%%------------------------------------------------------------------- + +%% Plugins Dir +{plugins_dir, "./plugins"}. + +%% File to store loaded plugin names. +{plugins_loaded_file, "./data/loaded_plugins"}. + +%%------------------------------------------------------------------- +%% Modules +%%------------------------------------------------------------------- + +%% Client presence management module. Publish presence messages when client connected or disconnected +{module, presence, [{qos, 0}]}. + +%% Subscribe topics automatically when client connected +{module, subscription, [{"$queue/clients/$c", 1}, backend]}. + +%% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) +{module, rewrite, [ + + %{topic, "x/#", [ + % {rewrite, "^x/y/(.+)$", "z/y/$1"}, + % {rewrite, "^x/(.+)$", "y/$1"} + %]}, + + %{topic, "y/+/z/#", [ + % {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} + %]} + +]}. + +%%------------------------------------------------------------------- +%% Erlang System Monitor +%%------------------------------------------------------------------- + +%% Long GC, don't monitor in production mode for: +%% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 + +{sysmon_long_gc, false}. + +%% Long Schedule(ms) +{sysmon_long_schedule, 240}. + +%% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. +%% 8 * 1024 * 1024 +{sysmon_large_heap, 8388608}. + +%% Busy Port +{sysmon_busy_port, false}. + +%% Busy Dist Port +{sysmon_busy_dist_port, true}. + diff --git a/etc/passwd.conf b/etc/passwd.conf new file mode 100644 index 000000000..e6998746d --- /dev/null +++ b/etc/passwd.conf @@ -0,0 +1,2 @@ +user1:passwd1 +user2:passwd2 diff --git a/rel/files/ssl/ssl.crt b/etc/ssl/ssl.crt similarity index 100% rename from rel/files/ssl/ssl.crt rename to etc/ssl/ssl.crt diff --git a/rel/files/ssl/ssl.key b/etc/ssl/ssl.key similarity index 100% rename from rel/files/ssl/ssl.key rename to etc/ssl/ssl.key diff --git a/rel/files/emqttd b/rel/files/emqttd deleted file mode 100755 index b78e68e25..000000000 --- a/rel/files/emqttd +++ /dev/null @@ -1,323 +0,0 @@ -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -# /bin/sh on Solaris is not a POSIX compatible shell, but /usr/bin/ksh is. -if [ `uname -s` = 'SunOS' -a "${POSIX_SHELL}" != "true" ]; then - POSIX_SHELL="true" - export POSIX_SHELL - # To support 'whoami' add /usr/ucb to path - PATH=/usr/ucb:$PATH - export PATH - exec /usr/bin/ksh $0 "$@" -fi -unset POSIX_SHELL # clear it so if we invoke other scripts, they run as ksh as well - -RUNNER_SCRIPT_DIR={{runner_script_dir}} -RUNNER_SCRIPT=${0##*/} - -RUNNER_BASE_DIR={{runner_base_dir}} -RUNNER_ETC_DIR={{runner_etc_dir}} -RUNNER_LIB_DIR={{platform_lib_dir}} -RUNNER_LOG_DIR={{runner_log_dir}} -RUNNER_DATA_DIR=$RUNNER_BASE_DIR/data -RUNNER_PLUGINS_DIR=$RUNNER_BASE_DIR/plugins - -# Note the trailing slash on $PIPE_DIR/ -PIPE_DIR={{pipe_dir}} -RUNNER_USER={{runner_user}} -PLATFORM_DATA_DIR={{platform_data_dir}} -SSL_DIST_CONFIG=$PLATFORM_DATA_DIR/ssl_distribution.args_file -RIAK_VERSION="git" - -WHOAMI=$(whoami) - -# Make sure this script is running as the appropriate user -if ([ "$RUNNER_USER" ] && [ "x$WHOAMI" != "x$RUNNER_USER" ]); then - type sudo > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "sudo doesn't appear to be installed and your EUID isn't $RUNNER_USER" 1>&2 - exit 1 - fi - echo "Attempting to restart script through sudo -H -u $RUNNER_USER" >&2 - exec sudo -H -u $RUNNER_USER -i $RUNNER_SCRIPT_DIR/$RUNNER_SCRIPT $@ -fi - -# Warn the user if ulimit -n is less than 1024 -ULIMIT_F=`ulimit -n` -if [ "$ULIMIT_F" -lt 1024 ]; then - echo "!!!!" - echo "!!!! WARNING: ulimit -n is ${ULIMIT_F}; 1024 is the recommended minimum." - echo "!!!!" -fi - -# Make sure CWD is set to runner base dir -cd $RUNNER_BASE_DIR - -# Make sure log directory exists -mkdir -p $RUNNER_LOG_DIR - -# Make sure the data directory exists -mkdir -p $PLATFORM_DATA_DIR - -# Warn the user if they don't have write permissions on the log dir -if [ ! -w $RUNNER_LOG_DIR ]; then - echo "!!!!" - echo "!!!! WARNING: $RUNNER_LOG_DIR not writable; logs and crash dumps unavailable." - echo "!!!!" -fi - -# Extract the target node name from node.args -NAME_ARG=`egrep '^\-s?name' $RUNNER_ETC_DIR/vm.args` -if [ -z "$NAME_ARG" ]; then - echo "vm.args needs to have either -name or -sname parameter." - exit 1 -fi -NODE_NAME=${NAME_ARG##* } - -# Extract the target cookie -COOKIE_ARG=`grep '^\-setcookie' $RUNNER_ETC_DIR/vm.args` -if [ -z "$COOKIE_ARG" ]; then - echo "vm.args needs to have a -setcookie parameter." - exit 1 -fi - -# Identify the script name -SCRIPT=`basename $0` - -# Parse out release and erts info -START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` -ERTS_VSN=${START_ERL% *} -APP_VSN=${START_ERL#* } - -# Add ERTS bin dir to our path -ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin - -# Setup command to control the node -NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" -NODETOOL_LITE="$ERTS_PATH/escript $ERTS_PATH/nodetool" - -# Common functions - -# Ping node without allowing nodetool to take stdin -ping_node() { - $NODETOOL ping < /dev/null -} - -# Set the PID global variable, return 1 on error -get_pid() { - PID=`$NODETOOL getpid < /dev/null` - ES=$? - if [ "$ES" -ne 0 ]; then - echo "Node is not running!" - return 1 - fi - - # don't allow empty or init pid's - if [ -z $PID ] || [ "$PID" -le 1 ]; then - return 1 - fi - - return 0 -} - - -# Scrape out SSL distribution config info from vm.args into $SSL_DIST_CONFIG -rm -f $SSL_DIST_CONFIG -sed -n '/Begin SSL distribution items/,/End SSL distribution items/p' \ - $RUNNER_ETC_DIR/vm.args > $SSL_DIST_CONFIG - -# Check the first argument for instructions -case "$1" in - start) - # Make sure there is not already a node running - RES=`ping_node` - if [ "$RES" = "pong" ]; then - echo "Node is already running!" - exit 1 - fi - # Sanity check the emqttd.config file - RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` - if [ $? != 0 ]; then - echo "Error reading $RUNNER_ETC_DIR/emqttd.config" - echo $RES - exit 1 - fi - HEART_COMMAND="$RUNNER_SCRIPT_DIR/$SCRIPT start" - export HEART_COMMAND - mkdir -p $PIPE_DIR - $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR \ - "exec $RUNNER_SCRIPT_DIR/$SCRIPT console" 2>&1 - - # Wait for the node to come up. We can't just ping it because - # distributed erlang comes up for a second before emqttd crashes - # (eg. in the case of an unwriteable disk). Once the node comes - # up we check for the node watcher process. If that's running - # then we assume things are good enough. This will at least let - # the user know when emqttd is crashing right after startup. - WAIT=${WAIT_FOR_ERLANG:-15} - while [ $WAIT -gt 0 ]; do - WAIT=`expr $WAIT - 1` - sleep 1 - RES=`ping_node` - if [ "$?" -ne 0 ]; then - continue - fi - echo "emqttd is started successfully!" - exit 0 - done - echo "emqttd failed to start within ${WAIT_FOR_ERLANG:-15} seconds," - echo "see the output of 'emqttd console' for more information." - echo "If you want to wait longer, set the environment variable" - echo "WAIT_FOR_ERLANG to the number of seconds to wait." - exit 1 - ;; - - stop) - UNAME_S=`uname -s` - case $UNAME_S in - Darwin) - # Make sure we explicitly set this because iTerm.app doesn't for - # some reason. - COMMAND_MODE=unix2003 - esac - - # Get the PID from nodetool - get_pid - GPR=$? - if [ "$GPR" -ne 0 ] || [ -z $PID ]; then - exit $GPR - fi - - # Tell nodetool to initiate a stop - $NODETOOL stop - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES - fi - - # Wait for the node to completely stop... - while `kill -s 0 $PID 2>/dev/null`; - do - sleep 1 - done - ;; - - restart) - ## Restart the VM without exiting the process - $NODETOOL restart - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES - fi - ;; - - reboot) - ## Restart the VM completely (uses heart to restart it) - $NODETOOL reboot - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES - fi - ;; - - ping) - ## See if the VM is alive - ping_node - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES - fi - ;; - - attach) - if [ "$2" = "-f" ]; then - echo "Forcing connection..." - else - # Make sure a node is running - RES=`ping_node` - ES=$? - if [ "$ES" -ne 0 ]; then - echo "Node is not running!" - exit $ES - fi - fi - - shift - exec $ERTS_PATH/to_erl $PIPE_DIR - ;; - - console) - RES=`ping_node` - if [ "$RES" = "pong" ]; then - echo "Node is already running - use '$SCRIPT attach' instead" - exit 1 - fi - # Sanity check the emqttd.config file - RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` - if [ $? != 0 ]; then - echo "Error reading $RUNNER_ETC_DIR/emqttd.config" - echo $RES - exit 1 - fi - # Setup beam-required vars - ROOTDIR=$RUNNER_BASE_DIR - ERL_LIBS=$ROOTDIR/plugins - BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin - EMU=beam - PROGNAME=`echo $0 | sed 's/.*\///'` - # Setup Mnesia Dir - MNESIA_DIR="$RUNNER_DATA_DIR/mnesia/$NODE_NAME" - CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$SCRIPT \ - -embedded -config $RUNNER_ETC_DIR/emqttd.config \ - -pa $RUNNER_LIB_DIR/basho-patches \ - -mnesia dir "\"${MNESIA_DIR}\"" \ - -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}" - export EMU - export ROOTDIR - export ERL_LIBS - export BINDIR - export PROGNAME - - # Dump environment info for logging purposes - echo "Exec: $CMD" - echo "Root: $ROOTDIR" - - # Log the startup - logger -t "$SCRIPT[$$]" "Starting up" - - # Start the VM - exec $CMD - ;; - chkconfig) - RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` - if [ $? != 0 ]; then - echo "Error reading $RUNNER_ETC_DIR/emqttd.config" - echo $RES - exit 1 - fi - echo "config is OK" - ;; - escript) - shift - $ERTS_PATH/escript "$@" - ;; - version) - echo $RIAK_VERSION - ;; - getpid) - # Get the PID from nodetool - get_pid - ES=$? - if [ "$ES" -ne 0 ] || [ -z $PID ]; then - exit $ES - fi - echo $PID - ;; - *) - echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|attach|chkconfig|escript|version|getpid}" - exit 1 - ;; -esac - -exit 0 diff --git a/rel/files/emqttd.cmd b/rel/files/emqttd.cmd deleted file mode 100644 index effa49536..000000000 --- a/rel/files/emqttd.cmd +++ /dev/null @@ -1,108 +0,0 @@ -@echo off -@setlocal -@setlocal enabledelayedexpansion - -@set node_name=emqttd - -@rem Get the absolute path to the parent directory, -@rem which is assumed to be the node root. -@for /F "delims=" %%I in ("%~dp0..") do @set node_root=%%~fI - -@set releases_dir=%node_root%\releases -@set runner_etc_dir=%node_root%\etc - -@rem Parse ERTS version and release version from start_erl.data -@for /F "usebackq tokens=1,2" %%I in ("%releases_dir%\start_erl.data") do @( - @call :set_trim erts_version %%I - @call :set_trim release_version %%J -) - -@set vm_args=%runner_etc_dir%\vm.args -@set sys_config=%runner_etc_dir%\emqttd.config -@set node_boot_script=%releases_dir%\%release_version%\%node_name% -@set clean_boot_script=%releases_dir%\%release_version%\start_clean - -@rem extract erlang cookie from vm.args -@for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie "%vm_args%"`) do @set erlang_cookie=%%J - -@set erts_bin=%node_root%\erts-%erts_version%\bin - -@set service_name=%node_name%_%release_version% - -@set erlsrv="%erts_bin%\erlsrv.exe" -@set epmd="%erts_bin%\epmd.exe" -@set escript="%erts_bin%\escript.exe" -@set werl="%erts_bin%\werl.exe" - -@if "%1"=="usage" @goto usage -@if "%1"=="install" @goto install -@if "%1"=="uninstall" @goto uninstall -@if "%1"=="start" @goto start -@if "%1"=="stop" @goto stop -@if "%1"=="restart" @call :stop && @goto start -@if "%1"=="console" @goto console -@if "%1"=="query" @goto query -@if "%1"=="attach" @goto attach -@if "%1"=="upgrade" @goto upgrade -@echo Unknown command: "%1" - -:usage -@echo Usage: %~n0 [install^|uninstall^|start^|stop^|restart^|console^|query^|attach^|upgrade] -@goto :EOF - -:install -@set description=Erlang node %node_name% in %node_root% -@set start_erl=%node_root%\bin\start_erl.cmd -@set args= ++ %node_name% ++ %node_root% -@%erlsrv% add %service_name% -c "%description%" -sname %node_name% -w "%node_root%" -m "%start_erl%" -args "%args%" -stopaction "init:stop()." -@goto :EOF - -:uninstall -@%erlsrv% remove %service_name% -@%epmd% -kill -@goto :EOF - -:start -@%erlsrv% start %service_name% -@goto :EOF - -:stop -@%erlsrv% stop %service_name% -@goto :EOF - -:console -set dest_path=%~dp0 -cd /d !dest_path!..\plugins -set current_path=%cd% -set plugins= -for /d %%P in (*) do ( -set "plugins=!plugins!"!current_path!\%%P\ebin" " -) -cd /d %node_root% - -@start "%node_name% console" %werl% -boot "%node_boot_script%" -config "%sys_config%" -args_file "%vm_args%" -sname %node_name% -pa %plugins% -@goto :EOF - -:query -@%erlsrv% list %service_name% -@exit %ERRORLEVEL% -@goto :EOF - -:attach -@for /f "usebackq" %%I in (`hostname`) do @set hostname=%%I -start "%node_name% attach" %werl% -boot "%clean_boot_script%" -remsh %node_name%@%hostname% -sname console -setcookie %erlang_cookie% -@goto :EOF - -:upgrade -@if "%2"=="" ( - @echo Missing upgrade package argument - @echo Usage: %~n0 upgrade {package base name} - @echo NOTE {package base name} MUST NOT include the .tar.gz suffix - @goto :EOF -) -@%escript% %node_root%\bin\install_upgrade.escript %node_name% %erlang_cookie% %2 -@goto :EOF - -:set_trim -@set %1=%2 -@goto :EOF diff --git a/rel/files/emqttd.test.config b/rel/files/emqttd.test.config deleted file mode 100644 index 48ad73252..000000000 --- a/rel/files/emqttd.test.config +++ /dev/null @@ -1,300 +0,0 @@ -% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -[{kernel, [ - {start_timer, true}, - {start_pg2, true} - ]}, - {sasl, [ - {sasl_error_logger, {file, "emqttd_sasl.log"}} - ]}, - {ssl, [ - %{versions, ['tlsv1.2', 'tlsv1.1']} - ]}, - {lager, [ - {colored, true}, - {async_threshold, 1000}, - {error_logger_redirect, false}, - {crash_log, "log/emqttd_crash.log"}, - {handlers, [ - {lager_console_backend, info}, - %%NOTICE: Level >= error - %%{lager_emqtt_backend, error}, - {lager_file_backend, [ - {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, - {file, "log/emqttd_info.log"}, - {level, info}, - {size, 104857600}, - {date, "$D0"}, - {count, 30} - ]}, - {lager_file_backend, [ - {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, - {file, "log/emqttd_error.log"}, - {level, error}, - {size, 104857600}, - {date, "$D0"}, - {count, 30} - ]} - ]} - ]}, - {esockd, [ - {logger, {lager, info}} - ]}, - {emqttd, [ - %% Authentication and Authorization - {access, [ - %% Authetication. Anonymous Default - {auth, [ - %% Authentication with username, password - %% {username, [{test, "password"}, {"test1", "password1"}]}, - - %% Authentication with clientid - %{clientid, [{password, no}, {file, "etc/clients.config"}]}, - - %% Authentication with LDAP - % {ldap, [ - % {servers, ["localhost"]}, - % {port, 389}, - % {timeout, 30}, - % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - % {ssl, fasle}, - % {sslopts, [ - % {"certfile", "ssl.crt"}, - % {"keyfile", "ssl.key"}]} - % ]}, - - %% Allow all - {anonymous, []} - ]}, - %% ACL config - {acl, [ - %% Internal ACL module - %% {internal, [{file, "testdata/test_acl.config"}, {nomatch, allow}]} - ]} - ]}, - %% MQTT Protocol Options - {mqtt, [ - %% Packet - {packet, [ - %% Max ClientId Length Allowed - {max_clientid_len, 1024}, - %% Max Packet Size Allowed, 64K default - {max_packet_size, 65536} - ]}, - %% Client - {client, [ - %% Socket is connected, but no 'CONNECT' packet received - {idle_timeout, 10} %% seconds - ]}, - %% Session - {session, [ - %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - %% 0 means no limit - {max_inflight, 100}, - - %% Retry interval for redelivering QoS1/2 messages. - {unack_retry_interval, 20}, - - %% Awaiting PUBREL Timeout - {await_rel_timeout, 20}, - - %% Max Packets that Awaiting PUBREL, 0 means no limit - {max_awaiting_rel, 0}, - - %% Statistics Collection Interval(seconds) - {collect_interval, 20}, - - %% Expired after 2 day (unit: minute) - {expired_after, 2880} - - ]}, - %% Queue - {queue, [ - %% simple | priority - {type, simple}, - - %% Topic Priority: 0~255, Default is 0 - %% {priority, [{"topic/1", 10}, {"topic/2", 8}]}, - - %% Max queue length. Enqueued messages when persistent client disconnected, - %% or inflight window is full. - {max_length, infinity}, - - %% Low-water mark of queued messages - {low_watermark, 0.2}, - - %% High-water mark of queued messages - {high_watermark, 0.6}, - - %% Queue Qos0 messages? - {queue_qos0, true} - ]} - ]}, - %% Broker Options - {broker, [ - %% System interval of publishing broker $SYS messages - {sys_interval, 60}, - - %% Retained messages - {retained, [ - %% Expired after seconds, never expired if 0 - {expired_after, 0}, - - %% Max number of retained messages - {max_message_num, 100000}, - - %% Max Payload Size of retained message - {max_playload_size, 65536} - ]}, - - %% PubSub and Router - {pubsub, [ - %% Default should be scheduler numbers - {pool_size, 8}, - - %% Route aging time(seconds) - {route_aging, 5} - ]}, - - %% Bridge - {bridge, [ - %%TODO: bridge queue size - {max_queue_len, 10000}, - - %% Ping Interval of bridge node - {ping_down_interval, 1} %seconds - ]} - ]}, - %% Modules - {modules, [ - %% Client presence management module. - %% Publish messages when client connected or disconnected - {presence, [{qos, 0}]}, - - %% Subscribe topics automatically when client connected - {subscription, [ - - %% $c will be replaced by clientid - %% {"$queue/clients/$c", 1}, - - %% Static subscriptions from backend - backend - ]} - - %% Rewrite rules - %% {rewrite, [{file, "etc/rewrite.config"}]} - ]}, - %% Plugins - {plugins, [ - %% Plugin App Library Dir - {plugins_dir, "./plugins"}, - - %% File to store loaded plugin names. - {loaded_file, "./data/loaded_plugins"} - ]}, - - %% Listeners - {listeners, [ - {mqtt, 1883, [ - %% Size of acceptor pool - {acceptors, 16}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% Connection Options - {connopts, [ - %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec - %% {rate_limit, "100,10"} %% 100K burst, 10K rate - ]}, - - %% Socket Options - {sockopts, [ - %Set buffer if hight thoughtput - %{recbuf, 4096}, - %{sndbuf, 4096}, - %{buffer, 4096}, - %{nodelay, true}, - {backlog, 512} - ]} - ]}, - - {mqtts, 8883, [ - %% Size of acceptor pool - {acceptors, 4}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% SSL certificate and key files - {ssl, [{certfile, "etc/ssl/ssl.crt"}, - {keyfile, "etc/ssl/ssl.key"}]}, - - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]}, - %% WebSocket over HTTPS Listener - %% {https, 8083, [ - %% %% Size of acceptor pool - %% {acceptors, 4}, - %% %% Maximum number of concurrent clients - %% {max_clients, 512}, - %% %% Socket Access Control - %% {access, [{allow, all}]}, - %% %% SSL certificate and key files - %% {ssl, [{certfile, "etc/ssl/ssl.crt"}, - %% {keyfile, "etc/ssl/ssl.key"}]}, - %% %% Socket Options - %% {sockopts, [ - %% %{buffer, 4096}, - %% {backlog, 1024} - %% ]} - %%]}, - - %% HTTP and WebSocket Listener - {http, 8083, [ - %% Size of acceptor pool - {acceptors, 4}, - %% Maximum number of concurrent clients - {max_clients, 64}, - %% Socket Access Control - {access, [{allow, all}]}, - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]} - ]}, - - %% Erlang System Monitor - {sysmon, [ - %% Long GC - {long_gc, 100}, - - %% Long Schedule(ms) - {long_schedule, 100}, - - %% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. - %% 8 * 1024 * 1024 - {large_heap, 8388608}, - - %% Busy Port - {busy_port, true}, - - %% Busy Dist Port - {busy_dist_port, true} - - ]} - ]} -]. - diff --git a/rel/files/rewrite.config b/rel/files/rewrite.config deleted file mode 100644 index 494a85f74..000000000 --- a/rel/files/rewrite.config +++ /dev/null @@ -1,14 +0,0 @@ -%%%----------------------------------------------------------------------------- -%% -%% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) -%% -%%%----------------------------------------------------------------------------- - -%{topic, "x/#", [ -% {rewrite, "^x/y/(.+)$", "z/y/$1"}, -% {rewrite, "^x/(.+)$", "y/$1"} -%]}. - -%{topic, "y/+/z/#", [ -% {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} -%]}. diff --git a/rel/reltool.config b/rel/reltool.config deleted file mode 100644 index c79fa74cd..000000000 --- a/rel/reltool.config +++ /dev/null @@ -1,98 +0,0 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -{sys, [ - {lib_dirs, ["../deps"]}, - {erts, [{mod_cond, derived}, {app_file, strip}]}, - {app_file, strip}, - {rel, "emqttd", git, - [ - kernel, - stdlib, - sasl, - asn1, - syntax_tools, - ssl, - crypto, - eldap, - xmerl, - os_mon, - inets, - goldrush, - compiler, - runtime_tools, - {observer, load}, - lager, - gen_logger, - gproc, - esockd, - mochiweb, - emqttd - ]}, - {rel, "start_clean", "", - [ - kernel, - stdlib - ]}, - {boot_rel, "emqttd"}, - {profile, embedded}, - {incl_cond, exclude}, - %{mod_cond, derived}, - {excl_archive_filters, [".*"]}, %% Do not archive built libs - {excl_sys_filters, ["^bin/(?!start_clean.boot)", - "^erts.*/bin/(dialyzer|typer)", - "^erts.*/(doc|info|include|lib|man|src)"]}, - {excl_app_filters, ["\.gitignore"]}, - {app, kernel, [{incl_cond, include}]}, - {app, stdlib, [{incl_cond, include}]}, - {app, sasl, [{incl_cond, include}]}, - {app, asn1, [{incl_cond, include}]}, - {app, crypto, [{incl_cond, include}]}, - {app, ssl, [{incl_cond, include}]}, - {app, xmerl, [{incl_cond, include}]}, - {app, os_mon, [{incl_cond, include}]}, - {app, syntax_tools, [{incl_cond, include}]}, - {app, public_key, [{incl_cond, include}]}, - {app, mnesia, [{incl_cond, include}]}, - {app, eldap, [{incl_cond, include}]}, - {app, inets, [{incl_cond, include}]}, - {app, compiler, [{incl_cond, include}]}, - {app, runtime_tools, [{incl_cond, include}]}, - {app, observer, [{incl_cond, include}]}, - {app, goldrush, [{incl_cond, include}]}, - {app, gen_logger, [{incl_cond, include}]}, - {app, lager, [{incl_cond, include}]}, - {app, gproc, [{incl_cond, include}]}, - {app, esockd, [{mod_cond, app}, {incl_cond, include}]}, - {app, mochiweb, [{mod_cond, app}, {incl_cond, include}]}, - {app, emqttd, [{mod_cond, app}, {incl_cond, include}, {lib_dir, ".."}]} - ]}. - -{target_dir, "emqttd"}. - -{overlay_vars, "vars.config"}. - -{overlay, [ - {mkdir, "log/"}, - {mkdir, "etc/"}, - {mkdir, "etc/ssl/"}, - {mkdir, "data/"}, - {mkdir, "data/mnesia"}, - {mkdir, "plugins/"}, - {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, - {template, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"}, - {template, "files/emqttd", "bin/emqttd"}, - {template, "files/emqttd_ctl", "bin/emqttd_ctl"}, - {template, "files/emqttd_top", "bin/emqttd_top"}, - {template, "files/emqttd.cmd", "bin/emqttd.cmd"}, - {copy, "files/start_erl.cmd", "bin/start_erl.cmd"}, - {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"}, - {copy, "files/ssl/ssl.crt", "etc/ssl/ssl.crt"}, - {copy, "files/ssl/ssl.key", "etc/ssl/ssl.key"}, - {template, "files/emqttd.config.production", "etc/emqttd.config"}, - {template, "files/emqttd.config.development", "etc/emqttd.config.development"}, - {template, "files/acl.config", "etc/acl.config"}, - {template, "files/rewrite.config", "etc/rewrite.config"}, - {template, "files/clients.config", "etc/clients.config"}, - {template, "files/vm.args", "etc/vm.args"}, - {copy, "files/loaded_plugins", "data/loaded_plugins"} - ]}. diff --git a/rel/reltool.config.script b/rel/reltool.config.script deleted file mode 100644 index fa6b571ac..000000000 --- a/rel/reltool.config.script +++ /dev/null @@ -1,18 +0,0 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et - -Sys = proplists:get_value(sys, CONFIG), -IncludeApps = [App || {app, App, _} <- Sys], - -[DepsDir] = proplists:get_value(lib_dirs, Sys), -DepApps = lists:map(fun(AppFile) -> - {ok, [{application, Name, Attrs}]} - = file:consult(filename:join(DepsDir, AppFile)), - Name - end, filelib:wildcard("*/ebin/*.app", DepsDir)), -AppendApps = DepApps -- IncludeApps, -Cond = [{mod_cond, app}, {incl_cond, include}], - -NewSys = lists:append(Sys, [{app, App, Cond} || App <- AppendApps]), - -lists:keyreplace(sys, 1, CONFIG, {sys, NewSys}). diff --git a/rel/vars.config b/rel/vars.config deleted file mode 100644 index 982940a28..000000000 --- a/rel/vars.config +++ /dev/null @@ -1,28 +0,0 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et - -%% Platform-specific installation paths -{platform_bin_dir, "./bin"}. -{platform_data_dir, "./var/data"}. -{platform_etc_dir, "./etc"}. -{platform_lib_dir, "./lib"}. -{platform_log_dir, "./log"}. - -%% -%% etc/emqttd.config -%% - - -%% -%% etc/vm.args -%% - -%% -%% bin/emqttd -%% -{runner_script_dir, "$(cd ${0%/*} && pwd)"}. -{runner_base_dir, "${RUNNER_SCRIPT_DIR%/*}"}. -{runner_etc_dir, "$RUNNER_BASE_DIR/etc"}. -{runner_log_dir, "$RUNNER_BASE_DIR/log"}. -{pipe_dir, "/tmp/$RUNNER_SCRIPT/"}. -{runner_user, ""}. diff --git a/src/emqttd_auth_ldap.erl b/src/emqttd_auth_ldap.erl deleted file mode 100644 index 11e1f27f3..000000000 --- a/src/emqttd_auth_ldap.erl +++ /dev/null @@ -1,77 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - -%% @doc LDAP Authentication Module --module(emqttd_auth_ldap). - --include("emqttd.hrl"). - --import(proplists, [get_value/2, get_value/3]). - --behaviour(emqttd_auth_mod). - --export([init/1, check/3, description/0]). - --record(state, {servers, user_dn, options}). - -init(Opts) -> - Servers = get_value(servers, Opts, ["localhost"]), - Port = get_value(port, Opts, 389), - Timeout = get_value(timeout, Opts, 30), - UserDn = get_value(user_dn, Opts), - LdapOpts = - case get_value(ssl, Opts, false) of - true -> - SslOpts = get_value(sslopts, Opts), - [{port, Port}, {timeout, Timeout}, {sslopts, SslOpts}]; - false -> - [{port, Port}, {timeout, Timeout}] - end, - {ok, #state{servers = Servers, user_dn = UserDn, options = LdapOpts}}. - -check(#mqtt_client{username = undefined}, _Password, _State) -> - {error, username_undefined}; -check(_Client, undefined, _State) -> - {error, password_undefined}; -check(_Client, <<>>, _State) -> - {error, password_undefined}; -check(#mqtt_client{username = Username}, Password, - #state{servers = Servers, user_dn = UserDn, options = Options}) -> - case eldap:open(Servers, Options) of - {ok, LDAP} -> - UserDn1 = fill(binary_to_list(Username), UserDn), - ldap_bind(LDAP, UserDn1, binary_to_list(Password)); - {error, Reason} -> - {error, Reason} - end. - -ldap_bind(LDAP, UserDn, Password) -> - case catch eldap:simple_bind(LDAP, UserDn, Password) of - ok -> - ok; - {error, invalidCredentials} -> - {error, invalid_credentials}; - {error, Error} -> - {error, Error}; - {'EXIT', Reason} -> - {error, Reason} - end. - -fill(Username, UserDn) -> - re:replace(UserDn, "\\$u", Username, [global, {return, list}]). - -description() -> "LDAP Authentication Module". - From 2e200f253fdc9f8620a0ddef5db9344c35fc2328 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 19 Jul 2016 17:46:05 +0800 Subject: [PATCH 016/116] 2.0 - erlang.mk to replace rebar --- Makefile | 64 +- bin/emqttd | 323 +++ bin/emqttd.cmd | 108 + {rel/files => bin}/emqttd_ctl | 0 {rel/files => bin}/emqttd_top | 0 {rel/files => bin}/install_upgrade.escript | 0 {rel/files => bin}/nodetool | 0 {rel/files => bin}/start_erl.cmd | 0 erlang.mk | 2741 ++++++++++++++++++++ etc/emqttd.conf | 4 +- rebar | Bin 182326 -> 0 bytes rebar.config | 49 - rel/files/acl.config | 28 - rel/files/clients.config | 3 - rel/files/emqttd.config.development | 303 --- rel/files/emqttd.config.production | 296 --- rel/files/erl | 34 - rel/files/loaded_plugins | 0 rel/sys.config | 38 + rel/{files => }/vm.args | 0 src/emqttd_app.erl | 1 + src/emqttd_sysmon_sup.erl | 14 +- 22 files changed, 3244 insertions(+), 762 deletions(-) create mode 100755 bin/emqttd create mode 100644 bin/emqttd.cmd rename {rel/files => bin}/emqttd_ctl (100%) rename {rel/files => bin}/emqttd_top (100%) rename {rel/files => bin}/install_upgrade.escript (100%) rename {rel/files => bin}/nodetool (100%) rename {rel/files => bin}/start_erl.cmd (100%) create mode 100644 erlang.mk delete mode 100755 rebar delete mode 100644 rebar.config delete mode 100644 rel/files/acl.config delete mode 100644 rel/files/clients.config delete mode 100644 rel/files/emqttd.config.development delete mode 100644 rel/files/emqttd.config.production delete mode 100755 rel/files/erl delete mode 100644 rel/files/loaded_plugins create mode 100644 rel/sys.config rename rel/{files => }/vm.args (100%) diff --git a/Makefile b/Makefile index 2e02db60e..77cdfdef1 100644 --- a/Makefile +++ b/Makefile @@ -1,54 +1,28 @@ -.PHONY: rel deps test plugins +PROJECT = emqttd +PROJECT_DESCRIPTION = Erlang MQTT Broker +PROJECT_VERSION = 2.0 -APP = emqttd -BASE_DIR = $(shell pwd) -REBAR = $(BASE_DIR)/rebar -DIST = $(BASE_DIR)/rel/$(APP) +DEPS = gproc lager gen_logger gen_conf esockd mochiweb -all: compile +dep_gproc = git https://github.com/uwiger/gproc.git +dep_lager = git https://github.com/basho/lager.git +dep_gen_conf = git https://github.com/emqtt/gen_conf.git +dep_gen_logger = git https://github.com/emqtt/gen_logger.git +dep_esockd = git https://github.com/emqtt/esockd.git udp +dep_mochiweb = git https://github.com/emqtt/mochiweb.git -# submods: -# @git submodule update --init +ERLC_OPTS += +'{parse_transform, lager_transform}' -compile: deps - @$(REBAR) compile +EUNIT_OPTS = verbose +EUNIT_ERL_OPTS = -args_file rel/vm.args -config rel/sys.config -deps: - @$(REBAR) get-deps +CT_SUITES = emqttd emqttd_access emqttd_backend emqttd_lib emqttd_mod emqttd_net \ + emqttd_mqueue emqttd_protocol emqttd_topic emqttd_trie +CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 -config rel/sys.config -update-deps: - @$(REBAR) update-deps +COVER = true -xref: - @$(REBAR) xref skip_deps=true +include erlang.mk -clean: - @$(REBAR) clean - -test: - ERL_FLAGS="-config rel/files/emqttd.test.config" $(REBAR) -v skip_deps=true ct - #$(REBAR) skip_deps=true eunit - -edoc: - @$(REBAR) doc - -rel: compile - @cd rel && $(REBAR) generate -f - -dist: rel - -PLT = $(BASE_DIR)/.emqttd_dialyzer.plt -APPS = erts kernel stdlib sasl crypto ssl os_mon syntax_tools \ - public_key mnesia inets compiler - -check_plt: compile - dialyzer --check_plt --plt $(PLT) --apps $(APPS) \ - deps/*/ebin ./ebin plugins/*/ebin - -build_plt: compile - dialyzer --build_plt --output_plt $(PLT) --apps $(APPS) \ - deps/*/ebin ./ebin plugins/*/ebin - -dialyzer: compile - dialyzer -Wno_return --plt $(PLT) deps/*/ebin ./ebin plugins/*/ebin +app:: rebar.config diff --git a/bin/emqttd b/bin/emqttd new file mode 100755 index 000000000..b78e68e25 --- /dev/null +++ b/bin/emqttd @@ -0,0 +1,323 @@ +#!/bin/sh +# -*- tab-width:4;indent-tabs-mode:nil -*- +# ex: ts=4 sw=4 et + +# /bin/sh on Solaris is not a POSIX compatible shell, but /usr/bin/ksh is. +if [ `uname -s` = 'SunOS' -a "${POSIX_SHELL}" != "true" ]; then + POSIX_SHELL="true" + export POSIX_SHELL + # To support 'whoami' add /usr/ucb to path + PATH=/usr/ucb:$PATH + export PATH + exec /usr/bin/ksh $0 "$@" +fi +unset POSIX_SHELL # clear it so if we invoke other scripts, they run as ksh as well + +RUNNER_SCRIPT_DIR={{runner_script_dir}} +RUNNER_SCRIPT=${0##*/} + +RUNNER_BASE_DIR={{runner_base_dir}} +RUNNER_ETC_DIR={{runner_etc_dir}} +RUNNER_LIB_DIR={{platform_lib_dir}} +RUNNER_LOG_DIR={{runner_log_dir}} +RUNNER_DATA_DIR=$RUNNER_BASE_DIR/data +RUNNER_PLUGINS_DIR=$RUNNER_BASE_DIR/plugins + +# Note the trailing slash on $PIPE_DIR/ +PIPE_DIR={{pipe_dir}} +RUNNER_USER={{runner_user}} +PLATFORM_DATA_DIR={{platform_data_dir}} +SSL_DIST_CONFIG=$PLATFORM_DATA_DIR/ssl_distribution.args_file +RIAK_VERSION="git" + +WHOAMI=$(whoami) + +# Make sure this script is running as the appropriate user +if ([ "$RUNNER_USER" ] && [ "x$WHOAMI" != "x$RUNNER_USER" ]); then + type sudo > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "sudo doesn't appear to be installed and your EUID isn't $RUNNER_USER" 1>&2 + exit 1 + fi + echo "Attempting to restart script through sudo -H -u $RUNNER_USER" >&2 + exec sudo -H -u $RUNNER_USER -i $RUNNER_SCRIPT_DIR/$RUNNER_SCRIPT $@ +fi + +# Warn the user if ulimit -n is less than 1024 +ULIMIT_F=`ulimit -n` +if [ "$ULIMIT_F" -lt 1024 ]; then + echo "!!!!" + echo "!!!! WARNING: ulimit -n is ${ULIMIT_F}; 1024 is the recommended minimum." + echo "!!!!" +fi + +# Make sure CWD is set to runner base dir +cd $RUNNER_BASE_DIR + +# Make sure log directory exists +mkdir -p $RUNNER_LOG_DIR + +# Make sure the data directory exists +mkdir -p $PLATFORM_DATA_DIR + +# Warn the user if they don't have write permissions on the log dir +if [ ! -w $RUNNER_LOG_DIR ]; then + echo "!!!!" + echo "!!!! WARNING: $RUNNER_LOG_DIR not writable; logs and crash dumps unavailable." + echo "!!!!" +fi + +# Extract the target node name from node.args +NAME_ARG=`egrep '^\-s?name' $RUNNER_ETC_DIR/vm.args` +if [ -z "$NAME_ARG" ]; then + echo "vm.args needs to have either -name or -sname parameter." + exit 1 +fi +NODE_NAME=${NAME_ARG##* } + +# Extract the target cookie +COOKIE_ARG=`grep '^\-setcookie' $RUNNER_ETC_DIR/vm.args` +if [ -z "$COOKIE_ARG" ]; then + echo "vm.args needs to have a -setcookie parameter." + exit 1 +fi + +# Identify the script name +SCRIPT=`basename $0` + +# Parse out release and erts info +START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` +ERTS_VSN=${START_ERL% *} +APP_VSN=${START_ERL#* } + +# Add ERTS bin dir to our path +ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin + +# Setup command to control the node +NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" +NODETOOL_LITE="$ERTS_PATH/escript $ERTS_PATH/nodetool" + +# Common functions + +# Ping node without allowing nodetool to take stdin +ping_node() { + $NODETOOL ping < /dev/null +} + +# Set the PID global variable, return 1 on error +get_pid() { + PID=`$NODETOOL getpid < /dev/null` + ES=$? + if [ "$ES" -ne 0 ]; then + echo "Node is not running!" + return 1 + fi + + # don't allow empty or init pid's + if [ -z $PID ] || [ "$PID" -le 1 ]; then + return 1 + fi + + return 0 +} + + +# Scrape out SSL distribution config info from vm.args into $SSL_DIST_CONFIG +rm -f $SSL_DIST_CONFIG +sed -n '/Begin SSL distribution items/,/End SSL distribution items/p' \ + $RUNNER_ETC_DIR/vm.args > $SSL_DIST_CONFIG + +# Check the first argument for instructions +case "$1" in + start) + # Make sure there is not already a node running + RES=`ping_node` + if [ "$RES" = "pong" ]; then + echo "Node is already running!" + exit 1 + fi + # Sanity check the emqttd.config file + RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` + if [ $? != 0 ]; then + echo "Error reading $RUNNER_ETC_DIR/emqttd.config" + echo $RES + exit 1 + fi + HEART_COMMAND="$RUNNER_SCRIPT_DIR/$SCRIPT start" + export HEART_COMMAND + mkdir -p $PIPE_DIR + $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR \ + "exec $RUNNER_SCRIPT_DIR/$SCRIPT console" 2>&1 + + # Wait for the node to come up. We can't just ping it because + # distributed erlang comes up for a second before emqttd crashes + # (eg. in the case of an unwriteable disk). Once the node comes + # up we check for the node watcher process. If that's running + # then we assume things are good enough. This will at least let + # the user know when emqttd is crashing right after startup. + WAIT=${WAIT_FOR_ERLANG:-15} + while [ $WAIT -gt 0 ]; do + WAIT=`expr $WAIT - 1` + sleep 1 + RES=`ping_node` + if [ "$?" -ne 0 ]; then + continue + fi + echo "emqttd is started successfully!" + exit 0 + done + echo "emqttd failed to start within ${WAIT_FOR_ERLANG:-15} seconds," + echo "see the output of 'emqttd console' for more information." + echo "If you want to wait longer, set the environment variable" + echo "WAIT_FOR_ERLANG to the number of seconds to wait." + exit 1 + ;; + + stop) + UNAME_S=`uname -s` + case $UNAME_S in + Darwin) + # Make sure we explicitly set this because iTerm.app doesn't for + # some reason. + COMMAND_MODE=unix2003 + esac + + # Get the PID from nodetool + get_pid + GPR=$? + if [ "$GPR" -ne 0 ] || [ -z $PID ]; then + exit $GPR + fi + + # Tell nodetool to initiate a stop + $NODETOOL stop + ES=$? + if [ "$ES" -ne 0 ]; then + exit $ES + fi + + # Wait for the node to completely stop... + while `kill -s 0 $PID 2>/dev/null`; + do + sleep 1 + done + ;; + + restart) + ## Restart the VM without exiting the process + $NODETOOL restart + ES=$? + if [ "$ES" -ne 0 ]; then + exit $ES + fi + ;; + + reboot) + ## Restart the VM completely (uses heart to restart it) + $NODETOOL reboot + ES=$? + if [ "$ES" -ne 0 ]; then + exit $ES + fi + ;; + + ping) + ## See if the VM is alive + ping_node + ES=$? + if [ "$ES" -ne 0 ]; then + exit $ES + fi + ;; + + attach) + if [ "$2" = "-f" ]; then + echo "Forcing connection..." + else + # Make sure a node is running + RES=`ping_node` + ES=$? + if [ "$ES" -ne 0 ]; then + echo "Node is not running!" + exit $ES + fi + fi + + shift + exec $ERTS_PATH/to_erl $PIPE_DIR + ;; + + console) + RES=`ping_node` + if [ "$RES" = "pong" ]; then + echo "Node is already running - use '$SCRIPT attach' instead" + exit 1 + fi + # Sanity check the emqttd.config file + RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` + if [ $? != 0 ]; then + echo "Error reading $RUNNER_ETC_DIR/emqttd.config" + echo $RES + exit 1 + fi + # Setup beam-required vars + ROOTDIR=$RUNNER_BASE_DIR + ERL_LIBS=$ROOTDIR/plugins + BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin + EMU=beam + PROGNAME=`echo $0 | sed 's/.*\///'` + # Setup Mnesia Dir + MNESIA_DIR="$RUNNER_DATA_DIR/mnesia/$NODE_NAME" + CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$SCRIPT \ + -embedded -config $RUNNER_ETC_DIR/emqttd.config \ + -pa $RUNNER_LIB_DIR/basho-patches \ + -mnesia dir "\"${MNESIA_DIR}\"" \ + -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}" + export EMU + export ROOTDIR + export ERL_LIBS + export BINDIR + export PROGNAME + + # Dump environment info for logging purposes + echo "Exec: $CMD" + echo "Root: $ROOTDIR" + + # Log the startup + logger -t "$SCRIPT[$$]" "Starting up" + + # Start the VM + exec $CMD + ;; + chkconfig) + RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` + if [ $? != 0 ]; then + echo "Error reading $RUNNER_ETC_DIR/emqttd.config" + echo $RES + exit 1 + fi + echo "config is OK" + ;; + escript) + shift + $ERTS_PATH/escript "$@" + ;; + version) + echo $RIAK_VERSION + ;; + getpid) + # Get the PID from nodetool + get_pid + ES=$? + if [ "$ES" -ne 0 ] || [ -z $PID ]; then + exit $ES + fi + echo $PID + ;; + *) + echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|attach|chkconfig|escript|version|getpid}" + exit 1 + ;; +esac + +exit 0 diff --git a/bin/emqttd.cmd b/bin/emqttd.cmd new file mode 100644 index 000000000..effa49536 --- /dev/null +++ b/bin/emqttd.cmd @@ -0,0 +1,108 @@ +@echo off +@setlocal +@setlocal enabledelayedexpansion + +@set node_name=emqttd + +@rem Get the absolute path to the parent directory, +@rem which is assumed to be the node root. +@for /F "delims=" %%I in ("%~dp0..") do @set node_root=%%~fI + +@set releases_dir=%node_root%\releases +@set runner_etc_dir=%node_root%\etc + +@rem Parse ERTS version and release version from start_erl.data +@for /F "usebackq tokens=1,2" %%I in ("%releases_dir%\start_erl.data") do @( + @call :set_trim erts_version %%I + @call :set_trim release_version %%J +) + +@set vm_args=%runner_etc_dir%\vm.args +@set sys_config=%runner_etc_dir%\emqttd.config +@set node_boot_script=%releases_dir%\%release_version%\%node_name% +@set clean_boot_script=%releases_dir%\%release_version%\start_clean + +@rem extract erlang cookie from vm.args +@for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie "%vm_args%"`) do @set erlang_cookie=%%J + +@set erts_bin=%node_root%\erts-%erts_version%\bin + +@set service_name=%node_name%_%release_version% + +@set erlsrv="%erts_bin%\erlsrv.exe" +@set epmd="%erts_bin%\epmd.exe" +@set escript="%erts_bin%\escript.exe" +@set werl="%erts_bin%\werl.exe" + +@if "%1"=="usage" @goto usage +@if "%1"=="install" @goto install +@if "%1"=="uninstall" @goto uninstall +@if "%1"=="start" @goto start +@if "%1"=="stop" @goto stop +@if "%1"=="restart" @call :stop && @goto start +@if "%1"=="console" @goto console +@if "%1"=="query" @goto query +@if "%1"=="attach" @goto attach +@if "%1"=="upgrade" @goto upgrade +@echo Unknown command: "%1" + +:usage +@echo Usage: %~n0 [install^|uninstall^|start^|stop^|restart^|console^|query^|attach^|upgrade] +@goto :EOF + +:install +@set description=Erlang node %node_name% in %node_root% +@set start_erl=%node_root%\bin\start_erl.cmd +@set args= ++ %node_name% ++ %node_root% +@%erlsrv% add %service_name% -c "%description%" -sname %node_name% -w "%node_root%" -m "%start_erl%" -args "%args%" -stopaction "init:stop()." +@goto :EOF + +:uninstall +@%erlsrv% remove %service_name% +@%epmd% -kill +@goto :EOF + +:start +@%erlsrv% start %service_name% +@goto :EOF + +:stop +@%erlsrv% stop %service_name% +@goto :EOF + +:console +set dest_path=%~dp0 +cd /d !dest_path!..\plugins +set current_path=%cd% +set plugins= +for /d %%P in (*) do ( +set "plugins=!plugins!"!current_path!\%%P\ebin" " +) +cd /d %node_root% + +@start "%node_name% console" %werl% -boot "%node_boot_script%" -config "%sys_config%" -args_file "%vm_args%" -sname %node_name% -pa %plugins% +@goto :EOF + +:query +@%erlsrv% list %service_name% +@exit %ERRORLEVEL% +@goto :EOF + +:attach +@for /f "usebackq" %%I in (`hostname`) do @set hostname=%%I +start "%node_name% attach" %werl% -boot "%clean_boot_script%" -remsh %node_name%@%hostname% -sname console -setcookie %erlang_cookie% +@goto :EOF + +:upgrade +@if "%2"=="" ( + @echo Missing upgrade package argument + @echo Usage: %~n0 upgrade {package base name} + @echo NOTE {package base name} MUST NOT include the .tar.gz suffix + @goto :EOF +) +@%escript% %node_root%\bin\install_upgrade.escript %node_name% %erlang_cookie% %2 +@goto :EOF + +:set_trim +@set %1=%2 +@goto :EOF diff --git a/rel/files/emqttd_ctl b/bin/emqttd_ctl similarity index 100% rename from rel/files/emqttd_ctl rename to bin/emqttd_ctl diff --git a/rel/files/emqttd_top b/bin/emqttd_top similarity index 100% rename from rel/files/emqttd_top rename to bin/emqttd_top diff --git a/rel/files/install_upgrade.escript b/bin/install_upgrade.escript similarity index 100% rename from rel/files/install_upgrade.escript rename to bin/install_upgrade.escript diff --git a/rel/files/nodetool b/bin/nodetool similarity index 100% rename from rel/files/nodetool rename to bin/nodetool diff --git a/rel/files/start_erl.cmd b/bin/start_erl.cmd similarity index 100% rename from rel/files/start_erl.cmd rename to bin/start_erl.cmd diff --git a/erlang.mk b/erlang.mk new file mode 100644 index 000000000..e348d4493 --- /dev/null +++ b/erlang.mk @@ -0,0 +1,2741 @@ +# Copyright (c) 2013-2015, Loïc Hoguin +# +# 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. + +.PHONY: all app apps deps search rel docs install-docs check tests clean distclean help erlang-mk + +ERLANG_MK_FILENAME := $(realpath $(lastword $(MAKEFILE_LIST))) + +ERLANG_MK_VERSION = 2.0.0-pre.2-130-gc6fe5ea + +# Core configuration. + +PROJECT ?= $(notdir $(CURDIR)) +PROJECT := $(strip $(PROJECT)) + +PROJECT_VERSION ?= rolling +PROJECT_MOD ?= $(PROJECT)_app + +# Verbosity. + +V ?= 0 + +verbose_0 = @ +verbose_2 = set -x; +verbose = $(verbose_$(V)) + +gen_verbose_0 = @echo " GEN " $@; +gen_verbose_2 = set -x; +gen_verbose = $(gen_verbose_$(V)) + +# Temporary files directory. + +ERLANG_MK_TMP ?= $(CURDIR)/.erlang.mk +export ERLANG_MK_TMP + +# "erl" command. + +ERL = erl +A0 -noinput -boot start_clean + +# Platform detection. + +ifeq ($(PLATFORM),) +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Linux) +PLATFORM = linux +else ifeq ($(UNAME_S),Darwin) +PLATFORM = darwin +else ifeq ($(UNAME_S),SunOS) +PLATFORM = solaris +else ifeq ($(UNAME_S),GNU) +PLATFORM = gnu +else ifeq ($(UNAME_S),FreeBSD) +PLATFORM = freebsd +else ifeq ($(UNAME_S),NetBSD) +PLATFORM = netbsd +else ifeq ($(UNAME_S),OpenBSD) +PLATFORM = openbsd +else ifeq ($(UNAME_S),DragonFly) +PLATFORM = dragonfly +else ifeq ($(shell uname -o),Msys) +PLATFORM = msys2 +else +$(error Unable to detect platform. Please open a ticket with the output of uname -a.) +endif + +export PLATFORM +endif + +# Core targets. + +all:: deps app rel + +# Noop to avoid a Make warning when there's nothing to do. +rel:: + $(verbose) : + +check:: tests + +clean:: clean-crashdump + +clean-crashdump: +ifneq ($(wildcard erl_crash.dump),) + $(gen_verbose) rm -f erl_crash.dump +endif + +distclean:: clean distclean-tmp + +distclean-tmp: + $(gen_verbose) rm -rf $(ERLANG_MK_TMP) + +help:: + $(verbose) printf "%s\n" \ + "erlang.mk (version $(ERLANG_MK_VERSION)) is distributed under the terms of the ISC License." \ + "Copyright (c) 2013-2015 Loïc Hoguin " \ + "" \ + "Usage: [V=1] $(MAKE) [target]..." \ + "" \ + "Core targets:" \ + " all Run deps, app and rel targets in that order" \ + " app Compile the project" \ + " deps Fetch dependencies (if needed) and compile them" \ + " search q=... Search for a package in the built-in index" \ + " rel Build a release for this project, if applicable" \ + " docs Build the documentation for this project" \ + " install-docs Install the man pages for this project" \ + " check Compile and run all tests and analysis for this project" \ + " tests Run the tests for this project" \ + " clean Delete temporary and output files from most targets" \ + " distclean Delete all temporary and output files" \ + " help Display this help and exit" \ + " erlang-mk Update erlang.mk to the latest version" + +# Core functions. + +empty := +space := $(empty) $(empty) +tab := $(empty) $(empty) +comma := , + +define newline + + +endef + +define comma_list +$(subst $(space),$(comma),$(strip $(1))) +endef + +# Adding erlang.mk to make Erlang scripts who call init:get_plain_arguments() happy. +define erlang +$(ERL) $(2) -pz $(ERLANG_MK_TMP)/rebar/ebin -eval "$(subst $(newline),,$(subst ",\",$(1)))" -- erlang.mk +endef + +ifeq ($(PLATFORM),msys2) +core_native_path = $(subst \,\\\\,$(shell cygpath -w $1)) +else +core_native_path = $1 +endif + +ifeq ($(shell which wget 2>/dev/null | wc -l), 1) +define core_http_get + wget --no-check-certificate -O $(1) $(2)|| rm $(1) +endef +else +define core_http_get.erl + ssl:start(), + inets:start(), + case httpc:request(get, {"$(2)", []}, [{autoredirect, true}], []) of + {ok, {{_, 200, _}, _, Body}} -> + case file:write_file("$(1)", Body) of + ok -> ok; + {error, R1} -> halt(R1) + end; + {error, R2} -> + halt(R2) + end, + halt(0). +endef + +define core_http_get + $(call erlang,$(call core_http_get.erl,$(call core_native_path,$1),$2)) +endef +endif + +core_eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) + +core_find = $(if $(wildcard $1),$(shell find $(1:%/=%) -type f -name $(subst *,\*,$2))) + +core_lc = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$(1))))))))))))))))))))))))))) + +core_ls = $(filter-out $(1),$(shell echo $(1))) + +# @todo Use a solution that does not require using perl. +core_relpath = $(shell perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $1 $2) + +# Automated update. + +ERLANG_MK_REPO ?= https://github.com/ninenines/erlang.mk +ERLANG_MK_COMMIT ?= +ERLANG_MK_BUILD_CONFIG ?= build.config +ERLANG_MK_BUILD_DIR ?= .erlang.mk.build + +erlang-mk: + git clone $(ERLANG_MK_REPO) $(ERLANG_MK_BUILD_DIR) +ifdef ERLANG_MK_COMMIT + cd $(ERLANG_MK_BUILD_DIR) && git checkout $(ERLANG_MK_COMMIT) +endif + if [ -f $(ERLANG_MK_BUILD_CONFIG) ]; then cp $(ERLANG_MK_BUILD_CONFIG) $(ERLANG_MK_BUILD_DIR)/build.config; fi + $(MAKE) -C $(ERLANG_MK_BUILD_DIR) + cp $(ERLANG_MK_BUILD_DIR)/erlang.mk ./erlang.mk + rm -rf $(ERLANG_MK_BUILD_DIR) + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: search + +define pkg_print + $(verbose) printf "%s\n" \ + $(if $(call core_eq,$(1),$(pkg_$(1)_name)),,"Pkg name: $(1)") \ + "App name: $(pkg_$(1)_name)" \ + "Description: $(pkg_$(1)_description)" \ + "Home page: $(pkg_$(1)_homepage)" \ + "Fetch with: $(pkg_$(1)_fetch)" \ + "Repository: $(pkg_$(1)_repo)" \ + "Commit: $(pkg_$(1)_commit)" \ + "" + +endef + +search: +ifdef q + $(foreach p,$(PACKAGES), \ + $(if $(findstring $(call core_lc,$(q)),$(call core_lc,$(pkg_$(p)_name) $(pkg_$(p)_description))), \ + $(call pkg_print,$(p)))) +else + $(foreach p,$(PACKAGES),$(call pkg_print,$(p))) +endif + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-deps + +# Configuration. + +ifdef OTP_DEPS +$(warning The variable OTP_DEPS is deprecated in favor of LOCAL_DEPS.) +endif + +IGNORE_DEPS ?= +export IGNORE_DEPS + +APPS_DIR ?= $(CURDIR)/apps +export APPS_DIR + +DEPS_DIR ?= $(CURDIR)/deps +export DEPS_DIR + +REBAR_DEPS_DIR = $(DEPS_DIR) +export REBAR_DEPS_DIR + +dep_name = $(if $(dep_$(1)),$(1),$(if $(pkg_$(1)_name),$(pkg_$(1)_name),$(1))) +dep_repo = $(patsubst git://github.com/%,https://github.com/%, \ + $(if $(dep_$(1)),$(word 2,$(dep_$(1))),$(pkg_$(1)_repo))) +dep_commit = $(if $(dep_$(1)_commit),$(dep_$(1)_commit),$(if $(dep_$(1)),$(word 3,$(dep_$(1))),$(pkg_$(1)_commit))) + +ALL_APPS_DIRS = $(if $(wildcard $(APPS_DIR)/),$(filter-out $(APPS_DIR),$(shell find $(APPS_DIR) -maxdepth 1 -type d))) +ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call dep_name,$(dep)))) + +ifeq ($(filter $(APPS_DIR) $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),) +ifeq ($(ERL_LIBS),) + ERL_LIBS = $(APPS_DIR):$(DEPS_DIR) +else + ERL_LIBS := $(ERL_LIBS):$(APPS_DIR):$(DEPS_DIR) +endif +endif +export ERL_LIBS + +export NO_AUTOPATCH + +# Verbosity. + +dep_verbose_0 = @echo " DEP " $(1); +dep_verbose_2 = set -x; +dep_verbose = $(dep_verbose_$(V)) + +# Core targets. + +ifdef IS_APP +apps:: +else +apps:: $(ALL_APPS_DIRS) +ifeq ($(IS_APP)$(IS_DEP),) + $(verbose) rm -f $(ERLANG_MK_TMP)/apps.log +endif + $(verbose) mkdir -p $(ERLANG_MK_TMP) +# Create ebin directory for all apps to make sure Erlang recognizes them +# as proper OTP applications when using -include_lib. This is a temporary +# fix, a proper fix would be to compile apps/* in the right order. + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + mkdir -p $$dep/ebin || exit $$?; \ + done + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/apps.log; then \ + :; \ + else \ + echo $$dep >> $(ERLANG_MK_TMP)/apps.log; \ + $(MAKE) -C $$dep IS_APP=1 || exit $$?; \ + fi \ + done +endif + +ifneq ($(SKIP_DEPS),) +deps:: +else +deps:: $(ALL_DEPS_DIRS) apps +ifeq ($(IS_APP)$(IS_DEP),) + $(verbose) rm -f $(ERLANG_MK_TMP)/deps.log +endif + $(verbose) mkdir -p $(ERLANG_MK_TMP) + $(verbose) for dep in $(ALL_DEPS_DIRS) ; do \ + if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ + :; \ + else \ + echo $$dep >> $(ERLANG_MK_TMP)/deps.log; \ + if [ -f $$dep/GNUmakefile ] || [ -f $$dep/makefile ] || [ -f $$dep/Makefile ]; then \ + $(MAKE) -C $$dep IS_DEP=1 || exit $$?; \ + else \ + echo "Error: No Makefile to build dependency $$dep."; \ + exit 2; \ + fi \ + fi \ + done +endif + +# Deps related targets. + +# @todo rename GNUmakefile and makefile into Makefile first, if they exist +# While Makefile file could be GNUmakefile or makefile, +# in practice only Makefile is needed so far. +define dep_autopatch + if [ -f $(DEPS_DIR)/$(1)/erlang.mk ]; then \ + $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ + $(call dep_autopatch_erlang_mk,$(1)); \ + elif [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ + if [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$(1)/Makefile` ]; then \ + $(call dep_autopatch2,$(1)); \ + elif [ 0 != `grep -ci rebar $(DEPS_DIR)/$(1)/Makefile` ]; then \ + $(call dep_autopatch2,$(1)); \ + elif [ -n "`find $(DEPS_DIR)/$(1)/ -type f -name \*.mk -not -name erlang.mk -exec grep -i rebar '{}' \;`" ]; then \ + $(call dep_autopatch2,$(1)); \ + else \ + $(call erlang,$(call dep_autopatch_app.erl,$(1))); \ + fi \ + else \ + if [ ! -d $(DEPS_DIR)/$(1)/src/ ]; then \ + $(call dep_autopatch_noop,$(1)); \ + else \ + $(call dep_autopatch2,$(1)); \ + fi \ + fi +endef + +define dep_autopatch2 + if [ -f $(DEPS_DIR)/$1/src/$1.app.src.script ]; then \ + $(call erlang,$(call dep_autopatch_appsrc_script.erl,$(1))); \ + fi; \ + $(call erlang,$(call dep_autopatch_appsrc.erl,$(1))); \ + if [ -f $(DEPS_DIR)/$(1)/rebar -o -f $(DEPS_DIR)/$(1)/rebar.config -o -f $(DEPS_DIR)/$(1)/rebar.config.script ]; then \ + $(call dep_autopatch_fetch_rebar); \ + $(call dep_autopatch_rebar,$(1)); \ + else \ + $(call dep_autopatch_gen,$(1)); \ + fi +endef + +define dep_autopatch_noop + printf "noop:\n" > $(DEPS_DIR)/$(1)/Makefile +endef + +# Overwrite erlang.mk with the current file by default. +ifeq ($(NO_AUTOPATCH_ERLANG_MK),) +define dep_autopatch_erlang_mk + echo "include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk" \ + > $(DEPS_DIR)/$1/erlang.mk +endef +else +define dep_autopatch_erlang_mk + : +endef +endif + +define dep_autopatch_gen + printf "%s\n" \ + "ERLC_OPTS = +debug_info" \ + "include ../../erlang.mk" > $(DEPS_DIR)/$(1)/Makefile +endef + +define dep_autopatch_fetch_rebar + mkdir -p $(ERLANG_MK_TMP); \ + if [ ! -d $(ERLANG_MK_TMP)/rebar ]; then \ + git clone -q -n -- https://github.com/rebar/rebar $(ERLANG_MK_TMP)/rebar; \ + cd $(ERLANG_MK_TMP)/rebar; \ + git checkout -q 791db716b5a3a7671e0b351f95ddf24b848ee173; \ + $(MAKE); \ + cd -; \ + fi +endef + +define dep_autopatch_rebar + if [ -f $(DEPS_DIR)/$(1)/Makefile ]; then \ + mv $(DEPS_DIR)/$(1)/Makefile $(DEPS_DIR)/$(1)/Makefile.orig.mk; \ + fi; \ + $(call erlang,$(call dep_autopatch_rebar.erl,$(1))); \ + rm -f $(DEPS_DIR)/$(1)/ebin/$(1).app +endef + +define dep_autopatch_rebar.erl + application:load(rebar), + application:set_env(rebar, log_level, debug), + Conf1 = case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config)") of + {ok, Conf0} -> Conf0; + _ -> [] + end, + {Conf, OsEnv} = fun() -> + case filelib:is_file("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)") of + false -> {Conf1, []}; + true -> + Bindings0 = erl_eval:new_bindings(), + Bindings1 = erl_eval:add_binding('CONFIG', Conf1, Bindings0), + Bindings = erl_eval:add_binding('SCRIPT', "$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings1), + Before = os:getenv(), + {ok, Conf2} = file:script("$(call core_native_path,$(DEPS_DIR)/$1/rebar.config.script)", Bindings), + {Conf2, lists:foldl(fun(E, Acc) -> lists:delete(E, Acc) end, os:getenv(), Before)} + end + end(), + Write = fun (Text) -> + file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) + end, + Escape = fun (Text) -> + re:replace(Text, "\\\\$$", "\$$$$", [global, {return, list}]) + end, + Write("IGNORE_DEPS += edown eper eunit_formatters meck node_package " + "rebar_lock_deps_plugin rebar_vsn_plugin reltool_util\n"), + Write("C_SRC_DIR = /path/do/not/exist\n"), + Write("C_SRC_TYPE = rebar\n"), + Write("DRV_CFLAGS = -fPIC\nexport DRV_CFLAGS\n"), + Write(["ERLANG_ARCH = ", rebar_utils:wordsize(), "\nexport ERLANG_ARCH\n"]), + fun() -> + Write("ERLC_OPTS = +debug_info\nexport ERLC_OPTS\n"), + case lists:keyfind(erl_opts, 1, Conf) of + false -> ok; + {_, ErlOpts} -> + lists:foreach(fun + ({d, D}) -> + Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); + ({i, I}) -> + Write(["ERLC_OPTS += -I ", I, "\n"]); + ({platform_define, Regex, D}) -> + case rebar_utils:is_arch(Regex) of + true -> Write("ERLC_OPTS += -D" ++ atom_to_list(D) ++ "=1\n"); + false -> ok + end; + ({parse_transform, PT}) -> + Write("ERLC_OPTS += +'{parse_transform, " ++ atom_to_list(PT) ++ "}'\n"); + (_) -> ok + end, ErlOpts) + end, + Write("\n") + end(), + fun() -> + File = case lists:keyfind(deps, 1, Conf) of + false -> []; + {_, Deps} -> + [begin case case Dep of + {N, S} when is_atom(N), is_list(S) -> {N, {hex, S}}; + {N, S} when is_tuple(S) -> {N, S}; + {N, _, S} -> {N, S}; + {N, _, S, _} -> {N, S}; + _ -> false + end of + false -> ok; + {Name, Source} -> + {Method, Repo, Commit} = case Source of + {hex, V} -> {hex, V, undefined}; + {git, R} -> {git, R, master}; + {M, R, {branch, C}} -> {M, R, C}; + {M, R, {ref, C}} -> {M, R, C}; + {M, R, {tag, C}} -> {M, R, C}; + {M, R, C} -> {M, R, C} + end, + Write(io_lib:format("DEPS += ~s\ndep_~s = ~s ~s ~s~n", [Name, Name, Method, Repo, Commit])) + end end || Dep <- Deps] + end + end(), + fun() -> + case lists:keyfind(erl_first_files, 1, Conf) of + false -> ok; + {_, Files} -> + Names = [[" ", case lists:reverse(F) of + "lre." ++ Elif -> lists:reverse(Elif); + Elif -> lists:reverse(Elif) + end] || "src/" ++ F <- Files], + Write(io_lib:format("COMPILE_FIRST +=~s\n", [Names])) + end + end(), + Write("\n\nrebar_dep: preprocess pre-deps deps pre-app app\n"), + Write("\npreprocess::\n"), + Write("\npre-deps::\n"), + Write("\npre-app::\n"), + PatchHook = fun(Cmd) -> + case Cmd of + "make -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); + "gmake -C" ++ Cmd1 -> "$$\(MAKE) -C" ++ Escape(Cmd1); + "make " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); + "gmake " ++ Cmd1 -> "$$\(MAKE) -f Makefile.orig.mk " ++ Escape(Cmd1); + _ -> Escape(Cmd) + end + end, + fun() -> + case lists:keyfind(pre_hooks, 1, Conf) of + false -> ok; + {_, Hooks} -> + [case H of + {'get-deps', Cmd} -> + Write("\npre-deps::\n\t" ++ PatchHook(Cmd) ++ "\n"); + {compile, Cmd} -> + Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); + {Regex, compile, Cmd} -> + case rebar_utils:is_arch(Regex) of + true -> Write("\npre-app::\n\tCC=$$\(CC) " ++ PatchHook(Cmd) ++ "\n"); + false -> ok + end; + _ -> ok + end || H <- Hooks] + end + end(), + ShellToMk = fun(V) -> + re:replace(re:replace(V, "(\\\\$$)(\\\\w*)", "\\\\1(\\\\2)", [global]), + "-Werror\\\\b", "", [{return, list}, global]) + end, + PortSpecs = fun() -> + case lists:keyfind(port_specs, 1, Conf) of + false -> + case filelib:is_dir("$(call core_native_path,$(DEPS_DIR)/$1/c_src)") of + false -> []; + true -> + [{"priv/" ++ proplists:get_value(so_name, Conf, "$(1)_drv.so"), + proplists:get_value(port_sources, Conf, ["c_src/*.c"]), []}] + end; + {_, Specs} -> + lists:flatten([case S of + {Output, Input} -> {ShellToMk(Output), Input, []}; + {Regex, Output, Input} -> + case rebar_utils:is_arch(Regex) of + true -> {ShellToMk(Output), Input, []}; + false -> [] + end; + {Regex, Output, Input, [{env, Env}]} -> + case rebar_utils:is_arch(Regex) of + true -> {ShellToMk(Output), Input, Env}; + false -> [] + end + end || S <- Specs]) + end + end(), + PortSpecWrite = fun (Text) -> + file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/c_src/Makefile.erlang.mk)", Text, [append]) + end, + case PortSpecs of + [] -> ok; + _ -> + Write("\npre-app::\n\t$$\(MAKE) -f c_src/Makefile.erlang.mk\n"), + PortSpecWrite(io_lib:format("ERL_CFLAGS = -finline-functions -Wall -fPIC -I \\"~s/erts-~s/include\\" -I \\"~s\\"\n", + [code:root_dir(), erlang:system_info(version), code:lib_dir(erl_interface, include)])), + PortSpecWrite(io_lib:format("ERL_LDFLAGS = -L \\"~s\\" -lerl_interface -lei\n", + [code:lib_dir(erl_interface, lib)])), + [PortSpecWrite(["\n", E, "\n"]) || E <- OsEnv], + FilterEnv = fun(Env) -> + lists:flatten([case E of + {_, _} -> E; + {Regex, K, V} -> + case rebar_utils:is_arch(Regex) of + true -> {K, V}; + false -> [] + end + end || E <- Env]) + end, + MergeEnv = fun(Env) -> + lists:foldl(fun ({K, V}, Acc) -> + case lists:keyfind(K, 1, Acc) of + false -> [{K, rebar_utils:expand_env_variable(V, K, "")}|Acc]; + {_, V0} -> [{K, rebar_utils:expand_env_variable(V, K, V0)}|Acc] + end + end, [], Env) + end, + PortEnv = case lists:keyfind(port_env, 1, Conf) of + false -> []; + {_, PortEnv0} -> FilterEnv(PortEnv0) + end, + PortSpec = fun ({Output, Input0, Env}) -> + filelib:ensure_dir("$(call core_native_path,$(DEPS_DIR)/$1/)" ++ Output), + Input = [[" ", I] || I <- Input0], + PortSpecWrite([ + [["\n", K, " = ", ShellToMk(V)] || {K, V} <- lists:reverse(MergeEnv(PortEnv))], + case $(PLATFORM) of + darwin -> "\n\nLDFLAGS += -flat_namespace -undefined suppress"; + _ -> "" + end, + "\n\nall:: ", Output, "\n\n", + "%.o: %.c\n\t$$\(CC) -c -o $$\@ $$\< $$\(CFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + "%.o: %.C\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + "%.o: %.cc\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + "%.o: %.cpp\n\t$$\(CXX) -c -o $$\@ $$\< $$\(CXXFLAGS) $$\(ERL_CFLAGS) $$\(DRV_CFLAGS) $$\(EXE_CFLAGS)\n\n", + [[Output, ": ", K, " = ", ShellToMk(V), "\n"] || {K, V} <- lists:reverse(MergeEnv(FilterEnv(Env)))], + Output, ": $$\(foreach ext,.c .C .cc .cpp,", + "$$\(patsubst %$$\(ext),%.o,$$\(filter %$$\(ext),$$\(wildcard", Input, "))))\n", + "\t$$\(CC) -o $$\@ $$\? $$\(LDFLAGS) $$\(ERL_LDFLAGS) $$\(DRV_LDFLAGS) $$\(EXE_LDFLAGS)", + case {filename:extension(Output), $(PLATFORM)} of + {[], _} -> "\n"; + {_, darwin} -> "\n"; + _ -> " -shared\n" + end]) + end, + [PortSpec(S) || S <- PortSpecs] + end, + Write("\ninclude $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(DEPS_DIR)/app)/erlang.mk"), + RunPlugin = fun(Plugin, Step) -> + case erlang:function_exported(Plugin, Step, 2) of + false -> ok; + true -> + c:cd("$(call core_native_path,$(DEPS_DIR)/$1/)"), + Ret = Plugin:Step({config, "", Conf, dict:new(), dict:new(), dict:new(), + dict:store(base_dir, "", dict:new())}, undefined), + io:format("rebar plugin ~p step ~p ret ~p~n", [Plugin, Step, Ret]) + end + end, + fun() -> + case lists:keyfind(plugins, 1, Conf) of + false -> ok; + {_, Plugins} -> + [begin + case lists:keyfind(deps, 1, Conf) of + false -> ok; + {_, Deps} -> + case lists:keyfind(P, 1, Deps) of + false -> ok; + _ -> + Path = "$(call core_native_path,$(DEPS_DIR)/)" ++ atom_to_list(P), + io:format("~s", [os:cmd("$(MAKE) -C $(call core_native_path,$(DEPS_DIR)/$1) " ++ Path)]), + io:format("~s", [os:cmd("$(MAKE) -C " ++ Path ++ " IS_DEP=1")]), + code:add_patha(Path ++ "/ebin") + end + end + end || P <- Plugins], + [case code:load_file(P) of + {module, P} -> ok; + _ -> + case lists:keyfind(plugin_dir, 1, Conf) of + false -> ok; + {_, PluginsDir} -> + ErlFile = "$(call core_native_path,$(DEPS_DIR)/$1/)" ++ PluginsDir ++ "/" ++ atom_to_list(P) ++ ".erl", + {ok, P, Bin} = compile:file(ErlFile, [binary]), + {module, P} = code:load_binary(P, ErlFile, Bin) + end + end || P <- Plugins], + [RunPlugin(P, preprocess) || P <- Plugins], + [RunPlugin(P, pre_compile) || P <- Plugins], + [RunPlugin(P, compile) || P <- Plugins] + end + end(), + halt() +endef + +define dep_autopatch_app.erl + UpdateModules = fun(App) -> + case filelib:is_regular(App) of + false -> ok; + true -> + {ok, [{application, '$(1)', L0}]} = file:consult(App), + Mods = filelib:fold_files("$(call core_native_path,$(DEPS_DIR)/$1/src)", "\\\\.erl$$", true, + fun (F, Acc) -> [list_to_atom(filename:rootname(filename:basename(F)))|Acc] end, []), + L = lists:keystore(modules, 1, L0, {modules, Mods}), + ok = file:write_file(App, io_lib:format("~p.~n", [{application, '$(1)', L}])) + end + end, + UpdateModules("$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"), + halt() +endef + +define dep_autopatch_appsrc_script.erl + AppSrc = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", + AppSrcScript = AppSrc ++ ".script", + Bindings = erl_eval:new_bindings(), + {ok, Conf} = file:script(AppSrcScript, Bindings), + ok = file:write_file(AppSrc, io_lib:format("~p.~n", [Conf])), + halt() +endef + +define dep_autopatch_appsrc.erl + AppSrcOut = "$(call core_native_path,$(DEPS_DIR)/$1/src/$1.app.src)", + AppSrcIn = case filelib:is_regular(AppSrcOut) of false -> "$(call core_native_path,$(DEPS_DIR)/$1/ebin/$1.app)"; true -> AppSrcOut end, + case filelib:is_regular(AppSrcIn) of + false -> ok; + true -> + {ok, [{application, $(1), L0}]} = file:consult(AppSrcIn), + L1 = lists:keystore(modules, 1, L0, {modules, []}), + L2 = case lists:keyfind(vsn, 1, L1) of {_, git} -> lists:keyreplace(vsn, 1, L1, {vsn, "git"}); _ -> L1 end, + L3 = case lists:keyfind(registered, 1, L2) of false -> [{registered, []}|L2]; _ -> L2 end, + ok = file:write_file(AppSrcOut, io_lib:format("~p.~n", [{application, $(1), L3}])), + case AppSrcOut of AppSrcIn -> ok; _ -> ok = file:delete(AppSrcIn) end + end, + halt() +endef + +define dep_fetch_git + git clone -q -n -- $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ + cd $(DEPS_DIR)/$(call dep_name,$(1)) && git checkout -q $(call dep_commit,$(1)); +endef + +define dep_fetch_git-submodule + git submodule update --init -- $(DEPS_DIR)/$1; +endef + +define dep_fetch_hg + hg clone -q -U $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); \ + cd $(DEPS_DIR)/$(call dep_name,$(1)) && hg update -q $(call dep_commit,$(1)); +endef + +define dep_fetch_svn + svn checkout -q $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); +endef + +define dep_fetch_cp + cp -R $(call dep_repo,$(1)) $(DEPS_DIR)/$(call dep_name,$(1)); +endef + +define dep_fetch_hex.erl + ssl:start(), + inets:start(), + {ok, {{_, 200, _}, _, Body}} = httpc:request(get, + {"https://s3.amazonaws.com/s3.hex.pm/tarballs/$(1)-$(2).tar", []}, + [], [{body_format, binary}]), + {ok, Files} = erl_tar:extract({binary, Body}, [memory]), + {_, Source} = lists:keyfind("contents.tar.gz", 1, Files), + ok = erl_tar:extract({binary, Source}, [{cwd, "$(call core_native_path,$(DEPS_DIR)/$1)"}, compressed]), + halt() +endef + +# Hex only has a package version. No need to look in the Erlang.mk packages. +define dep_fetch_hex + $(call erlang,$(call dep_fetch_hex.erl,$(1),$(strip $(word 2,$(dep_$(1)))))); +endef + +define dep_fetch_fail + echo "Error: Unknown or invalid dependency: $(1)." >&2; \ + exit 78; +endef + +# Kept for compatibility purposes with older Erlang.mk configuration. +define dep_fetch_legacy + $(warning WARNING: '$(1)' dependency configuration uses deprecated format.) \ + git clone -q -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1); \ + cd $(DEPS_DIR)/$(1) && git checkout -q $(if $(word 2,$(dep_$(1))),$(word 2,$(dep_$(1))),master); +endef + +define dep_fetch + $(if $(dep_$(1)), \ + $(if $(dep_fetch_$(word 1,$(dep_$(1)))), \ + $(word 1,$(dep_$(1))), \ + $(if $(IS_DEP),legacy,fail)), \ + $(if $(filter $(1),$(PACKAGES)), \ + $(pkg_$(1)_fetch), \ + fail)) +endef + +define dep_target +$(DEPS_DIR)/$(call dep_name,$1): + $(eval DEP_NAME := $(call dep_name,$1)) + $(eval DEP_STR := $(if $(filter-out $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) + $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ + echo "Error: Dependency" $(DEP_STR) "conflicts with application found in $(APPS_DIR)/$(DEP_NAME)."; \ + exit 17; \ + fi + $(verbose) mkdir -p $(DEPS_DIR) + $(dep_verbose) $(call dep_fetch_$(strip $(call dep_fetch,$(1))),$(1)) + $(verbose) if [ -f $(DEPS_DIR)/$(1)/configure.ac -o -f $(DEPS_DIR)/$(1)/configure.in ] \ + && [ ! -f $(DEPS_DIR)/$(1)/configure ]; then \ + echo " AUTO " $(1); \ + cd $(DEPS_DIR)/$(1) && autoreconf -Wall -vif -I m4; \ + fi + - $(verbose) if [ -f $(DEPS_DIR)/$(DEP_NAME)/configure ]; then \ + echo " CONF " $(DEP_STR); \ + cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \ + fi +ifeq ($(filter $(1),$(NO_AUTOPATCH)),) + $(verbose) if [ "$(1)" = "amqp_client" -a "$(RABBITMQ_CLIENT_PATCH)" ]; then \ + if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \ + echo " PATCH Downloading rabbitmq-codegen"; \ + git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ + fi; \ + if [ ! -d $(DEPS_DIR)/rabbitmq-server ]; then \ + echo " PATCH Downloading rabbitmq-server"; \ + git clone https://github.com/rabbitmq/rabbitmq-server.git $(DEPS_DIR)/rabbitmq-server; \ + fi; \ + ln -s $(DEPS_DIR)/amqp_client/deps/rabbit_common-0.0.0 $(DEPS_DIR)/rabbit_common; \ + elif [ "$(1)" = "rabbit" -a "$(RABBITMQ_SERVER_PATCH)" ]; then \ + if [ ! -d $(DEPS_DIR)/rabbitmq-codegen ]; then \ + echo " PATCH Downloading rabbitmq-codegen"; \ + git clone https://github.com/rabbitmq/rabbitmq-codegen.git $(DEPS_DIR)/rabbitmq-codegen; \ + fi \ + else \ + $$(call dep_autopatch,$(DEP_NAME)) \ + fi +endif +endef + +$(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep)))) + +ifndef IS_APP +clean:: clean-apps + +clean-apps: + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep clean IS_APP=1 || exit $$?; \ + done + +distclean:: distclean-apps + +distclean-apps: + $(verbose) for dep in $(ALL_APPS_DIRS) ; do \ + $(MAKE) -C $$dep distclean IS_APP=1 || exit $$?; \ + done +endif + +ifndef SKIP_DEPS +distclean:: distclean-deps + +distclean-deps: + $(gen_verbose) rm -rf $(DEPS_DIR) +endif + +# External plugins. + +DEP_PLUGINS ?= + +define core_dep_plugin +-include $(DEPS_DIR)/$(1) + +$(DEPS_DIR)/$(1): $(DEPS_DIR)/$(2) ; +endef + +$(foreach p,$(DEP_PLUGINS),\ + $(eval $(if $(findstring /,$p),\ + $(call core_dep_plugin,$p,$(firstword $(subst /, ,$p))),\ + $(call core_dep_plugin,$p/plugins.mk,$p)))) + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# Configuration. + +DTL_FULL_PATH ?= +DTL_PATH ?= templates/ +DTL_SUFFIX ?= _dtl +DTL_OPTS ?= + +# Verbosity. + +dtl_verbose_0 = @echo " DTL " $(filter %.dtl,$(?F)); +dtl_verbose = $(dtl_verbose_$(V)) + +# Core targets. + +DTL_FILES = $(sort $(call core_find,$(DTL_PATH),*.dtl)) + +ifneq ($(DTL_FILES),) + +ifdef DTL_FULL_PATH +BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(subst /,_,$(DTL_FILES:$(DTL_PATH)%=%)))) +else +BEAM_FILES += $(addprefix ebin/,$(patsubst %.dtl,%_dtl.beam,$(notdir $(DTL_FILES)))) +endif + +# Rebuild templates when the Makefile changes. +$(DTL_FILES): $(MAKEFILE_LIST) + @touch $@ + +define erlydtl_compile.erl + [begin + Module0 = case "$(strip $(DTL_FULL_PATH))" of + "" -> + filename:basename(F, ".dtl"); + _ -> + "$(DTL_PATH)" ++ F2 = filename:rootname(F, ".dtl"), + re:replace(F2, "/", "_", [{return, list}, global]) + end, + Module = list_to_atom(string:to_lower(Module0) ++ "$(DTL_SUFFIX)"), + case erlydtl:compile(F, Module, [$(DTL_OPTS)] ++ [{out_dir, "ebin/"}, return_errors, {doc_root, "templates"}]) of + ok -> ok; + {ok, _} -> ok + end + end || F <- string:tokens("$(1)", " ")], + halt(). +endef + +ebin/$(PROJECT).app:: $(DTL_FILES) | ebin/ + $(if $(strip $?),\ + $(dtl_verbose) $(call erlang,$(call erlydtl_compile.erl,$?),-pa ebin/ $(DEPS_DIR)/erlydtl/ebin/)) + +endif + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +# Verbosity. + +proto_verbose_0 = @echo " PROTO " $(filter %.proto,$(?F)); +proto_verbose = $(proto_verbose_$(V)) + +# Core targets. + +define compile_proto + $(verbose) mkdir -p ebin/ include/ + $(proto_verbose) $(call erlang,$(call compile_proto.erl,$(1))) + $(proto_verbose) erlc +debug_info -o ebin/ ebin/*.erl + $(verbose) rm ebin/*.erl +endef + +define compile_proto.erl + [begin + Dir = filename:dirname(filename:dirname(F)), + protobuffs_compile:generate_source(F, + [{output_include_dir, Dir ++ "/include"}, + {output_src_dir, Dir ++ "/ebin"}]) + end || F <- string:tokens("$(1)", " ")], + halt(). +endef + +ifneq ($(wildcard src/),) +ebin/$(PROJECT).app:: $(sort $(call core_find,src/,*.proto)) + $(if $(strip $?),$(call compile_proto,$?)) +endif + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: clean-app + +# Configuration. + +ERLC_OPTS ?= -Werror +debug_info +warn_export_vars +warn_shadow_vars \ + +warn_obsolete_guard # +bin_opt_info +warn_export_all +warn_missing_spec +COMPILE_FIRST ?= +COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) +ERLC_EXCLUDE ?= +ERLC_EXCLUDE_PATHS = $(addprefix src/,$(addsuffix .erl,$(ERLC_EXCLUDE))) + +ERLC_MIB_OPTS ?= +COMPILE_MIB_FIRST ?= +COMPILE_MIB_FIRST_PATHS = $(addprefix mibs/,$(addsuffix .mib,$(COMPILE_MIB_FIRST))) + +# Verbosity. + +app_verbose_0 = @echo " APP " $(PROJECT); +app_verbose_2 = set -x; +app_verbose = $(app_verbose_$(V)) + +appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; +appsrc_verbose_2 = set -x; +appsrc_verbose = $(appsrc_verbose_$(V)) + +makedep_verbose_0 = @echo " DEPEND" $(PROJECT).d; +makedep_verbose_2 = set -x; +makedep_verbose = $(makedep_verbose_$(V)) + +erlc_verbose_0 = @echo " ERLC " $(filter-out $(patsubst %,%.erl,$(ERLC_EXCLUDE)),\ + $(filter %.erl %.core,$(?F))); +erlc_verbose_2 = set -x; +erlc_verbose = $(erlc_verbose_$(V)) + +xyrl_verbose_0 = @echo " XYRL " $(filter %.xrl %.yrl,$(?F)); +xyrl_verbose_2 = set -x; +xyrl_verbose = $(xyrl_verbose_$(V)) + +asn1_verbose_0 = @echo " ASN1 " $(filter %.asn1,$(?F)); +asn1_verbose_2 = set -x; +asn1_verbose = $(asn1_verbose_$(V)) + +mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); +mib_verbose_2 = set -x; +mib_verbose = $(mib_verbose_$(V)) + +ifneq ($(wildcard src/),) + +# Targets. + +ifeq ($(wildcard ebin/test),) +app:: deps $(PROJECT).d + $(verbose) $(MAKE) --no-print-directory app-build +else +app:: clean deps $(PROJECT).d + $(verbose) $(MAKE) --no-print-directory app-build +endif + +ifeq ($(wildcard src/$(PROJECT_MOD).erl),) +define app_file +{application, $(PROJECT), [ + {description, "$(PROJECT_DESCRIPTION)"}, + {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), + {id$(comma)$(space)"$(1)"}$(comma)) + {modules, [$(call comma_list,$(2))]}, + {registered, []}, + {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]} +]}. +endef +else +define app_file +{application, $(PROJECT), [ + {description, "$(PROJECT_DESCRIPTION)"}, + {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), + {id$(comma)$(space)"$(1)"}$(comma)) + {modules, [$(call comma_list,$(2))]}, + {registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]}, + {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS))]}, + {mod, {$(PROJECT_MOD), []}} +]}. +endef +endif + +app-build: ebin/$(PROJECT).app + $(verbose) : + +# Source files. + +ERL_FILES = $(sort $(call core_find,src/,*.erl)) +CORE_FILES = $(sort $(call core_find,src/,*.core)) + +# ASN.1 files. + +ifneq ($(wildcard asn1/),) +ASN1_FILES = $(sort $(call core_find,asn1/,*.asn1)) +ERL_FILES += $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) + +define compile_asn1 + $(verbose) mkdir -p include/ + $(asn1_verbose) erlc -v -I include/ -o asn1/ +noobj $(1) + $(verbose) mv asn1/*.erl src/ + $(verbose) mv asn1/*.hrl include/ + $(verbose) mv asn1/*.asn1db include/ +endef + +$(PROJECT).d:: $(ASN1_FILES) + $(if $(strip $?),$(call compile_asn1,$?)) +endif + +# SNMP MIB files. + +ifneq ($(wildcard mibs/),) +MIB_FILES = $(sort $(call core_find,mibs/,*.mib)) + +$(PROJECT).d:: $(COMPILE_MIB_FIRST_PATHS) $(MIB_FILES) + $(verbose) mkdir -p include/ priv/mibs/ + $(mib_verbose) erlc -v $(ERLC_MIB_OPTS) -o priv/mibs/ -I priv/mibs/ $? + $(mib_verbose) erlc -o include/ -- $(addprefix priv/mibs/,$(patsubst %.mib,%.bin,$(notdir $?))) +endif + +# Leex and Yecc files. + +XRL_FILES = $(sort $(call core_find,src/,*.xrl)) +XRL_ERL_FILES = $(addprefix src/,$(patsubst %.xrl,%.erl,$(notdir $(XRL_FILES)))) +ERL_FILES += $(XRL_ERL_FILES) + +YRL_FILES = $(sort $(call core_find,src/,*.yrl)) +YRL_ERL_FILES = $(addprefix src/,$(patsubst %.yrl,%.erl,$(notdir $(YRL_FILES)))) +ERL_FILES += $(YRL_ERL_FILES) + +$(PROJECT).d:: $(XRL_FILES) $(YRL_FILES) + $(if $(strip $?),$(xyrl_verbose) erlc -v -o src/ $?) + +# Erlang and Core Erlang files. + +define makedep.erl + E = ets:new(makedep, [bag]), + G = digraph:new([acyclic]), + ErlFiles = lists:usort(string:tokens("$(ERL_FILES)", " ")), + Modules = [{list_to_atom(filename:basename(F, ".erl")), F} || F <- ErlFiles], + Add = fun (Mod, Dep) -> + case lists:keyfind(Dep, 1, Modules) of + false -> ok; + {_, DepFile} -> + {_, ModFile} = lists:keyfind(Mod, 1, Modules), + ets:insert(E, {ModFile, DepFile}), + digraph:add_vertex(G, Mod), + digraph:add_vertex(G, Dep), + digraph:add_edge(G, Mod, Dep) + end + end, + AddHd = fun (F, Mod, DepFile) -> + case file:open(DepFile, [read]) of + {error, enoent} -> ok; + {ok, Fd} -> + F(F, Fd, Mod), + {_, ModFile} = lists:keyfind(Mod, 1, Modules), + ets:insert(E, {ModFile, DepFile}) + end + end, + Attr = fun + (F, Mod, behavior, Dep) -> Add(Mod, Dep); + (F, Mod, behaviour, Dep) -> Add(Mod, Dep); + (F, Mod, compile, {parse_transform, Dep}) -> Add(Mod, Dep); + (F, Mod, compile, Opts) when is_list(Opts) -> + case proplists:get_value(parse_transform, Opts) of + undefined -> ok; + Dep -> Add(Mod, Dep) + end; + (F, Mod, include, Hrl) -> + case filelib:is_file("include/" ++ Hrl) of + true -> AddHd(F, Mod, "include/" ++ Hrl); + false -> + case filelib:is_file("src/" ++ Hrl) of + true -> AddHd(F, Mod, "src/" ++ Hrl); + false -> false + end + end; + (F, Mod, include_lib, "$1/include/" ++ Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); + (F, Mod, include_lib, Hrl) -> AddHd(F, Mod, "include/" ++ Hrl); + (F, Mod, import, {Imp, _}) -> + case filelib:is_file("src/" ++ atom_to_list(Imp) ++ ".erl") of + false -> ok; + true -> Add(Mod, Imp) + end; + (_, _, _, _) -> ok + end, + MakeDepend = fun(F, Fd, Mod) -> + case io:parse_erl_form(Fd, undefined) of + {ok, {attribute, _, Key, Value}, _} -> + Attr(F, Mod, Key, Value), + F(F, Fd, Mod); + {eof, _} -> + file:close(Fd); + _ -> + F(F, Fd, Mod) + end + end, + [begin + Mod = list_to_atom(filename:basename(F, ".erl")), + {ok, Fd} = file:open(F, [read]), + MakeDepend(MakeDepend, Fd, Mod) + end || F <- ErlFiles], + Depend = sofs:to_external(sofs:relation_to_family(sofs:relation(ets:tab2list(E)))), + CompileFirst = [X || X <- lists:reverse(digraph_utils:topsort(G)), [] =/= digraph:in_neighbours(G, X)], + ok = file:write_file("$(1)", [ + [[F, "::", [[" ", D] || D <- Deps], "; @touch \$$@\n"] || {F, Deps} <- Depend], + "\nCOMPILE_FIRST +=", [[" ", atom_to_list(CF)] || CF <- CompileFirst], "\n" + ]), + halt() +endef + +ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) +$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) + $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) +endif + +# Rebuild everything when the Makefile changes. +$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(MAKEFILE_LIST) + @touch $@ + +-include $(PROJECT).d + +ebin/$(PROJECT).app:: ebin/ + +ebin/: + $(verbose) mkdir -p ebin/ + +define compile_erl + $(erlc_verbose) erlc -v $(if $(IS_DEP),$(filter-out -Werror,$(ERLC_OPTS)),$(ERLC_OPTS)) -o ebin/ \ + -pa ebin/ -I include/ $(filter-out $(ERLC_EXCLUDE_PATHS),$(COMPILE_FIRST_PATHS) $(1)) +endef + +ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) + $(eval FILES_TO_COMPILE := $(filter-out src/$(PROJECT).app.src,$?)) + $(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE))) + $(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null || true)) + $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ + $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) +ifeq ($(wildcard src/$(PROJECT).app.src),) + $(app_verbose) printf "$(subst $(newline),\n,$(subst ",\",$(call app_file,$(GITDESCRIBE),$(MODULES))))" \ + > ebin/$(PROJECT).app +else + $(verbose) if [ -z "$$(grep -e '^[^%]*{\s*modules\s*,' src/$(PROJECT).app.src)" ]; then \ + echo "Empty modules entry not found in $(PROJECT).app.src. Please consult the erlang.mk README for instructions." >&2; \ + exit 1; \ + fi + $(appsrc_verbose) cat src/$(PROJECT).app.src \ + | sed "s/{[[:space:]]*modules[[:space:]]*,[[:space:]]*\[\]}/{modules, \[$(call comma_list,$(MODULES))\]}/" \ + | sed "s/{id,[[:space:]]*\"git\"}/{id, \"$(subst /,\/,$(GITDESCRIBE))\"}/" \ + > ebin/$(PROJECT).app +endif + +clean:: clean-app + +clean-app: + $(gen_verbose) rm -rf $(PROJECT).d ebin/ priv/mibs/ $(XRL_ERL_FILES) $(YRL_ERL_FILES) \ + $(addprefix include/,$(patsubst %.mib,%.hrl,$(notdir $(MIB_FILES)))) \ + $(addprefix include/,$(patsubst %.asn1,%.hrl,$(notdir $(ASN1_FILES)))) \ + $(addprefix include/,$(patsubst %.asn1,%.asn1db,$(notdir $(ASN1_FILES)))) \ + $(addprefix src/,$(patsubst %.asn1,%.erl,$(notdir $(ASN1_FILES)))) + +endif + +# Copyright (c) 2015, Viktor Söderqvist +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: docs-deps + +# Configuration. + +ALL_DOC_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DOC_DEPS)) + +# Targets. + +$(foreach dep,$(DOC_DEPS),$(eval $(call dep_target,$(dep)))) + +ifneq ($(SKIP_DEPS),) +doc-deps: +else +doc-deps: $(ALL_DOC_DEPS_DIRS) + $(verbose) for dep in $(ALL_DOC_DEPS_DIRS) ; do $(MAKE) -C $$dep; done +endif + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: rel-deps + +# Configuration. + +ALL_REL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(REL_DEPS)) + +# Targets. + +$(foreach dep,$(REL_DEPS),$(eval $(call dep_target,$(dep)))) + +ifneq ($(SKIP_DEPS),) +rel-deps: +else +rel-deps: $(ALL_REL_DEPS_DIRS) + $(verbose) for dep in $(ALL_REL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done +endif + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: test-deps test-dir test-build clean-test-dir + +# Configuration. + +TEST_DIR ?= $(CURDIR)/test + +ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) + +TEST_ERLC_OPTS ?= +debug_info +warn_export_vars +warn_shadow_vars +warn_obsolete_guard +TEST_ERLC_OPTS += -DTEST=1 + +# Targets. + +$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) + +ifneq ($(SKIP_DEPS),) +test-deps: +else +test-deps: $(ALL_TEST_DEPS_DIRS) + $(verbose) for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep IS_DEP=1; done +endif + +ifneq ($(wildcard $(TEST_DIR)),) +test-dir: + $(gen_verbose) erlc -v $(TEST_ERLC_OPTS) -I include/ -o $(TEST_DIR) \ + $(call core_find,$(TEST_DIR)/,*.erl) -pa ebin/ +endif + +ifeq ($(wildcard src),) +test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build:: clean deps test-deps + $(verbose) $(MAKE) --no-print-directory test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" +else +ifeq ($(wildcard ebin/test),) +test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build:: clean deps test-deps $(PROJECT).d + $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" + $(gen_verbose) touch ebin/test +else +test-build:: ERLC_OPTS=$(TEST_ERLC_OPTS) +test-build:: deps test-deps $(PROJECT).d + $(verbose) $(MAKE) --no-print-directory app-build test-dir ERLC_OPTS="$(TEST_ERLC_OPTS)" +endif + +clean:: clean-test-dir + +clean-test-dir: +ifneq ($(wildcard $(TEST_DIR)/*.beam),) + $(gen_verbose) rm -f $(TEST_DIR)/*.beam +endif +endif + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: rebar.config + +# We strip out -Werror because we don't want to fail due to +# warnings when used as a dependency. + +compat_prepare_erlc_opts = $(shell echo "$1" | sed 's/, */,/g') + +define compat_convert_erlc_opts +$(if $(filter-out -Werror,$1),\ + $(if $(findstring +,$1),\ + $(shell echo $1 | cut -b 2-))) +endef + +define compat_erlc_opts_to_list +[$(call comma_list,$(foreach o,$(call compat_prepare_erlc_opts,$1),$(call compat_convert_erlc_opts,$o)))] +endef + +define compat_rebar_config +{deps, [ +$(call comma_list,$(foreach d,$(DEPS),\ + $(if $(filter hex,$(call dep_fetch,$d)),\ + {$(call dep_name,$d)$(comma)"$(call dep_repo,$d)"},\ + {$(call dep_name,$d)$(comma)".*"$(comma){git,"$(call dep_repo,$d)"$(comma)"$(call dep_commit,$d)"}}))) +]}. +{erl_opts, $(call compat_erlc_opts_to_list,$(ERLC_OPTS))}. +endef + +$(eval _compat_rebar_config = $$(compat_rebar_config)) +$(eval export _compat_rebar_config) + +rebar.config: + $(gen_verbose) echo "$${_compat_rebar_config}" > rebar.config + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: asciidoc asciidoc-guide asciidoc-manual install-asciidoc distclean-asciidoc + +MAN_INSTALL_PATH ?= /usr/local/share/man +MAN_SECTIONS ?= 3 7 + +docs:: asciidoc + +asciidoc: asciidoc-guide asciidoc-manual + +ifeq ($(wildcard doc/src/guide/book.asciidoc),) +asciidoc-guide: +else +asciidoc-guide: distclean-asciidoc doc-deps + a2x -v -f pdf doc/src/guide/book.asciidoc && mv doc/src/guide/book.pdf doc/guide.pdf + a2x -v -f chunked doc/src/guide/book.asciidoc && mv doc/src/guide/book.chunked/ doc/html/ +endif + +ifeq ($(wildcard doc/src/manual/*.asciidoc),) +asciidoc-manual: +else +asciidoc-manual: distclean-asciidoc doc-deps + for f in doc/src/manual/*.asciidoc ; do \ + a2x -v -f manpage $$f ; \ + done + for s in $(MAN_SECTIONS); do \ + mkdir -p doc/man$$s/ ; \ + mv doc/src/manual/*.$$s doc/man$$s/ ; \ + gzip doc/man$$s/*.$$s ; \ + done + +install-docs:: install-asciidoc + +install-asciidoc: asciidoc-manual + for s in $(MAN_SECTIONS); do \ + mkdir -p $(MAN_INSTALL_PATH)/man$$s/ ; \ + install -g `id -u` -o `id -g` -m 0644 doc/man$$s/*.gz $(MAN_INSTALL_PATH)/man$$s/ ; \ + done +endif + +distclean:: distclean-asciidoc + +distclean-asciidoc: + $(gen_verbose) rm -rf doc/html/ doc/guide.pdf doc/man3/ doc/man7/ + +# Copyright (c) 2014-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: bootstrap bootstrap-lib bootstrap-rel new list-templates + +# Core targets. + +help:: + $(verbose) printf "%s\n" "" \ + "Bootstrap targets:" \ + " bootstrap Generate a skeleton of an OTP application" \ + " bootstrap-lib Generate a skeleton of an OTP library" \ + " bootstrap-rel Generate the files needed to build a release" \ + " new-app in=NAME Create a new local OTP application NAME" \ + " new-lib in=NAME Create a new local OTP library NAME" \ + " new t=TPL n=NAME Generate a module NAME based on the template TPL" \ + " new t=T n=N in=APP Generate a module NAME based on the template TPL in APP" \ + " list-templates List available templates" + +# Bootstrap templates. + +define bs_appsrc +{application, $p, [ + {description, ""}, + {vsn, "0.1.0"}, + {id, "git"}, + {modules, []}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {mod, {$p_app, []}}, + {env, []} +]}. +endef + +define bs_appsrc_lib +{application, $p, [ + {description, ""}, + {vsn, "0.1.0"}, + {id, "git"}, + {modules, []}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]} +]}. +endef + +# To prevent autocompletion issues with ZSH, we add "include erlang.mk" +# separately during the actual bootstrap. +ifdef SP +define bs_Makefile +PROJECT = $p +PROJECT_DESCRIPTION = New project +PROJECT_VERSION = 0.0.1 + +# Whitespace to be used when creating files from templates. +SP = $(SP) + +endef +else +define bs_Makefile +PROJECT = $p +PROJECT_DESCRIPTION = New project +PROJECT_VERSION = 0.0.1 + +endef +endif + +define bs_apps_Makefile +PROJECT = $p +PROJECT_DESCRIPTION = New project +PROJECT_VERSION = 0.0.1 + +include $(call core_relpath,$(dir $(ERLANG_MK_FILENAME)),$(APPS_DIR)/app)/erlang.mk +endef + +define bs_app +-module($p_app). +-behaviour(application). + +-export([start/2]). +-export([stop/1]). + +start(_Type, _Args) -> + $p_sup:start_link(). + +stop(_State) -> + ok. +endef + +define bs_relx_config +{release, {$p_release, "1"}, [$p]}. +{extended_start_script, true}. +{sys_config, "rel/sys.config"}. +{vm_args, "rel/vm.args"}. +endef + +define bs_sys_config +[ +]. +endef + +define bs_vm_args +-name $p@127.0.0.1 +-setcookie $p +-heart +endef + +# Normal templates. + +define tpl_supervisor +-module($(n)). +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Procs = [], + {ok, {{one_for_one, 1, 5}, Procs}}. +endef + +define tpl_gen_server +-module($(n)). +-behaviour(gen_server). + +%% API. +-export([start_link/0]). + +%% gen_server. +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([code_change/3]). + +-record(state, { +}). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + gen_server:start_link(?MODULE, [], []). + +%% gen_server. + +init([]) -> + {ok, #state{}}. + +handle_call(_Request, _From, State) -> + {reply, ignored, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. +endef + +define tpl_module +-module($(n)). +-export([]). +endef + +define tpl_cowboy_http +-module($(n)). +-behaviour(cowboy_http_handler). + +-export([init/3]). +-export([handle/2]). +-export([terminate/3]). + +-record(state, { +}). + +init(_, Req, _Opts) -> + {ok, Req, #state{}}. + +handle(Req, State=#state{}) -> + {ok, Req2} = cowboy_req:reply(200, Req), + {ok, Req2, State}. + +terminate(_Reason, _Req, _State) -> + ok. +endef + +define tpl_gen_fsm +-module($(n)). +-behaviour(gen_fsm). + +%% API. +-export([start_link/0]). + +%% gen_fsm. +-export([init/1]). +-export([state_name/2]). +-export([handle_event/3]). +-export([state_name/3]). +-export([handle_sync_event/4]). +-export([handle_info/3]). +-export([terminate/3]). +-export([code_change/4]). + +-record(state, { +}). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + gen_fsm:start_link(?MODULE, [], []). + +%% gen_fsm. + +init([]) -> + {ok, state_name, #state{}}. + +state_name(_Event, StateData) -> + {next_state, state_name, StateData}. + +handle_event(_Event, StateName, StateData) -> + {next_state, StateName, StateData}. + +state_name(_Event, _From, StateData) -> + {reply, ignored, state_name, StateData}. + +handle_sync_event(_Event, _From, StateName, StateData) -> + {reply, ignored, StateName, StateData}. + +handle_info(_Info, StateName, StateData) -> + {next_state, StateName, StateData}. + +terminate(_Reason, _StateName, _StateData) -> + ok. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. +endef + +define tpl_cowboy_loop +-module($(n)). +-behaviour(cowboy_loop_handler). + +-export([init/3]). +-export([info/3]). +-export([terminate/3]). + +-record(state, { +}). + +init(_, Req, _Opts) -> + {loop, Req, #state{}, 5000, hibernate}. + +info(_Info, Req, State) -> + {loop, Req, State, hibernate}. + +terminate(_Reason, _Req, _State) -> + ok. +endef + +define tpl_cowboy_rest +-module($(n)). + +-export([init/3]). +-export([content_types_provided/2]). +-export([get_html/2]). + +init(_, _Req, _Opts) -> + {upgrade, protocol, cowboy_rest}. + +content_types_provided(Req, State) -> + {[{{<<"text">>, <<"html">>, '*'}, get_html}], Req, State}. + +get_html(Req, State) -> + {<<"This is REST!">>, Req, State}. +endef + +define tpl_cowboy_ws +-module($(n)). +-behaviour(cowboy_websocket_handler). + +-export([init/3]). +-export([websocket_init/3]). +-export([websocket_handle/3]). +-export([websocket_info/3]). +-export([websocket_terminate/3]). + +-record(state, { +}). + +init(_, _, _) -> + {upgrade, protocol, cowboy_websocket}. + +websocket_init(_, Req, _Opts) -> + Req2 = cowboy_req:compact(Req), + {ok, Req2, #state{}}. + +websocket_handle({text, Data}, Req, State) -> + {reply, {text, Data}, Req, State}; +websocket_handle({binary, Data}, Req, State) -> + {reply, {binary, Data}, Req, State}; +websocket_handle(_Frame, Req, State) -> + {ok, Req, State}. + +websocket_info(_Info, Req, State) -> + {ok, Req, State}. + +websocket_terminate(_Reason, _Req, _State) -> + ok. +endef + +define tpl_ranch_protocol +-module($(n)). +-behaviour(ranch_protocol). + +-export([start_link/4]). +-export([init/4]). + +-type opts() :: []. +-export_type([opts/0]). + +-record(state, { + socket :: inet:socket(), + transport :: module() +}). + +start_link(Ref, Socket, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), + {ok, Pid}. + +-spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok. +init(Ref, Socket, Transport, _Opts) -> + ok = ranch:accept_ack(Ref), + loop(#state{socket=Socket, transport=Transport}). + +loop(State) -> + loop(State). +endef + +# Plugin-specific targets. + +define render_template + $(verbose) printf -- '$(subst $(newline),\n,$(subst %,%%,$(subst ','\'',$(subst $(tab),$(WS),$(call $(1))))))\n' > $(2) +endef + +ifndef WS +ifdef SP +WS = $(subst a,,a $(wordlist 1,$(SP),a a a a a a a a a a a a a a a a a a a a)) +else +WS = $(tab) +endif +endif + +bootstrap: +ifneq ($(wildcard src/),) + $(error Error: src/ directory already exists) +endif + $(eval p := $(PROJECT)) + $(eval n := $(PROJECT)_sup) + $(call render_template,bs_Makefile,Makefile) + $(verbose) echo "include erlang.mk" >> Makefile + $(verbose) mkdir src/ +ifdef LEGACY + $(call render_template,bs_appsrc,src/$(PROJECT).app.src) +endif + $(call render_template,bs_app,src/$(PROJECT)_app.erl) + $(call render_template,tpl_supervisor,src/$(PROJECT)_sup.erl) + +bootstrap-lib: +ifneq ($(wildcard src/),) + $(error Error: src/ directory already exists) +endif + $(eval p := $(PROJECT)) + $(call render_template,bs_Makefile,Makefile) + $(verbose) echo "include erlang.mk" >> Makefile + $(verbose) mkdir src/ +ifdef LEGACY + $(call render_template,bs_appsrc_lib,src/$(PROJECT).app.src) +endif + +bootstrap-rel: +ifneq ($(wildcard relx.config),) + $(error Error: relx.config already exists) +endif +ifneq ($(wildcard rel/),) + $(error Error: rel/ directory already exists) +endif + $(eval p := $(PROJECT)) + $(call render_template,bs_relx_config,relx.config) + $(verbose) mkdir rel/ + $(call render_template,bs_sys_config,rel/sys.config) + $(call render_template,bs_vm_args,rel/vm.args) + +new-app: +ifndef in + $(error Usage: $(MAKE) new-app in=APP) +endif +ifneq ($(wildcard $(APPS_DIR)/$in),) + $(error Error: Application $in already exists) +endif + $(eval p := $(in)) + $(eval n := $(in)_sup) + $(verbose) mkdir -p $(APPS_DIR)/$p/src/ + $(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile) +ifdef LEGACY + $(call render_template,bs_appsrc,$(APPS_DIR)/$p/src/$p.app.src) +endif + $(call render_template,bs_app,$(APPS_DIR)/$p/src/$p_app.erl) + $(call render_template,tpl_supervisor,$(APPS_DIR)/$p/src/$p_sup.erl) + +new-lib: +ifndef in + $(error Usage: $(MAKE) new-lib in=APP) +endif +ifneq ($(wildcard $(APPS_DIR)/$in),) + $(error Error: Application $in already exists) +endif + $(eval p := $(in)) + $(verbose) mkdir -p $(APPS_DIR)/$p/src/ + $(call render_template,bs_apps_Makefile,$(APPS_DIR)/$p/Makefile) +ifdef LEGACY + $(call render_template,bs_appsrc_lib,$(APPS_DIR)/$p/src/$p.app.src) +endif + +new: +ifeq ($(wildcard src/)$(in),) + $(error Error: src/ directory does not exist) +endif +ifndef t + $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) +endif +ifndef tpl_$(t) + $(error Unknown template) +endif +ifndef n + $(error Usage: $(MAKE) new t=TEMPLATE n=NAME [in=APP]) +endif +ifdef in + $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new t=$t n=$n in= +else + $(call render_template,tpl_$(t),src/$(n).erl) +endif + +list-templates: + $(verbose) echo Available templates: $(sort $(patsubst tpl_%,%,$(filter tpl_%,$(.VARIABLES)))) + +# Copyright (c) 2014-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: clean-c_src distclean-c_src-env + +# Configuration. + +C_SRC_DIR ?= $(CURDIR)/c_src +C_SRC_ENV ?= $(C_SRC_DIR)/env.mk +C_SRC_OUTPUT ?= $(CURDIR)/priv/$(PROJECT) +C_SRC_TYPE ?= shared + +# System type and C compiler/flags. + +ifeq ($(PLATFORM),msys2) + C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= .exe + C_SRC_OUTPUT_SHARED_EXTENSION ?= .dll +else + C_SRC_OUTPUT_EXECUTABLE_EXTENSION ?= + C_SRC_OUTPUT_SHARED_EXTENSION ?= .so +endif + +ifeq ($(C_SRC_TYPE),shared) + C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_SHARED_EXTENSION) +else + C_SRC_OUTPUT_FILE = $(C_SRC_OUTPUT)$(C_SRC_OUTPUT_EXECUTABLE_EXTENSION) +endif + +ifeq ($(PLATFORM),msys2) +# We hardcode the compiler used on MSYS2. The default CC=cc does +# not produce working code. The "gcc" MSYS2 package also doesn't. + CC = /mingw64/bin/gcc + export CC + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +else ifeq ($(PLATFORM),darwin) + CC ?= cc + CFLAGS ?= -O3 -std=c99 -arch x86_64 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -arch x86_64 -finline-functions -Wall + LDFLAGS ?= -arch x86_64 -flat_namespace -undefined suppress +else ifeq ($(PLATFORM),freebsd) + CC ?= cc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +else ifeq ($(PLATFORM),linux) + CC ?= gcc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +endif + +ifneq ($(PLATFORM),msys2) + CFLAGS += -fPIC + CXXFLAGS += -fPIC +endif + +CFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" +CXXFLAGS += -I"$(ERTS_INCLUDE_DIR)" -I"$(ERL_INTERFACE_INCLUDE_DIR)" + +LDLIBS += -L"$(ERL_INTERFACE_LIB_DIR)" -lerl_interface -lei + +# Verbosity. + +c_verbose_0 = @echo " C " $(?F); +c_verbose = $(c_verbose_$(V)) + +cpp_verbose_0 = @echo " CPP " $(?F); +cpp_verbose = $(cpp_verbose_$(V)) + +link_verbose_0 = @echo " LD " $(@F); +link_verbose = $(link_verbose_$(V)) + +# Targets. + +ifeq ($(wildcard $(C_SRC_DIR)),) +else ifneq ($(wildcard $(C_SRC_DIR)/Makefile),) +app:: app-c_src + +test-build:: app-c_src + +app-c_src: + $(MAKE) -C $(C_SRC_DIR) + +clean:: + $(MAKE) -C $(C_SRC_DIR) clean + +else + +ifeq ($(SOURCES),) +SOURCES := $(sort $(foreach pat,*.c *.C *.cc *.cpp,$(call core_find,$(C_SRC_DIR)/,$(pat)))) +endif +OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) + +COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c +COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c + +app:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) + +test-build:: $(C_SRC_ENV) $(C_SRC_OUTPUT_FILE) + +$(C_SRC_OUTPUT_FILE): $(OBJECTS) + $(verbose) mkdir -p priv/ + $(link_verbose) $(CC) $(OBJECTS) \ + $(LDFLAGS) $(if $(filter $(C_SRC_TYPE),shared),-shared) $(LDLIBS) \ + -o $(C_SRC_OUTPUT_FILE) + +%.o: %.c + $(COMPILE_C) $(OUTPUT_OPTION) $< + +%.o: %.cc + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.C + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.cpp + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +clean:: clean-c_src + +clean-c_src: + $(gen_verbose) rm -f $(C_SRC_OUTPUT_FILE) $(OBJECTS) + +endif + +ifneq ($(wildcard $(C_SRC_DIR)),) +$(C_SRC_ENV): + $(verbose) $(ERL) -eval "file:write_file(\"$(call core_native_path,$(C_SRC_ENV))\", \ + io_lib:format( \ + \"ERTS_INCLUDE_DIR ?= ~s/erts-~s/include/~n\" \ + \"ERL_INTERFACE_INCLUDE_DIR ?= ~s~n\" \ + \"ERL_INTERFACE_LIB_DIR ?= ~s~n\", \ + [code:root_dir(), erlang:system_info(version), \ + code:lib_dir(erl_interface, include), \ + code:lib_dir(erl_interface, lib)])), \ + halt()." + +distclean:: distclean-c_src-env + +distclean-c_src-env: + $(gen_verbose) rm -f $(C_SRC_ENV) + +-include $(C_SRC_ENV) +endif + +# Templates. + +define bs_c_nif +#include "erl_nif.h" + +static int loads = 0; + +static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info) +{ + /* Initialize private data. */ + *priv_data = NULL; + + loads++; + + return 0; +} + +static int upgrade(ErlNifEnv* env, void** priv_data, void** old_priv_data, ERL_NIF_TERM load_info) +{ + /* Convert the private data to the new version. */ + *priv_data = *old_priv_data; + + loads++; + + return 0; +} + +static void unload(ErlNifEnv* env, void* priv_data) +{ + if (loads == 1) { + /* Destroy the private data. */ + } + + loads--; +} + +static ERL_NIF_TERM hello(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + if (enif_is_atom(env, argv[0])) { + return enif_make_tuple2(env, + enif_make_atom(env, "hello"), + argv[0]); + } + + return enif_make_tuple2(env, + enif_make_atom(env, "error"), + enif_make_atom(env, "badarg")); +} + +static ErlNifFunc nif_funcs[] = { + {"hello", 1, hello} +}; + +ERL_NIF_INIT($n, nif_funcs, load, NULL, upgrade, unload) +endef + +define bs_erl_nif +-module($n). + +-export([hello/1]). + +-on_load(on_load/0). +on_load() -> + PrivDir = case code:priv_dir(?MODULE) of + {error, _} -> + AppPath = filename:dirname(filename:dirname(code:which(?MODULE))), + filename:join(AppPath, "priv"); + Path -> + Path + end, + erlang:load_nif(filename:join(PrivDir, atom_to_list(?MODULE)), 0). + +hello(_) -> + erlang:nif_error({not_loaded, ?MODULE}). +endef + +new-nif: +ifneq ($(wildcard $(C_SRC_DIR)/$n.c),) + $(error Error: $(C_SRC_DIR)/$n.c already exists) +endif +ifneq ($(wildcard src/$n.erl),) + $(error Error: src/$n.erl already exists) +endif +ifdef in + $(verbose) $(MAKE) -C $(APPS_DIR)/$(in)/ new-nif n=$n in= +else + $(verbose) mkdir -p $(C_SRC_DIR) src/ + $(call render_template,bs_c_nif,$(C_SRC_DIR)/$n.c) + $(call render_template,bs_erl_nif,src/$n.erl) +endif + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: ci ci-setup distclean-kerl + +KERL ?= $(CURDIR)/kerl +export KERL + +KERL_URL ?= https://raw.githubusercontent.com/yrashk/kerl/master/kerl + +OTP_GIT ?= https://github.com/erlang/otp + +CI_INSTALL_DIR ?= $(HOME)/erlang +CI_OTP ?= + +ifeq ($(strip $(CI_OTP)),) +ci:: +else +ci:: $(addprefix ci-,$(CI_OTP)) + +ci-prepare: $(addprefix $(CI_INSTALL_DIR)/,$(CI_OTP)) + +ci-setup:: + +ci_verbose_0 = @echo " CI " $(1); +ci_verbose = $(ci_verbose_$(V)) + +define ci_target +ci-$(1): $(CI_INSTALL_DIR)/$(1) + $(ci_verbose) \ + PATH="$(CI_INSTALL_DIR)/$(1)/bin:$(PATH)" \ + CI_OTP_RELEASE="$(1)" \ + CT_OPTS="-label $(1)" \ + $(MAKE) clean ci-setup tests +endef + +$(foreach otp,$(CI_OTP),$(eval $(call ci_target,$(otp)))) + +define ci_otp_target +ifeq ($(wildcard $(CI_INSTALL_DIR)/$(1)),) +$(CI_INSTALL_DIR)/$(1): $(KERL) + $(KERL) build git $(OTP_GIT) $(1) $(1) + $(KERL) install $(1) $(CI_INSTALL_DIR)/$(1) +endif +endef + +$(foreach otp,$(CI_OTP),$(eval $(call ci_otp_target,$(otp)))) + +$(KERL): + $(gen_verbose) $(call core_http_get,$(KERL),$(KERL_URL)) + $(verbose) chmod +x $(KERL) + +help:: + $(verbose) printf "%s\n" "" \ + "Continuous Integration targets:" \ + " ci Run '$(MAKE) tests' on all configured Erlang versions." \ + "" \ + "The CI_OTP variable must be defined with the Erlang versions" \ + "that must be tested. For example: CI_OTP = OTP-17.3.4 OTP-17.5.3" + +distclean:: distclean-kerl + +distclean-kerl: + $(gen_verbose) rm -rf $(KERL) +endif + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: ct apps-ct distclean-ct + +# Configuration. + +CT_OPTS ?= +ifneq ($(wildcard $(TEST_DIR)),) + CT_SUITES ?= $(sort $(subst _SUITE.erl,,$(notdir $(call core_find,$(TEST_DIR)/,*_SUITE.erl)))) +else + CT_SUITES ?= +endif + +# Core targets. + +tests:: ct + +distclean:: distclean-ct + +help:: + $(verbose) printf "%s\n" "" \ + "Common_test targets:" \ + " ct Run all the common_test suites for this project" \ + "" \ + "All your common_test suites have their associated targets." \ + "A suite named http_SUITE can be ran using the ct-http target." + +# Plugin-specific targets. + +CT_RUN = ct_run \ + -no_auto_compile \ + -noinput \ + -pa $(CURDIR)/ebin $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(TEST_DIR) \ + -dir $(TEST_DIR) \ + -logdir $(CURDIR)/logs + +ifeq ($(CT_SUITES),) +ct: $(if $(IS_APP),,apps-ct) +else +ct: test-build $(if $(IS_APP),,apps-ct) + $(verbose) mkdir -p $(CURDIR)/logs/ + $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(CT_SUITES)) $(CT_OPTS) +endif + +ifneq ($(ALL_APPS_DIRS),) +define ct_app_target +apps-ct-$1: + $(MAKE) -C $1 ct IS_APP=1 +endef + +$(foreach app,$(ALL_APPS_DIRS),$(eval $(call ct_app_target,$(app)))) + +apps-ct: test-build $(addprefix apps-ct-,$(ALL_APPS_DIRS)) +endif + +ifndef t +CT_EXTRA = +else +ifeq (,$(findstring :,$t)) +CT_EXTRA = -group $t +else +t_words = $(subst :, ,$t) +CT_EXTRA = -group $(firstword $(t_words)) -case $(lastword $(t_words)) +endif +endif + +define ct_suite_target +ct-$(1): test-build + $(verbose) mkdir -p $(CURDIR)/logs/ + $(gen_verbose) $(CT_RUN) -sname ct_$(PROJECT) -suite $(addsuffix _SUITE,$(1)) $(CT_EXTRA) $(CT_OPTS) +endef + +$(foreach test,$(CT_SUITES),$(eval $(call ct_suite_target,$(test)))) + +distclean-ct: + $(gen_verbose) rm -rf $(CURDIR)/logs/ + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: plt distclean-plt dialyze + +# Configuration. + +DIALYZER_PLT ?= $(CURDIR)/.$(PROJECT).plt +export DIALYZER_PLT + +PLT_APPS ?= +DIALYZER_DIRS ?= --src -r $(wildcard src) $(ALL_APPS_DIRS) +DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs + +# Core targets. + +check:: dialyze + +distclean:: distclean-plt + +help:: + $(verbose) printf "%s\n" "" \ + "Dialyzer targets:" \ + " plt Build a PLT file for this project" \ + " dialyze Analyze the project using Dialyzer" + +# Plugin-specific targets. + +define filter_opts.erl + Opts = binary:split(<<"$1">>, <<"-">>, [global]), + Filtered = lists:reverse(lists:foldl(fun + (O = <<"pa ", _/bits>>, Acc) -> [O|Acc]; + (O = <<"D ", _/bits>>, Acc) -> [O|Acc]; + (O = <<"I ", _/bits>>, Acc) -> [O|Acc]; + (_, Acc) -> Acc + end, [], Opts)), + io:format("~s~n", [[["-", O] || O <- Filtered]]), + halt(). +endef + +$(DIALYZER_PLT): deps app + $(verbose) dialyzer --build_plt --apps erts kernel stdlib $(PLT_APPS) $(OTP_DEPS) $(LOCAL_DEPS) $(DEPS) + +plt: $(DIALYZER_PLT) + +distclean-plt: + $(gen_verbose) rm -f $(DIALYZER_PLT) + +ifneq ($(wildcard $(DIALYZER_PLT)),) +dialyze: +else +dialyze: $(DIALYZER_PLT) +endif + $(verbose) dialyzer --no_native `$(call erlang,$(call filter_opts.erl,$(ERLC_OPTS)))` $(DIALYZER_DIRS) $(DIALYZER_OPTS) + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-edoc edoc + +# Configuration. + +EDOC_OPTS ?= + +# Core targets. + +ifneq ($(wildcard doc/overview.edoc),) +docs:: edoc +endif + +distclean:: distclean-edoc + +# Plugin-specific targets. + +edoc: distclean-edoc doc-deps + $(gen_verbose) $(ERL) -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), halt().' + +distclean-edoc: + $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info + +# Copyright (c) 2014 Dave Cottlehuber +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: distclean-escript escript + +# Configuration. + +ESCRIPT_NAME ?= $(PROJECT) +ESCRIPT_FILE ?= $(ESCRIPT_NAME) + +ESCRIPT_COMMENT ?= This is an -*- erlang -*- file + +ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*" +ESCRIPT_SYS_CONFIG ?= "rel/sys.config" +ESCRIPT_EMU_ARGS ?= -pa . \ + -sasl errlog_type error \ + -escript main $(ESCRIPT_NAME) +ESCRIPT_SHEBANG ?= /usr/bin/env escript +ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**" + +# Core targets. + +distclean:: distclean-escript + +help:: + $(verbose) printf "%s\n" "" \ + "Escript targets:" \ + " escript Build an executable escript archive" \ + +# Plugin-specific targets. + +# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl +# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center +# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE : +# Software may only be used for the great good and the true happiness of all +# sentient beings. + +define ESCRIPT_RAW +'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\ +'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\ +' [F || F <- A, not filelib:is_dir(F) ] end,'\ +'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\ +'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\ +'Ez = fun(Escript) ->'\ +' Static = Files([$(ESCRIPT_STATIC)]),'\ +' Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\ +' Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\ +' escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\ +' {archive, Archive, [memory]},'\ +' {shebang, "$(ESCRIPT_SHEBANG)"},'\ +' {comment, "$(ESCRIPT_COMMENT)"},'\ +' {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\ +' ]),'\ +' file:change_mode(Escript, 8#755)'\ +'end,'\ +'Ez("$(ESCRIPT_FILE)"),'\ +'halt().' +endef + +ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW)) + +escript:: distclean-escript deps app + $(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND) + +distclean-escript: + $(gen_verbose) rm -f $(ESCRIPT_NAME) + +# Copyright (c) 2014, Enrique Fernandez +# Copyright (c) 2015, Loïc Hoguin +# This file is contributed to erlang.mk and subject to the terms of the ISC License. + +.PHONY: eunit apps-eunit + +# Configuration + +EUNIT_OPTS ?= +EUNIT_ERL_OPTS ?= + +# Core targets. + +tests:: eunit + +help:: + $(verbose) printf "%s\n" "" \ + "EUnit targets:" \ + " eunit Run all the EUnit tests for this project" + +# Plugin-specific targets. + +define eunit.erl + case "$(COVER)" of + "" -> ok; + _ -> + case cover:compile_beam_directory("ebin") of + {error, _} -> halt(1); + _ -> ok + end + end, + case eunit:test($1, [$(EUNIT_OPTS)]) of + ok -> ok; + error -> halt(2) + end, + case "$(COVER)" of + "" -> ok; + _ -> + cover:export("eunit.coverdata") + end, + halt() +endef + +EUNIT_ERL_OPTS += -pa $(TEST_DIR) $(DEPS_DIR)/*/ebin $(APPS_DIR)/*/ebin $(CURDIR)/ebin + +ifdef t +ifeq (,$(findstring :,$(t))) +eunit: test-build + $(gen_verbose) $(call erlang,$(call eunit.erl,['$(t)']),$(EUNIT_ERL_OPTS)) +else +eunit: test-build + $(gen_verbose) $(call erlang,$(call eunit.erl,fun $(t)/0),$(EUNIT_ERL_OPTS)) +endif +else +EUNIT_EBIN_MODS = $(notdir $(basename $(ERL_FILES) $(BEAM_FILES))) +EUNIT_TEST_MODS = $(notdir $(basename $(call core_find,$(TEST_DIR)/,*.erl))) + +EUNIT_MODS = $(foreach mod,$(EUNIT_EBIN_MODS) $(filter-out \ + $(patsubst %,%_tests,$(EUNIT_EBIN_MODS)),$(EUNIT_TEST_MODS)),'$(mod)') + +eunit: test-build $(if $(IS_APP),,apps-eunit) + $(gen_verbose) $(call erlang,$(call eunit.erl,[$(call comma_list,$(EUNIT_MODS))]),$(EUNIT_ERL_OPTS)) + +ifneq ($(ALL_APPS_DIRS),) +apps-eunit: + $(verbose) for app in $(ALL_APPS_DIRS); do $(MAKE) -C $$app eunit IS_APP=1; done +endif +endif + +# Copyright (c) 2013-2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: relx-rel distclean-relx-rel distclean-relx run + +# Configuration. + +RELX ?= $(CURDIR)/relx +RELX_CONFIG ?= $(CURDIR)/relx.config + +RELX_URL ?= https://github.com/erlware/relx/releases/download/v3.19.0/relx +RELX_OPTS ?= +RELX_OUTPUT_DIR ?= _rel + +ifeq ($(firstword $(RELX_OPTS)),-o) + RELX_OUTPUT_DIR = $(word 2,$(RELX_OPTS)) +else + RELX_OPTS += -o $(RELX_OUTPUT_DIR) +endif + +# Core targets. + +ifeq ($(IS_DEP),) +ifneq ($(wildcard $(RELX_CONFIG)),) +rel:: relx-rel +endif +endif + +distclean:: distclean-relx-rel distclean-relx + +# Plugin-specific targets. + +$(RELX): + $(gen_verbose) $(call core_http_get,$(RELX),$(RELX_URL)) + $(verbose) chmod +x $(RELX) + +relx-rel: $(RELX) rel-deps app + $(verbose) $(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) + +distclean-relx-rel: + $(gen_verbose) rm -rf $(RELX_OUTPUT_DIR) + +distclean-relx: + $(gen_verbose) rm -rf $(RELX) + +# Run target. + +ifeq ($(wildcard $(RELX_CONFIG)),) +run: +else + +define get_relx_release.erl + {ok, Config} = file:consult("$(RELX_CONFIG)"), + {release, {Name, _}, _} = lists:keyfind(release, 1, Config), + io:format("~s", [Name]), + halt(0). +endef + +RELX_RELEASE = `$(call erlang,$(get_relx_release.erl))` + +run: all + $(verbose) $(RELX_OUTPUT_DIR)/$(RELX_RELEASE)/bin/$(RELX_RELEASE) console + +help:: + $(verbose) printf "%s\n" "" \ + "Relx targets:" \ + " run Compile the project, build the release and run it" + +endif + +# Copyright (c) 2014, M Robert Martin +# Copyright (c) 2015, Loïc Hoguin +# This file is contributed to erlang.mk and subject to the terms of the ISC License. + +.PHONY: shell + +# Configuration. + +SHELL_ERL ?= erl +SHELL_PATHS ?= $(CURDIR)/ebin $(APPS_DIR)/*/ebin $(DEPS_DIR)/*/ebin +SHELL_OPTS ?= + +ALL_SHELL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(SHELL_DEPS)) + +# Core targets + +help:: + $(verbose) printf "%s\n" "" \ + "Shell targets:" \ + " shell Run an erlang shell with SHELL_OPTS or reasonable default" + +# Plugin-specific targets. + +$(foreach dep,$(SHELL_DEPS),$(eval $(call dep_target,$(dep)))) + +build-shell-deps: $(ALL_SHELL_DEPS_DIRS) + $(verbose) for dep in $(ALL_SHELL_DEPS_DIRS) ; do $(MAKE) -C $$dep ; done + +shell: build-shell-deps + $(gen_verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) $(SHELL_OPTS) + +# Copyright (c) 2015, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +ifeq ($(filter triq,$(DEPS) $(TEST_DEPS)),triq) +.PHONY: triq + +# Targets. + +tests:: triq + +define triq_check.erl + code:add_pathsa(["$(CURDIR)/ebin", "$(DEPS_DIR)/*/ebin"]), + try + case $(1) of + all -> [true] =:= lists:usort([triq:check(M) || M <- [$(call comma_list,$(3))]]); + module -> triq:check($(2)); + function -> triq:check($(2)) + end + of + true -> halt(0); + _ -> halt(1) + catch error:undef -> + io:format("Undefined property or module~n"), + halt(0) + end. +endef + +ifdef t +ifeq (,$(findstring :,$(t))) +triq: test-build + $(verbose) $(call erlang,$(call triq_check.erl,module,$(t))) +else +triq: test-build + $(verbose) echo Testing $(t)/0 + $(verbose) $(call erlang,$(call triq_check.erl,function,$(t)())) +endif +else +triq: test-build + $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename $(wildcard ebin/*.beam)))))) + $(gen_verbose) $(call erlang,$(call triq_check.erl,all,undefined,$(MODULES))) +endif +endif + +# Copyright (c) 2015, Erlang Solutions Ltd. +# This file is part of erlang.mk and subject to the terms of the ISC License. + +.PHONY: xref distclean-xref + +# Configuration. + +ifeq ($(XREF_CONFIG),) + XREF_ARGS := +else + XREF_ARGS := -c $(XREF_CONFIG) +endif + +XREFR ?= $(CURDIR)/xrefr +export XREFR + +XREFR_URL ?= https://github.com/inaka/xref_runner/releases/download/0.2.2/xrefr + +# Core targets. + +help:: + $(verbose) printf "%s\n" "" \ + "Xref targets:" \ + " xref Run Xrefr using $XREF_CONFIG as config file if defined" + +distclean:: distclean-xref + +# Plugin-specific targets. + +$(XREFR): + $(gen_verbose) $(call core_http_get,$(XREFR),$(XREFR_URL)) + $(verbose) chmod +x $(XREFR) + +xref: deps app $(XREFR) + $(gen_verbose) $(XREFR) $(XREFR_ARGS) + +distclean-xref: + $(gen_verbose) rm -rf $(XREFR) + +# Copyright 2015, Viktor Söderqvist +# This file is part of erlang.mk and subject to the terms of the ISC License. + +COVER_REPORT_DIR = cover + +# Hook in coverage to ct + +ifdef COVER +ifdef CT_RUN +# All modules in 'ebin' +COVER_MODS = $(notdir $(basename $(call core_ls,ebin/*.beam))) + +test-build:: $(TEST_DIR)/ct.cover.spec + +$(TEST_DIR)/ct.cover.spec: + $(verbose) echo Cover mods: $(COVER_MODS) + $(gen_verbose) printf "%s\n" \ + '{incl_mods,[$(subst $(space),$(comma),$(COVER_MODS))]}.' \ + '{export,"$(CURDIR)/ct.coverdata"}.' > $@ + +CT_RUN += -cover $(TEST_DIR)/ct.cover.spec +endif +endif + +# Core targets + +ifdef COVER +ifneq ($(COVER_REPORT_DIR),) +tests:: + $(verbose) $(MAKE) --no-print-directory cover-report +endif +endif + +clean:: coverdata-clean + +ifneq ($(COVER_REPORT_DIR),) +distclean:: cover-report-clean +endif + +help:: + $(verbose) printf "%s\n" "" \ + "Cover targets:" \ + " cover-report Generate a HTML coverage report from previously collected" \ + " cover data." \ + " all.coverdata Merge {eunit,ct}.coverdata into one coverdata file." \ + "" \ + "If COVER=1 is set, coverage data is generated by the targets eunit and ct. The" \ + "target tests additionally generates a HTML coverage report from the combined" \ + "coverdata files from each of these testing tools. HTML reports can be disabled" \ + "by setting COVER_REPORT_DIR to empty." + +# Plugin specific targets + +COVERDATA = $(filter-out all.coverdata,$(wildcard *.coverdata)) + +.PHONY: coverdata-clean +coverdata-clean: + $(gen_verbose) rm -f *.coverdata ct.cover.spec + +# Merge all coverdata files into one. +all.coverdata: $(COVERDATA) + $(gen_verbose) $(ERL) -eval ' \ + $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) \ + cover:export("$@"), halt(0).' + +# These are only defined if COVER_REPORT_DIR is non-empty. Set COVER_REPORT_DIR to +# empty if you want the coverdata files but not the HTML report. +ifneq ($(COVER_REPORT_DIR),) + +.PHONY: cover-report-clean cover-report + +cover-report-clean: + $(gen_verbose) rm -rf $(COVER_REPORT_DIR) + +ifeq ($(COVERDATA),) +cover-report: +else + +# Modules which include eunit.hrl always contain one line without coverage +# because eunit defines test/0 which is never called. We compensate for this. +EUNIT_HRL_MODS = $(subst $(space),$(comma),$(shell \ + grep -e '^\s*-include.*include/eunit\.hrl"' src/*.erl \ + | sed "s/^src\/\(.*\)\.erl:.*/'\1'/" | uniq)) + +define cover_report.erl + $(foreach f,$(COVERDATA),cover:import("$(f)") == ok orelse halt(1),) + Ms = cover:imported_modules(), + [cover:analyse_to_file(M, "$(COVER_REPORT_DIR)/" ++ atom_to_list(M) + ++ ".COVER.html", [html]) || M <- Ms], + Report = [begin {ok, R} = cover:analyse(M, module), R end || M <- Ms], + EunitHrlMods = [$(EUNIT_HRL_MODS)], + Report1 = [{M, {Y, case lists:member(M, EunitHrlMods) of + true -> N - 1; false -> N end}} || {M, {Y, N}} <- Report], + TotalY = lists:sum([Y || {_, {Y, _}} <- Report1]), + TotalN = lists:sum([N || {_, {_, N}} <- Report1]), + Perc = fun(Y, N) -> case Y + N of 0 -> 100; S -> round(100 * Y / S) end end, + TotalPerc = Perc(TotalY, TotalN), + {ok, F} = file:open("$(COVER_REPORT_DIR)/index.html", [write]), + io:format(F, "~n" + "~n" + "Coverage report~n" + "~n", []), + io:format(F, "

Coverage

~n

Total: ~p%

~n", [TotalPerc]), + io:format(F, "~n", []), + [io:format(F, "" + "~n", + [M, M, Perc(Y, N)]) || {M, {Y, N}} <- Report1], + How = "$(subst $(space),$(comma)$(space),$(basename $(COVERDATA)))", + Date = "$(shell date -u "+%Y-%m-%dT%H:%M:%SZ")", + io:format(F, "
ModuleCoverage
~p~p%
~n" + "

Generated using ~s and erlang.mk on ~s.

~n" + "", [How, Date]), + halt(). +endef + +cover-report: + $(gen_verbose) mkdir -p $(COVER_REPORT_DIR) + $(gen_verbose) $(call erlang,$(cover_report.erl)) + +endif +endif # ifneq ($(COVER_REPORT_DIR),) diff --git a/etc/emqttd.conf b/etc/emqttd.conf index 92a575795..04daa1712 100644 --- a/etc/emqttd.conf +++ b/etc/emqttd.conf @@ -190,7 +190,7 @@ {bridge_max_queue_len, 10000}. %% Ping Interval of bridge node -{bridge_ping_down_interval, 1} %seconds +{bridge_ping_down_interval, 1}. % second %%------------------------------------------------------------------- %% Plugins @@ -233,7 +233,7 @@ %% Long GC, don't monitor in production mode for: %% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 -{sysmon_long_gc, false}. +{sysmon_long_gc, false}. %% Long Schedule(ms) {sysmon_long_schedule, 240}. diff --git a/rebar b/rebar deleted file mode 100755 index c2b7e20224dff58e53a9ed3f149efbef8586b9c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182326 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%0AcqG2JHpOn!G;(!qD1ktteX${pHvVNZ~~I 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�@K+#^FuA;X-b&2l7tp@D$jHch1q;iW1T{AXeu$} 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 diff --git a/rebar.config b/rebar.config deleted file mode 100644 index 5d90eb17e..000000000 --- a/rebar.config +++ /dev/null @@ -1,49 +0,0 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ts=4 sw=4 ft=erlang et - -{require_min_otp_vsn, "R17"}. - -%% fail_on_warning, -{erl_opts, [debug_info, {parse_transform, lager_transform}]}. - -{erl_opts, [warn_export_all, - warn_unused_import, - {i, "include"}, - {src_dirs, ["src"]}]}. - -{validate_app_modules, true}. - -{erl_first_files, ["src/gen_server2.erl", - "src/emqttd_auth_mod.erl", - "src/emqttd_acl_mod.erl"]}. - -{eunit_opts, []}. %%verbose - -{ct_dir, "test"}. - -{ct_log_dir, "logs"}. - -{ct_extra_params, "-name ct_emqttd@127.0.0.1 -config rel/files/emqttd.test.config"}. - -{ct_use_short_names, false}. - -{xref_checks, [undefined_function_calls]}. - -{cover_enabled, true}. - -%% plugins cannot find emqttd.hrl without ".." lib dirs:( -%% but this setting will make deps apps collision -%% comment in 0.13.0 release -%% {lib_dirs, ["../"]}. - -{sub_dirs, ["rel", "plugins/*/"]}. - -{deps, [ - {gproc, ".*", {git, "git://github.com/uwiger/gproc.git", {branch, "master"}}}, - {lager, ".*", {git, "git://github.com/basho/lager.git", {branch, "master"}}}, - {gen_logger, ".*", {git, "git://github.com/emqtt/gen_logger.git", {branch, "master"}}}, - {esockd, ".*", {git, "git://github.com/emqtt/esockd.git", {branch, "master"}}}, - {mochiweb, "4.*", {git, "git://github.com/emqtt/mochiweb.git", {branch, "master"}}} -]}. - -{recursive_cmds, [ct, eunit, clean]}. diff --git a/rel/files/acl.config b/rel/files/acl.config deleted file mode 100644 index 9b1d512a6..000000000 --- a/rel/files/acl.config +++ /dev/null @@ -1,28 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% -%%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL) -%%% -%%% -type who() :: all | binary() | -%%% {ipaddr, esockd_access:cidr()} | -%%% {client, binary()} | -%%% {user, binary()}. -%%% -%%% -type access() :: subscribe | publish | pubsub. -%%% -%%% -type topic() :: binary(). -%%% -%%% -type rule() :: {allow, all} | -%%% {allow, who(), access(), list(topic())} | -%%% {deny, all} | -%%% {deny, who(), access(), list(topic())}. -%%% -%%%----------------------------------------------------------------------------- - -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. - diff --git a/rel/files/clients.config b/rel/files/clients.config deleted file mode 100644 index 2c880c365..000000000 --- a/rel/files/clients.config +++ /dev/null @@ -1,3 +0,0 @@ -testclientid0 -testclientid1 127.0.0.1 -testclientid2 192.168.0.1/24 diff --git a/rel/files/emqttd.config.development b/rel/files/emqttd.config.development deleted file mode 100644 index 8cec54f04..000000000 --- a/rel/files/emqttd.config.development +++ /dev/null @@ -1,303 +0,0 @@ -% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -[{kernel, [ - {start_timer, true}, - {start_pg2, true} - ]}, - {sasl, [ - {sasl_error_logger, {file, "log/emqttd_sasl.log"}} - ]}, - {ssl, [ - %{versions, ['tlsv1.2', 'tlsv1.1']} - ]}, - {lager, [ - {colored, true}, - {async_threshold, 1000}, - {error_logger_redirect, false}, - {crash_log, "log/emqttd_crash.log"}, - {handlers, [ - {lager_console_backend, info}, - %%NOTICE: Level >= error - %%{lager_emqtt_backend, error}, - {lager_file_backend, [ - {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, - {file, "log/emqttd_info.log"}, - {level, info}, - {size, 104857600}, - {date, "$D0"}, - {count, 30} - ]}, - {lager_file_backend, [ - {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, - {file, "log/emqttd_error.log"}, - {level, error}, - {size, 104857600}, - {date, "$D0"}, - {count, 30} - ]} - ]} - ]}, - {esockd, [ - {logger, {lager, info}} - ]}, - {emqttd, [ - %% Authentication and Authorization - {access, [ - %% Authetication. Anonymous Default - {auth, [ - %% Authentication with username, password - %{username, []}, - - %% Authentication with clientid - %{clientid, [{password, no}, {file, "etc/clients.config"}]}, - - %% Authentication with LDAP - % {ldap, [ - % {servers, ["localhost"]}, - % {port, 389}, - % {timeout, 30}, - % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - % {ssl, fasle}, - % {sslopts, [ - % {certfile, "ssl.crt"}, - % {keyfile, "ssl.key"}]} - % ]}, - - %% Allow all - {anonymous, []} - ]}, - %% ACL config - {acl, [ - %% Internal ACL module - {internal, [{file, "etc/acl.config"}, {nomatch, allow}]} - ]} - ]}, - %% MQTT Protocol Options - {mqtt, [ - %% Packet - {packet, [ - %% Max ClientId Length Allowed - {max_clientid_len, 1024}, - %% Max Packet Size Allowed, 64K default - {max_packet_size, 65536} - ]}, - %% Client - {client, [ - %% Socket is connected, but no 'CONNECT' packet received - {idle_timeout, 10} %% seconds - ]}, - %% Session - {session, [ - %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - %% 0 means no limit - {max_inflight, 100}, - - %% Retry interval for redelivering QoS1/2 messages. - {unack_retry_interval, 20}, - - %% Awaiting PUBREL Timeout - {await_rel_timeout, 20}, - - %% Max Packets that Awaiting PUBREL, 0 means no limit - {max_awaiting_rel, 0}, - - %% Statistics Collection Interval(seconds) - {collect_interval, 20}, - - %% Expired after 2 day (unit: minute) - {expired_after, 2880} - - ]}, - %% Queue - {queue, [ - %% simple | priority - {type, simple}, - - %% Topic Priority: 0~255, Default is 0 - %% {priority, [{"topic/1", 10}, {"topic/2", 8}]}, - - %% Max queue length. Enqueued messages when persistent client disconnected, - %% or inflight window is full. - {max_length, infinity}, - - %% Low-water mark of queued messages - {low_watermark, 0.2}, - - %% High-water mark of queued messages - {high_watermark, 0.6}, - - %% Queue Qos0 messages? - {queue_qos0, true} - ]} - ]}, - %% Broker Options - {broker, [ - %% System interval of publishing broker $SYS messages - {sys_interval, 60}, - - %% Retained messages - {retained, [ - %% Expired after seconds, never expired if 0 - {expired_after, 0}, - - %% Max number of retained messages - {max_message_num, 100000}, - - %% Max Payload Size of retained message - {max_playload_size, 65536} - ]}, - - %% PubSub and Router - {pubsub, [ - %% Default should be scheduler numbers - {pool_size, 8}, - - %% Store Subscription: true | false - {subscription, true}, - - %% Route aging time(seconds) - {route_aging, 5} - ]}, - - %% Bridge - {bridge, [ - %%TODO: bridge queue size - {max_queue_len, 10000}, - - %% Ping Interval of bridge node - {ping_down_interval, 1} %seconds - ]} - ]}, - %% Modules - {modules, [ - %% Client presence management module. - %% Publish messages when client connected or disconnected - {presence, [{qos, 0}]}, - - %% Subscribe topics automatically when client connected - {subscription, [ - - %% $c will be replaced by clientid - %% {"$queue/clients/$c", 1}, - - %% Static subscriptions from backend - backend - ]} - - %% Rewrite rules - %% {rewrite, [{file, "etc/rewrite.config"}]} - ]}, - %% Plugins - {plugins, [ - %% Plugin App Library Dir - {plugins_dir, "./plugins"}, - - %% File to store loaded plugin names. - {loaded_file, "./data/loaded_plugins"} - ]}, - - %% Listeners - {listeners, [ - {mqtt, 1883, [ - %% Size of acceptor pool - {acceptors, 16}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% Connection Options - {connopts, [ - %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec - %% {rate_limit, "100,10"} %% 100K burst, 10K rate - ]}, - - %% Socket Options - {sockopts, [ - %Set buffer if hight thoughtput - %{recbuf, 4096}, - %{sndbuf, 4096}, - %{buffer, 4096}, - %{nodelay, true}, - {backlog, 512} - ]} - ]}, - - {mqtts, 8883, [ - %% Size of acceptor pool - {acceptors, 4}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% SSL certificate and key files - {ssl, [{certfile, "etc/ssl/ssl.crt"}, - {keyfile, "etc/ssl/ssl.key"}]}, - - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]}, - %% WebSocket over HTTPS Listener - %% {https, 8083, [ - %% %% Size of acceptor pool - %% {acceptors, 4}, - %% %% Maximum number of concurrent clients - %% {max_clients, 512}, - %% %% Socket Access Control - %% {access, [{allow, all}]}, - %% %% SSL certificate and key files - %% {ssl, [{certfile, "etc/ssl/ssl.crt"}, - %% {keyfile, "etc/ssl/ssl.key"}]}, - %% %% Socket Options - %% {sockopts, [ - %% %{buffer, 4096}, - %% {backlog, 1024} - %% ]} - %%]}, - - %% HTTP and WebSocket Listener - {http, 8083, [ - %% Size of acceptor pool - {acceptors, 4}, - %% Maximum number of concurrent clients - {max_clients, 64}, - %% Socket Access Control - {access, [{allow, all}]}, - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]} - ]}, - - %% Erlang System Monitor - {sysmon, [ - %% Long GC - {long_gc, 100}, - - %% Long Schedule(ms) - {long_schedule, 100}, - - %% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. - %% 8 * 1024 * 1024 - {large_heap, 8388608}, - - %% Busy Port - {busy_port, true}, - - %% Busy Dist Port - {busy_dist_port, true} - - ]} - ]} -]. - diff --git a/rel/files/emqttd.config.production b/rel/files/emqttd.config.production deleted file mode 100644 index d41c9f0e9..000000000 --- a/rel/files/emqttd.config.production +++ /dev/null @@ -1,296 +0,0 @@ -% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -[{kernel, [ - {start_timer, true}, - {start_pg2, true} - ]}, - {sasl, [ - {sasl_error_logger, {file, "log/emqttd_sasl.log"}} - ]}, - {ssl, [ - %{versions, ['tlsv1.2', 'tlsv1.1']} - ]}, - {lager, [ - {colored, true}, - {async_threshold, 5000}, - {error_logger_redirect, false}, - {crash_log, "log/emqttd_crash.log"}, - {handlers, [ - {lager_console_backend, error}, - %%NOTICE: Level >= error - %%{lager_emqtt_backend, error}, - {lager_file_backend, [ - {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, - {file, "log/emqttd_error.log"}, - {level, error}, - {size, 104857600}, - {date, "$D0"}, - {count, 30} - ]} - ]} - ]}, - {esockd, [ - {logger, {lager, error}} - ]}, - {emqttd, [ - %% Authentication and Authorization - {access, [ - %% Authetication. Anonymous Default - {auth, [ - %% Authentication with username, password - %{username, []}, - - %% Authentication with clientid - %{clientid, [{password, no}, {file, "etc/clients.config"}]}, - - %% Authentication with LDAP - % {ldap, [ - % {servers, ["localhost"]}, - % {port, 389}, - % {timeout, 30}, - % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - % {ssl, fasle}, - % {sslopts, [ - % {certfile, "ssl.crt"}, - % {keyfile, "ssl.key"}]} - % ]}, - - %% Allow all - {anonymous, []} - ]}, - %% ACL config - {acl, [ - %% Internal ACL module - {internal, [{file, "etc/acl.config"}, {nomatch, allow}]} - ]} - ]}, - %% MQTT Protocol Options - {mqtt, [ - %% Packet - {packet, [ - %% Max ClientId Length Allowed - {max_clientid_len, 512}, - %% Max Packet Size Allowed, 64K default - {max_packet_size, 65536} - ]}, - %% Client - {client, [ - %% Socket is connected, but no 'CONNECT' packet received - {idle_timeout, 30} %% seconds - ]}, - %% Session - {session, [ - %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - %% 0 means no limit - {max_inflight, 100}, - - %% Retry interval for redelivering QoS1/2 messages. - {unack_retry_interval, 60}, - - %% Awaiting PUBREL Timeout - {await_rel_timeout, 20}, - - %% Max Packets that Awaiting PUBREL, 0 means no limit - {max_awaiting_rel, 0}, - - %% Statistics Collection Interval(seconds) - {collect_interval, 0}, - - %% Expired after 2 day (unit: minute) - {expired_after, 2880} - - ]}, - %% Queue - {queue, [ - %% simple | priority - {type, simple}, - - %% Topic Priority: 0~255, Default is 0 - %% {priority, [{"topic/1", 10}, {"topic/2", 8}]}, - - %% Max queue length. Enqueued messages when persistent client disconnected, - %% or inflight window is full. - {max_length, infinity}, - - %% Low-water mark of queued messages - {low_watermark, 0.2}, - - %% High-water mark of queued messages - {high_watermark, 0.6}, - - %% Queue Qos0 messages? - {queue_qos0, true} - ]} - ]}, - %% Broker Options - {broker, [ - %% System interval of publishing broker $SYS messages - {sys_interval, 60}, - - %% Retained messages - {retained, [ - %% Expired after seconds, never expired if 0 - {expired_after, 0}, - - %% Max number of retained messages - {max_message_num, 100000}, - - %% Max Payload Size of retained message - {max_playload_size, 65536} - ]}, - - %% PubSub and Router - {pubsub, [ - %% Default should be scheduler numbers - {pool_size, 8}, - - %% Store Subscription: true | false - {subscription, true}, - - %% Route aging time(seconds) - {route_aging, 5} - ]}, - - %% Bridge - {bridge, [ - %%TODO: bridge queue size - {max_queue_len, 10000}, - - %% Ping Interval of bridge node - {ping_down_interval, 1} %seconds - ]} - ]}, - %% Modules - {modules, [ - %% Client presence management module. - %% Publish messages when client connected or disconnected - {presence, [{qos, 0}]}, - - %% Subscribe topics automatically when client connected - {subscription, [ - - %% $c will be replaced by clientid - %% {"$queue/clients/$c", 1}, - - %% Static subscriptions from backend - backend - ]} - - %% Rewrite rules - %% {rewrite, [{file, "etc/rewrite.config"}]} - ]}, - %% Plugins - {plugins, [ - %% Plugin App Library Dir - {plugins_dir, "./plugins"}, - - %% File to store loaded plugin names. - {loaded_file, "./data/loaded_plugins"} - ]}, - - %% Listeners - {listeners, [ - {mqtt, 1883, [ - %% Size of acceptor pool - {acceptors, 16}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% Connection Options - {connopts, [ - %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec - %% {rate_limit, "100,10"} %% 100K burst, 10K rate - ]}, - - %% Socket Options - {sockopts, [ - %Set buffer if hight thoughtput - %{recbuf, 4096}, - %{sndbuf, 4096}, - %{buffer, 4096}, - %{nodelay, true}, - {backlog, 1024} - ]} - ]}, - - {mqtts, 8883, [ - %% Size of acceptor pool - {acceptors, 4}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% SSL certificate and key files - {ssl, [{certfile, "etc/ssl/ssl.crt"}, - {keyfile, "etc/ssl/ssl.key"}]}, - - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]}, - %% WebSocket over HTTPS Listener - %% {https, 8083, [ - %% %% Size of acceptor pool - %% {acceptors, 4}, - %% %% Maximum number of concurrent clients - %% {max_clients, 512}, - %% %% Socket Access Control - %% {access, [{allow, all}]}, - %% %% SSL certificate and key files - %% {ssl, [{certfile, "etc/ssl/ssl.crt"}, - %% {keyfile, "etc/ssl/ssl.key"}]}, - %% %% Socket Options - %% {sockopts, [ - %% %{buffer, 4096}, - %% {backlog, 1024} - %% ]} - %%]}, - - %% HTTP and WebSocket Listener - {http, 8083, [ - %% Size of acceptor pool - {acceptors, 4}, - %% Maximum number of concurrent clients - {max_clients, 64}, - %% Socket Access Control - {access, [{allow, all}]}, - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]} - ]}, - - %% Erlang System Monitor - {sysmon, [ - %% Long GC, don't monitor in production mode for: - %% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 - {long_gc, false}, - - %% Long Schedule(ms) - {long_schedule, 240}, - - %% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. - %% 8 * 1024 * 1024 - {large_heap, 8388608}, - - %% Busy Port - {busy_port, false}, - - %% Busy Dist Port - {busy_dist_port, true} - - ]} - ]} -]. - diff --git a/rel/files/erl b/rel/files/erl deleted file mode 100755 index 6f65e3fc9..000000000 --- a/rel/files/erl +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh - -## This script replaces the default "erl" in erts-VSN/bin. This is necessary -## as escript depends on erl and in turn, erl depends on having access to a -## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect -## of running escript -- the embedded node bypasses erl and uses erlexec directly -## (as it should). -## -## Note that this script makes the assumption that there is a start_clean.boot -## file available in $ROOTDIR/release/VSN. - -# Determine the abspath of where this script is executing from. -ERTS_BIN_DIR=$(cd ${0%/*} && pwd) - -# Now determine the root directory -- this script runs from erts-VSN/bin, -# so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR -# path. -ROOTDIR=${ERTS_BIN_DIR%/*/*} - -# Parse out release and erts info -START_ERL=`cat $ROOTDIR/releases/start_erl.data` -ERTS_VSN=${START_ERL% *} -APP_VSN=${START_ERL#* } - -BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin -EMU=beam -PROGNAME=`echo $0 | sed 's/.*\\///'` -CMD="$BINDIR/erlexec" -export EMU -export ROOTDIR -export BINDIR -export PROGNAME - -exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"} diff --git a/rel/files/loaded_plugins b/rel/files/loaded_plugins deleted file mode 100644 index e69de29bb..000000000 diff --git a/rel/sys.config b/rel/sys.config new file mode 100644 index 000000000..f8f4c82d0 --- /dev/null +++ b/rel/sys.config @@ -0,0 +1,38 @@ +% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ft=erlang ts=4 sw=4 et +[{kernel, [ + {start_timer, true}, + {start_pg2, true} + ]}, + {sasl, [ + {sasl_error_logger, {file, "log/emqttd_sasl.log"}} + ]}, + {ssl, [ + %{versions, ['tlsv1.2', 'tlsv1.1']} + ]}, + {esockd, [ + {logger, {lager, error}} + ]}, + {lager, [ + {colored, true}, + {async_threshold, 5000}, + {error_logger_redirect, false}, + {crash_log, "log/emqttd_crash.log"}, + {handlers, [ + {lager_console_backend, error}, + %%NOTICE: Level >= error + %%{lager_emqtt_backend, error}, + {lager_file_backend, [ + {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, + {file, "log/emqttd_error.log"}, + {level, error}, + {size, 104857600}, + {date, "$D0"}, + {count, 30} + ]} + ]} + ]}, + {emqttd, [ + {gen_conf, "etc/emqttd.conf"} + ]} +]. diff --git a/rel/files/vm.args b/rel/vm.args similarity index 100% rename from rel/files/vm.args rename to rel/vm.args diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 5c56e824f..277d0fd4d 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -46,6 +46,7 @@ Reason :: term()). start(_StartType, _StartArgs) -> print_banner(), + gen_conf:init(emqttd), emqttd_mnesia:start(), {ok, Sup} = emqttd_sup:start_link(), start_servers(Sup), diff --git a/src/emqttd_sysmon_sup.erl b/src/emqttd_sysmon_sup.erl index 02d60530a..883dab20e 100644 --- a/src/emqttd_sysmon_sup.erl +++ b/src/emqttd_sysmon_sup.erl @@ -28,7 +28,17 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Sysmon = {sysmon, {emqttd_sysmon, start_link, [emqttd:env(sysmon)]}, - permanent, 5000, worker, [emqttd_sysmon]} , + Sysmon = {sysmon, {emqttd_sysmon, start_link, [opts()]}, + permanent, 5000, worker, [emqttd_sysmon]}, {ok, {{one_for_one, 10, 100}, [Sysmon]}}. +opts() -> + Opts = [{long_gc, config(sysmon_long_gc)}, + {long_schedule, config(sysmon_long_schedule)}, + {large_heap, config(sysmon_large_heap)}, + {busy_port, config(busy_port)}, + {busy_dist_port, config(sysmon_busy_dist_port)}], + [{Key, Val} || {Key, {ok, Val}} <- Opts]. + +config(Key) -> gen_conf:value(emqttd, Key). + From 3e77773d04b14e23b01846fa3ad2d044e99fb963 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 19 Jul 2016 17:58:31 +0800 Subject: [PATCH 017/116] generated by erlang.mk --- rebar.config | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 rebar.config diff --git a/rebar.config b/rebar.config new file mode 100644 index 000000000..5306e0ae0 --- /dev/null +++ b/rebar.config @@ -0,0 +1,4 @@ +{deps, [ +{gproc,".*",{git,"https://github.com/uwiger/gproc.git",""}},{lager,".*",{git,"https://github.com/basho/lager.git",""}},{gen_logger,".*",{git,"https://github.com/emqtt/gen_logger.git",""}},{gen_conf,".*",{git,"https://github.com/emqtt/gen_conf.git",""}},{esockd,".*",{git,"https://github.com/emqtt/esockd.git","udp"}},{mochiweb,".*",{git,"https://github.com/emqtt/mochiweb.git",""}} +]}. +{erl_opts, [{parse_transform,lager_transform}]}. From 710890dd226604347e8059051e8dfa7e59db5c27 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 21 Jul 2016 09:31:14 +0800 Subject: [PATCH 018/116] relx --- .gitignore | 3 +- bin/emqttd | 604 ++++++++++++++++++++++-------------- bin/emqttd_ctl | 124 ++++---- bin/emqttd_top | 117 ------- bin/install_upgrade.escript | 44 --- bin/install_upgrade_escript | 143 +++++++++ bin/nodetool | 44 ++- etc/emqttd.conf | 87 +++--- etc/rewrite.conf | 14 + rel/vars.config | 19 ++ relx | Bin 0 -> 181078 bytes relx.config | 41 +++ src/emqttd.app.src | 2 +- src/emqttd_client.erl | 2 +- 14 files changed, 738 insertions(+), 506 deletions(-) delete mode 100755 bin/emqttd_top delete mode 100644 bin/install_upgrade.escript create mode 100755 bin/install_upgrade_escript create mode 100644 etc/rewrite.conf create mode 100644 rel/vars.config create mode 100755 relx create mode 100644 relx.config diff --git a/.gitignore b/.gitignore index 8e6271dba..0982fe30f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ deps erl_crash.dump ebin !ebin/.placeholder -rel/emqttd -rel/emqttd* .concrete/DEV_MODE .rebar test/ebin/*.beam @@ -28,3 +26,4 @@ logs ct.coverdata .idea/ emqttd.iml +_rel/ diff --git a/bin/emqttd b/bin/emqttd index b78e68e25..e04524fd4 100755 --- a/bin/emqttd +++ b/bin/emqttd @@ -2,46 +2,21 @@ # -*- tab-width:4;indent-tabs-mode:nil -*- # ex: ts=4 sw=4 et -# /bin/sh on Solaris is not a POSIX compatible shell, but /usr/bin/ksh is. -if [ `uname -s` = 'SunOS' -a "${POSIX_SHELL}" != "true" ]; then - POSIX_SHELL="true" - export POSIX_SHELL - # To support 'whoami' add /usr/ucb to path - PATH=/usr/ucb:$PATH - export PATH - exec /usr/bin/ksh $0 "$@" -fi -unset POSIX_SHELL # clear it so if we invoke other scripts, they run as ksh as well +set -e -RUNNER_SCRIPT_DIR={{runner_script_dir}} -RUNNER_SCRIPT=${0##*/} - -RUNNER_BASE_DIR={{runner_base_dir}} -RUNNER_ETC_DIR={{runner_etc_dir}} -RUNNER_LIB_DIR={{platform_lib_dir}} -RUNNER_LOG_DIR={{runner_log_dir}} -RUNNER_DATA_DIR=$RUNNER_BASE_DIR/data -RUNNER_PLUGINS_DIR=$RUNNER_BASE_DIR/plugins - -# Note the trailing slash on $PIPE_DIR/ -PIPE_DIR={{pipe_dir}} -RUNNER_USER={{runner_user}} -PLATFORM_DATA_DIR={{platform_data_dir}} -SSL_DIST_CONFIG=$PLATFORM_DATA_DIR/ssl_distribution.args_file -RIAK_VERSION="git" - -WHOAMI=$(whoami) - -# Make sure this script is running as the appropriate user -if ([ "$RUNNER_USER" ] && [ "x$WHOAMI" != "x$RUNNER_USER" ]); then - type sudo > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "sudo doesn't appear to be installed and your EUID isn't $RUNNER_USER" 1>&2 - exit 1 - fi - echo "Attempting to restart script through sudo -H -u $RUNNER_USER" >&2 - exec sudo -H -u $RUNNER_USER -i $RUNNER_SCRIPT_DIR/$RUNNER_SCRIPT $@ -fi +SCRIPT=$(readlink $0 || true) +if [ -z $SCRIPT ]; then + SCRIPT=$0 +fi; +SCRIPT_DIR="$(cd `dirname "$SCRIPT"` && pwd -P)" +RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +REL_NAME="emqttd" +REL_VSN="{{ rel_vsn }}" +ERTS_VSN="{{ erts_vsn }}" +CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" +REL_DIR="$RELEASE_ROOT_DIR/releases/$REL_VSN" +ERL_OPTS="{{ erl_opts }}" +RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-$RELEASE_ROOT_DIR/log}" # Warn the user if ulimit -n is less than 1024 ULIMIT_F=`ulimit -n` @@ -51,153 +26,231 @@ if [ "$ULIMIT_F" -lt 1024 ]; then echo "!!!!" fi -# Make sure CWD is set to runner base dir -cd $RUNNER_BASE_DIR +find_erts_dir() { + __erts_dir="$RELEASE_ROOT_DIR/erts-$ERTS_VSN" + if [ -d "$__erts_dir" ]; then + ERTS_DIR="$__erts_dir"; + ROOTDIR="$RELEASE_ROOT_DIR" + else + __erl="$(which erl)" + code="io:format(\"~s\", [code:root_dir()]), halt()." + __erl_root="$("$__erl" -noshell -eval "$code")" + ERTS_DIR="$__erl_root/erts-$ERTS_VSN" + ROOTDIR="$__erl_root" + fi +} + +# Get node pid +relx_get_pid() { + if output="$(relx_nodetool rpcterms os getpid)" + then + echo "$output" | sed -e 's/"//g' + return 0 + else + echo "$output" + return 1 + fi +} + +relx_get_nodename() { + id="longname$(relx_gen_id)-${NAME}" + "$BINDIR/erl" -boot start_clean -eval '[Host] = tl(string:tokens(atom_to_list(node()),"@")), io:format("~s~n", [Host]), halt()' -noshell ${NAME_TYPE} $id +} + +# Connect to a remote node +relx_rem_sh() { + # Generate a unique id used to allow multiple remsh to the same node + # transparently + id="remsh$(relx_gen_id)-${NAME}" + + # Get the node's ticktime so that we use the same thing. + TICKTIME="$(relx_nodetool rpcterms net_kernel get_net_ticktime)" + + # Setup remote shell command to control node + exec "$BINDIR/erl" "$NAME_TYPE" "$id" -remsh "$NAME" -boot start_clean \ + -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ + -setcookie "$COOKIE" -hidden -kernel net_ticktime $TICKTIME +} + +# Generate a random id +relx_gen_id() { + od -t x -N 4 /dev/urandom | head -n1 | awk '{print $2}' +} + +# Control a node +relx_nodetool() { + command="$1"; shift + + "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" "$NAME_TYPE" "$NAME" \ + -setcookie "$COOKIE" "$command" $@ +} + +# Run an escript in the node's environment +relx_escript() { + shift; scriptpath="$1"; shift + export RELEASE_ROOT_DIR + + "$ERTS_DIR/bin/escript" "$ROOTDIR/$scriptpath" $@ +} + +# Output a start command for the last argument of run_erl +relx_start_command() { + printf "exec \"%s\" \"%s\"" "$RELEASE_ROOT_DIR/bin/$REL_NAME" \ + "$START_OPTION" +} + +# Use $CWD/vm.args if exists, otherwise releases/VSN/vm.args +if [ -z "$VMARGS_PATH" ]; then + if [ -f "$RELEASE_ROOT_DIR/vm.args" ]; then + VMARGS_PATH="$RELEASE_ROOT_DIR/vm.args" + else + VMARGS_PATH="$REL_DIR/vm.args" + fi +fi + +orig_vmargs_path="$VMARGS_PATH.orig" +if [ $RELX_REPLACE_OS_VARS ]; then + #Make sure we don't break dev mode by keeping the symbolic link to + #the user's vm.args + if [ ! -L "$orig_vmargs_path" ]; then + #we're in copy mode, rename the vm.args file to vm.args.orig + mv "$VMARGS_PATH" "$orig_vmargs_path" + fi + + awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < "$orig_vmargs_path" > "$VMARGS_PATH" + else + #We don't need to replace env. vars, just rename the + #symlink vm.args.orig to vm.args, and keep it as a + #symlink. + if [ -L "$orig_vmargs_path" ]; then + mv "$orig_vmargs_path" "$VMARGS_PATH" + fi +fi # Make sure log directory exists -mkdir -p $RUNNER_LOG_DIR +mkdir -p "$RUNNER_LOG_DIR" -# Make sure the data directory exists -mkdir -p $PLATFORM_DATA_DIR +# Use $CWD/sys.config if exists, otherwise releases/VSN/sys.config +if [ -z "$RELX_CONFIG_PATH" ]; then + if [ -f "$RELEASE_ROOT_DIR/sys.config" ]; then + RELX_CONFIG_PATH="$RELEASE_ROOT_DIR/sys.config" + else + RELX_CONFIG_PATH="$REL_DIR/sys.config" + fi +fi -# Warn the user if they don't have write permissions on the log dir -if [ ! -w $RUNNER_LOG_DIR ]; then - echo "!!!!" - echo "!!!! WARNING: $RUNNER_LOG_DIR not writable; logs and crash dumps unavailable." - echo "!!!!" +orig_relx_config_path="$RELX_CONFIG_PATH.orig" +if [ $RELX_REPLACE_OS_VARS ]; then + #Make sure we don't break dev mode by keeping the symbolic link to + #the user's sys.config + if [ ! -L "$orig_relx_config_path" ]; then + #We're in copy mode, rename sys.config to sys.config.orig + mv "$RELX_CONFIG_PATH" "$orig_relx_config_path" + fi + + awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < "$orig_relx_config_path" > "$RELX_CONFIG_PATH" + else + #We don't need to replace env. vars, just rename the + #symlink sys.config.orig to sys.config. Keep it as + #a symlink. + if [ -L "$orig_relx_config_path" ]; then + mv "$orig_relx_config_path" "$RELX_CONFIG_PATH" + fi fi # Extract the target node name from node.args -NAME_ARG=`egrep '^\-s?name' $RUNNER_ETC_DIR/vm.args` +NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH" || true) if [ -z "$NAME_ARG" ]; then echo "vm.args needs to have either -name or -sname parameter." exit 1 fi -NODE_NAME=${NAME_ARG##* } + +# Extract the name type and name from the NAME_ARG for REMSH +NAME_TYPE="$(echo "$NAME_ARG" | awk '{print $1}')" +NAME="$(echo "$NAME_ARG" | awk '{print $2}')" + +PIPE_DIR="${PIPE_DIR:-/tmp/erl_pipes/$NAME/}" # Extract the target cookie -COOKIE_ARG=`grep '^\-setcookie' $RUNNER_ETC_DIR/vm.args` +COOKIE_ARG="$(grep '^-setcookie' "$VMARGS_PATH" || true)" if [ -z "$COOKIE_ARG" ]; then echo "vm.args needs to have a -setcookie parameter." exit 1 fi -# Identify the script name -SCRIPT=`basename $0` +# Extract cookie name from COOKIE_ARG +COOKIE="$(echo "$COOKIE_ARG" | awk '{print $2}')" -# Parse out release and erts info -START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` -ERTS_VSN=${START_ERL% *} -APP_VSN=${START_ERL#* } +find_erts_dir +export ROOTDIR="$RELEASE_ROOT_DIR" +export BINDIR="$ERTS_DIR/bin" +export EMU="beam" +export PROGNAME="erl" +export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH" +ERTS_LIB_DIR="$ERTS_DIR/../lib" +MNESIA_DATA_DIR="$ROOTDIR/data/mnesia/$NAME" -# Add ERTS bin dir to our path -ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin +cd "$ROOTDIR" -# Setup command to control the node -NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" -NODETOOL_LITE="$ERTS_PATH/escript $ERTS_PATH/nodetool" - -# Common functions - -# Ping node without allowing nodetool to take stdin -ping_node() { - $NODETOOL ping < /dev/null -} - -# Set the PID global variable, return 1 on error -get_pid() { - PID=`$NODETOOL getpid < /dev/null` - ES=$? - if [ "$ES" -ne 0 ]; then - echo "Node is not running!" - return 1 - fi - - # don't allow empty or init pid's - if [ -z $PID ] || [ "$PID" -le 1 ]; then - return 1 - fi - - return 0 -} - - -# Scrape out SSL distribution config info from vm.args into $SSL_DIST_CONFIG -rm -f $SSL_DIST_CONFIG -sed -n '/Begin SSL distribution items/,/End SSL distribution items/p' \ - $RUNNER_ETC_DIR/vm.args > $SSL_DIST_CONFIG +# User can specify an sname without @hostname +# This will fail when creating remote shell +# So here we check for @ and add @hostname if missing +case $NAME in + *@*) + # Nothing to do + ;; + *) + NAME=$NAME@$(relx_get_nodename) + ;; +esac # Check the first argument for instructions case "$1" in - start) - # Make sure there is not already a node running - RES=`ping_node` - if [ "$RES" = "pong" ]; then - echo "Node is already running!" - exit 1 - fi - # Sanity check the emqttd.config file - RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` - if [ $? != 0 ]; then - echo "Error reading $RUNNER_ETC_DIR/emqttd.config" - echo $RES - exit 1 - fi - HEART_COMMAND="$RUNNER_SCRIPT_DIR/$SCRIPT start" - export HEART_COMMAND - mkdir -p $PIPE_DIR - $ERTS_PATH/run_erl -daemon $PIPE_DIR $RUNNER_LOG_DIR \ - "exec $RUNNER_SCRIPT_DIR/$SCRIPT console" 2>&1 + start|start_boot) - # Wait for the node to come up. We can't just ping it because - # distributed erlang comes up for a second before emqttd crashes - # (eg. in the case of an unwriteable disk). Once the node comes - # up we check for the node watcher process. If that's running - # then we assume things are good enough. This will at least let - # the user know when emqttd is crashing right after startup. - WAIT=${WAIT_FOR_ERLANG:-15} - while [ $WAIT -gt 0 ]; do - WAIT=`expr $WAIT - 1` - sleep 1 - RES=`ping_node` - if [ "$?" -ne 0 ]; then - continue - fi - echo "emqttd is started successfully!" - exit 0 - done - echo "emqttd failed to start within ${WAIT_FOR_ERLANG:-15} seconds," - echo "see the output of 'emqttd console' for more information." - echo "If you want to wait longer, set the environment variable" - echo "WAIT_FOR_ERLANG to the number of seconds to wait." - exit 1 + # Make sure there is not already a node running + #RES=`$NODETOOL ping` + #if [ "$RES" = "pong" ]; then + # echo "Node is already running!" + # exit 1 + #fi + # Save this for later. + CMD=$1 + case "$1" in + start) + shift + START_OPTION="console" + HEART_OPTION="start" + ;; + start_boot) + shift + START_OPTION="console_boot" + HEART_OPTION="start_boot" + ;; + esac + RUN_PARAM="$@" + + # Set arguments for the heart command + set -- "$SCRIPT_DIR/$REL_NAME" "$HEART_OPTION" + [ "$RUN_PARAM" ] && set -- "$@" "$RUN_PARAM" + + # Export the HEART_COMMAND + HEART_COMMAND="$RELEASE_ROOT_DIR/bin/$REL_NAME $CMD" + export HEART_COMMAND + + mkdir -p "$PIPE_DIR" + + "$BINDIR/run_erl" -daemon "$PIPE_DIR" "$RUNNER_LOG_DIR" \ + "$(relx_start_command)" ;; stop) - UNAME_S=`uname -s` - case $UNAME_S in - Darwin) - # Make sure we explicitly set this because iTerm.app doesn't for - # some reason. - COMMAND_MODE=unix2003 - esac - - # Get the PID from nodetool - get_pid - GPR=$? - if [ "$GPR" -ne 0 ] || [ -z $PID ]; then - exit $GPR - fi - - # Tell nodetool to initiate a stop - $NODETOOL stop - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES - fi - # Wait for the node to completely stop... - while `kill -s 0 $PID 2>/dev/null`; + PID="$(relx_get_pid)" + if ! relx_nodetool "stop"; then + exit 1 + fi + while $(kill -s 0 "$PID" 2>/dev/null); do sleep 1 done @@ -205,117 +258,214 @@ case "$1" in restart) ## Restart the VM without exiting the process - $NODETOOL restart - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES + if ! relx_nodetool "restart"; then + exit 1 fi ;; reboot) ## Restart the VM completely (uses heart to restart it) - $NODETOOL reboot - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES + if ! relx_nodetool "reboot"; then + exit 1 + fi + ;; + + pid) + ## Get the VM's pid + if ! relx_get_pid; then + exit 1 fi ;; ping) ## See if the VM is alive - ping_node - ES=$? - if [ "$ES" -ne 0 ]; then - exit $ES + if ! relx_nodetool "ping"; then + exit 1 + fi + ;; + + escript) + ## Run an escript under the node's environment + if ! relx_escript $@; then + exit 1 fi ;; attach) - if [ "$2" = "-f" ]; then - echo "Forcing connection..." - else - # Make sure a node is running - RES=`ping_node` - ES=$? - if [ "$ES" -ne 0 ]; then - echo "Node is not running!" - exit $ES - fi + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" + exit 1 fi shift - exec $ERTS_PATH/to_erl $PIPE_DIR + exec "$BINDIR/to_erl" "$PIPE_DIR" ;; - console) - RES=`ping_node` - if [ "$RES" = "pong" ]; then - echo "Node is already running - use '$SCRIPT attach' instead" + remote_console) + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" exit 1 fi - # Sanity check the emqttd.config file - RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` - if [ $? != 0 ]; then - echo "Error reading $RUNNER_ETC_DIR/emqttd.config" - echo $RES + + shift + relx_rem_sh + ;; + + upgrade|downgrade|install) + if [ -z "$2" ]; then + echo "Missing package argument" + echo "Usage: $REL_NAME $1 {package base name}" + echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" exit 1 fi + + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" + exit 1 + fi + + exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ + "install" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" + ;; + + unpack) + if [ -z "$2" ]; then + echo "Missing package argument" + echo "Usage: $REL_NAME $1 {package base name}" + echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" + exit 1 + fi + + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" + exit 1 + fi + + exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ + "unpack" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" + ;; + + console|console_clean|console_boot) + # .boot file typically just $REL_NAME (ie, the app name) + # however, for debugging, sometimes start_clean.boot is useful. + # For e.g. 'setup', one may even want to name another boot script. + case "$1" in + console) + if [ -f "$REL_DIR/$REL_NAME.boot" ]; then + BOOTFILE="$REL_DIR/$REL_NAME" + else + BOOTFILE="$REL_DIR/start" + fi + ;; + console_clean) + BOOTFILE="$ROOTDIR/bin/start_clean" + ;; + console_boot) + shift + BOOTFILE="$1" + shift + ;; + esac # Setup beam-required vars - ROOTDIR=$RUNNER_BASE_DIR - ERL_LIBS=$ROOTDIR/plugins - BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin - EMU=beam - PROGNAME=`echo $0 | sed 's/.*\///'` - # Setup Mnesia Dir - MNESIA_DIR="$RUNNER_DATA_DIR/mnesia/$NODE_NAME" - CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$SCRIPT \ - -embedded -config $RUNNER_ETC_DIR/emqttd.config \ - -pa $RUNNER_LIB_DIR/basho-patches \ - -mnesia dir "\"${MNESIA_DIR}\"" \ - -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}" + EMU="beam" + PROGNAME="${0#*/}" + export EMU - export ROOTDIR - export ERL_LIBS - export BINDIR export PROGNAME + # Store passed arguments since they will be erased by `set` + ARGS="$@" + + # Build an array of arguments to pass to exec later on + # Build it here because this command will be used for logging. + set -- "$BINDIR/erlexec" -boot "$BOOTFILE" -mode "$CODE_LOADING_MODE" \ + -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ + -config "$RELX_CONFIG_PATH" \ + -mnesia dir "\"${MNESIA_DATA_DIR}\"" \ + -args_file "$VMARGS_PATH" + # Dump environment info for logging purposes - echo "Exec: $CMD" + echo "Exec: $@" -- ${1+$ARGS} echo "Root: $ROOTDIR" # Log the startup - logger -t "$SCRIPT[$$]" "Starting up" + echo "$RELEASE_ROOT_DIR" + logger -t "$REL_NAME[$$]" "Starting up" # Start the VM - exec $CMD + exec "$@" -- ${1+$ARGS} ;; - chkconfig) - RES=`$NODETOOL_LITE chkconfig $RUNNER_ETC_DIR/emqttd.config` - if [ $? != 0 ]; then - echo "Error reading $RUNNER_ETC_DIR/emqttd.config" - echo $RES + + foreground) + # start up the release in the foreground for use by runit + # or other supervision services + + [ -f "$REL_DIR/$REL_NAME.boot" ] && BOOTFILE="$REL_NAME" || BOOTFILE=start + FOREGROUNDOPTIONS="-noshell -noinput +Bd" + + # Setup beam-required vars + EMU=beam + PROGNAME="${0#*/}" + + export EMU + export PROGNAME + + # Store passed arguments since they will be erased by `set` + ARGS="$@" + + # Build an array of arguments to pass to exec later on + # Build it here because this command will be used for logging. + set -- "$BINDIR/erlexec" $FOREGROUNDOPTIONS \ + -boot "$REL_DIR/$BOOTFILE" -mode "$CODE_LOADING_MODE" -config "$RELX_CONFIG_PATH" \ + -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ + -mnesia dir "\"${MNESIA_DATA_DIR}\"" \ + -args_file "$VMARGS_PATH" + + # Dump environment info for logging purposes + echo "Exec: $@" -- ${1+$ARGS} + echo "Root: $ROOTDIR" + + # Start the VM + exec "$@" -- ${1+$ARGS} + ;; + rpc) + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" exit 1 fi - echo "config is OK" - ;; - escript) + shift - $ERTS_PATH/escript "$@" + + relx_nodetool rpc $@ ;; - version) - echo $RIAK_VERSION - ;; - getpid) - # Get the PID from nodetool - get_pid - ES=$? - if [ "$ES" -ne 0 ] || [ -z $PID ]; then - exit $ES + rpcterms) + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" + exit 1 fi - echo $PID + + shift + + relx_nodetool rpcterms $@ + ;; + eval) + # Make sure a node IS running + if ! relx_nodetool "ping" > /dev/null; then + echo "Node is not running!" + exit 1 + fi + + shift + relx_nodetool "eval" $@ ;; *) - echo "Usage: $SCRIPT {start|stop|restart|reboot|ping|console|attach|chkconfig|escript|version|getpid}" + echo "Usage: $REL_NAME {start|start_boot |foreground|stop|restart|reboot|pid|ping|console|console_clean|console_boot |attach|remote_console|upgrade|escript|rpc|rpcterms|eval}" exit 1 ;; esac diff --git a/bin/emqttd_ctl b/bin/emqttd_ctl index 4292b893e..a2d1a28bc 100755 --- a/bin/emqttd_ctl +++ b/bin/emqttd_ctl @@ -2,90 +2,82 @@ # -*- tab-width:4;indent-tabs-mode:nil -*- # ex: ts=4 sw=4 et -# /bin/sh on Solaris is not a POSIX compatible shell, but /usr/bin/ksh is. -if [ `uname -s` = 'SunOS' -a "${POSIX_SHELL}" != "true" ]; then - POSIX_SHELL="true" - export POSIX_SHELL - # To support 'whoami' add /usr/ucb to path - PATH=/usr/ucb:$PATH - export PATH - exec /usr/bin/ksh $0 "$@" -fi -unset POSIX_SHELL # clear it so if we invoke other scripts, they run as ksh as well +set -e -RUNNER_SCRIPT_DIR={{runner_script_dir}} -RUNNER_SCRIPT=${0##*/} +SCRIPT=$(readlink $0 || true) +if [ -z $SCRIPT ]; then + SCRIPT=$0 +fi; +SCRIPT_DIR="$(cd `dirname "$SCRIPT"` && pwd -P)" +RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +REL_NAME="emqttd" +REL_VSN="{{ rel_vsn }}" +ERTS_VSN="{{ erts_vsn }}" +REL_DIR="$RELEASE_ROOT_DIR/releases/$REL_VSN" +ERL_OPTS="{{ erl_opts }}" +RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-$RELEASE_ROOT_DIR/log}" -RUNNER_BASE_DIR={{runner_base_dir}} -RUNNER_ETC_DIR={{runner_etc_dir}} -RUNNER_LIB_DIR={{platform_lib_dir}} -RUNNER_LOG_DIR={{runner_log_dir}} -RUNNER_USER={{runner_user}} - -WHOAMI=$(whoami) - -# Make sure this script is running as the appropriate user -if ([ "$RUNNER_USER" ] && [ "x$WHOAMI" != "x$RUNNER_USER" ]); then - type sudo > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "sudo doesn't appear to be installed and your EUID isn't $RUNNER_USER" 1>&2 - exit 1 +find_erts_dir() { + __erts_dir="$RELEASE_ROOT_DIR/erts-$ERTS_VSN" + if [ -d "$__erts_dir" ]; then + ERTS_DIR="$__erts_dir"; + ROOTDIR="$RELEASE_ROOT_DIR" + else + __erl="$(which erl)" + code="io:format(\"~s\", [code:root_dir()]), halt()." + __erl_root="$("$__erl" -noshell -eval "$code")" + ERTS_DIR="$__erl_root/erts-$ERTS_VSN" + ROOTDIR="$__erl_root" fi - echo "Attempting to restart script through sudo -H -u $RUNNER_USER" >&2 - exec sudo -H -u $RUNNER_USER -i $RUNNER_SCRIPT_DIR/$RUNNER_SCRIPT $@ -fi +} -# Make sure CWD is set to runner base dir -cd $RUNNER_BASE_DIR +relx_get_nodename() { + id="longname$(relx_gen_id)-${NAME}" + "$BINDIR/erl" -boot start_clean -eval '[Host] = tl(string:tokens(atom_to_list(node()),"@")), io:format("~s~n", [Host]), halt()' -noshell ${NAME_TYPE} $id +} + +# Control a node +relx_nodetool() { + command="$1"; shift + + "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" "$NAME_TYPE" "$NAME" \ + -setcookie "$COOKIE" "$command" $@ +} + +# Use $CWD/vm.args if exists, otherwise releases/VSN/vm.args +if [ -z "$VMARGS_PATH" ]; then + if [ -f "$RELEASE_ROOT_DIR/vm.args" ]; then + VMARGS_PATH="$RELEASE_ROOT_DIR/vm.args" + else + VMARGS_PATH="$REL_DIR/vm.args" + fi +fi # Extract the target node name from node.args -NAME_ARG=`egrep "^ *-s?name" $RUNNER_ETC_DIR/vm.args` +NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH" || true) if [ -z "$NAME_ARG" ]; then echo "vm.args needs to have either -name or -sname parameter." exit 1 fi -# Learn how to specify node name for connection from remote nodes -echo "$NAME_ARG" | grep '^-sname' > /dev/null 2>&1 -if [ "X$?" = "X0" ]; then - NAME_PARAM="-sname" - NAME_HOST="" -else - NAME_PARAM="-name" - echo "$NAME_ARG" | grep '@.*' > /dev/null 2>&1 - if [ "X$?" = "X0" ]; then - NAME_HOST=`echo "${NAME_ARG}" | sed -e 's/.*\(@.*\)$/\1/'` - else - NAME_HOST="" - fi -fi +# Extract the name type and name from the NAME_ARG for REMSH +NAME_TYPE="$(echo "$NAME_ARG" | awk '{print $1}')" +NAME="$(echo "$NAME_ARG" | awk '{print $2}')" # Extract the target cookie -COOKIE_ARG=`grep '\-setcookie' $RUNNER_ETC_DIR/vm.args` +COOKIE_ARG="$(grep '^-setcookie' "$VMARGS_PATH" || true)" if [ -z "$COOKIE_ARG" ]; then echo "vm.args needs to have a -setcookie parameter." exit 1 fi -# Identify the script name -SCRIPT=`basename $0` +# Extract cookie name from COOKIE_ARG +COOKIE="$(echo "$COOKIE_ARG" | awk '{print $2}')" -# Parse out release and erts info -START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` -ERTS_VSN=${START_ERL% *} -APP_VSN=${START_ERL#* } +find_erts_dir +export ROOTDIR="$RELEASE_ROOT_DIR" +export BINDIR="$ERTS_DIR/bin" +cd "$ROOTDIR" -# Add ERTS bin dir to our path -ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin - -# Setup command to control the node -NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" - -RES=`$NODETOOL ping` -if [ "$RES" != "pong" ]; then - echo "Node is not running!" - exit 1 -fi - -$NODETOOL rpc emqttd_ctl run $@ +relx_nodetool rpc emqttd_ctl run $@ diff --git a/bin/emqttd_top b/bin/emqttd_top deleted file mode 100755 index 24533c436..000000000 --- a/bin/emqttd_top +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -# /bin/sh on Solaris is not a POSIX compatible shell, but /usr/bin/ksh is. -if [ `uname -s` = 'SunOS' -a "${POSIX_SHELL}" != "true" ]; then - POSIX_SHELL="true" - export POSIX_SHELL - # To support 'whoami' add /usr/ucb to path - PATH=/usr/ucb:$PATH - export PATH - exec /usr/bin/ksh $0 "$@" -fi -unset POSIX_SHELL # clear it so if we invoke other scripts, they run as ksh as well - -RUNNER_SCRIPT_DIR={{runner_script_dir}} -RUNNER_SCRIPT=${0##*/} - -RUNNER_BASE_DIR={{runner_base_dir}} -RUNNER_ETC_DIR={{runner_etc_dir}} -RUNNER_LIB_DIR={{platform_lib_dir}} -RUNNER_USER={{runner_user}} - -WHOAMI=$(whoami) - -# Make sure this script is running as the appropriate user -if ([ "$RUNNER_USER" ] && [ "x$WHOAMI" != "x$RUNNER_USER" ]); then - type sudo > /dev/null 2>&1 - if [ $? -ne 0 ]; then - echo "sudo doesn't appear to be installed and your EUID isn't $RUNNER_USER" 1>&2 - exit 1 - fi - echo "Attempting to restart script through sudo -H -u $RUNNER_USER" >&2 - exec sudo -H -u $RUNNER_USER -i $RUNNER_SCRIPT_DIR/$RUNNER_SCRIPT $@ -fi - -# Make sure CWD is set to runner base dir -cd $RUNNER_BASE_DIR - -# Extract the target node name from node.args -NAME_ARG=`egrep "^ *-s?name" $RUNNER_ETC_DIR/vm.args` -if [ -z "$NAME_ARG" ]; then - echo "vm.args needs to have either -name or -sname parameter." - exit 1 -fi - -# Learn how to specify node name for connection from remote nodes -echo "$NAME_ARG" | grep '^-sname' > /dev/null 2>&1 -if [ "X$?" = "X0" ]; then - NAME_PARAM="-sname" - NAME_HOST="" -else - NAME_PARAM="-name" - echo "$NAME_ARG" | grep '@.*' > /dev/null 2>&1 - if [ "X$?" = "X0" ]; then - NAME_HOST=`echo "${NAME_ARG}" | sed -e 's/.*\(@.*\)$/\1/'` - else - NAME_HOST="" - fi -fi - -# Extract the target cookie -COOKIE_ARG=`grep '\-setcookie' $RUNNER_ETC_DIR/vm.args` -if [ -z "$COOKIE_ARG" ]; then - echo "vm.args needs to have a -setcookie parameter." - exit 1 -fi - -# Identify the script name -SCRIPT=`basename $0` - -# Parse out release and erts info -START_ERL=`cat $RUNNER_BASE_DIR/releases/start_erl.data` -ERTS_VSN=${START_ERL% *} -APP_VSN=${START_ERL#* } - -# Add ERTS bin dir to our path -ERTS_PATH=$RUNNER_BASE_DIR/erts-$ERTS_VSN/bin - -NODE_NAME=${NAME_ARG#* } - -# Setup command to control the node -NODETOOL="$ERTS_PATH/escript $ERTS_PATH/nodetool $NAME_ARG $COOKIE_ARG" - -RES=`$NODETOOL ping` -if [ "$RES" != "pong" ]; then - echo "Node is not running!" - exit 1 -fi - -case "$1" in - runtime) - SORTBY="runtime" - ;; - reductions) - SORTBY="reductions" - ;; - memory) - SORTBY="memory" - ;; - msg_q) - SORTBY="msg_q" - ;; - *) - echo "Usage: $SCRIPT {runtime | reductions | memory | msg_q}" - exit 1 - ;; -esac - -MYPID=$$ -ETOP_ARGS="-sort $SORTBY -interval 10 -lines 50 -tracing off" -$ERTS_PATH/erl -noshell -noinput \ - -pa $RUNNER_LIB_DIR/basho-patches \ - -hidden $NAME_PARAM emqttd_top$MYPID$NAME_HOST $COOKIE_ARG \ - -s etop -s erlang halt -output text \ - -node $NODE_NAME $ETOP_ARGS - diff --git a/bin/install_upgrade.escript b/bin/install_upgrade.escript deleted file mode 100644 index 56cea1963..000000000 --- a/bin/install_upgrade.escript +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env escript -%%! -noshell -noinput -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et - --define(TIMEOUT, 60000). --define(INFO(Fmt,Args), io:format(Fmt,Args)). - -main([NodeName, Cookie, ReleasePackage]) -> - TargetNode = start_distribution(NodeName, Cookie), - {ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release, - [ReleasePackage], ?TIMEOUT), - ?INFO("Unpacked Release ~p~n", [Vsn]), - {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, - check_install_release, [Vsn], ?TIMEOUT), - {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, - install_release, [Vsn], ?TIMEOUT), - ?INFO("Installed Release ~p~n", [Vsn]), - ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), - ?INFO("Made Release ~p Permanent~n", [Vsn]); -main(_) -> - init:stop(1). - -start_distribution(NodeName, Cookie) -> - MyNode = make_script_node(NodeName), - {ok, _Pid} = net_kernel:start([MyNode, shortnames]), - erlang:set_cookie(node(), list_to_atom(Cookie)), - TargetNode = make_target_node(NodeName), - case {net_kernel:hidden_connect_node(TargetNode), - net_adm:ping(TargetNode)} of - {true, pong} -> - ok; - {_, pang} -> - io:format("Node ~p not responding to pings.\n", [TargetNode]), - init:stop(1) - end, - TargetNode. - -make_target_node(Node) -> - [_, Host] = string:tokens(atom_to_list(node()), "@"), - list_to_atom(lists:concat([Node, "@", Host])). - -make_script_node(Node) -> - list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])). diff --git a/bin/install_upgrade_escript b/bin/install_upgrade_escript new file mode 100755 index 000000000..4abce858d --- /dev/null +++ b/bin/install_upgrade_escript @@ -0,0 +1,143 @@ +#!/usr/bin/env escript +%%! -noshell -noinput +%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ft=erlang ts=4 sw=4 et + +-define(TIMEOUT, 300000). +-define(INFO(Fmt,Args), io:format(Fmt,Args)). + +%% Unpack or upgrade to a new tar.gz release +main(["unpack", RelName, NameTypeArg, NodeName, Cookie, VersionArg]) -> + TargetNode = start_distribution(NodeName, NameTypeArg, Cookie), + WhichReleases = which_releases(TargetNode), + Version = parse_version(VersionArg), + case proplists:get_value(Version, WhichReleases) of + undefined -> + %% not installed, so unpack tarball: + ?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]), + ReleasePackage = Version ++ "/" ++ RelName, + case rpc:call(TargetNode, release_handler, unpack_release, + [ReleasePackage], ?TIMEOUT) of + {ok, Vsn} -> + ?INFO("Unpacked successfully: ~p~n", [Vsn]); + {error, UnpackReason} -> + print_existing_versions(TargetNode), + ?INFO("Unpack failed: ~p~n",[UnpackReason]), + erlang:halt(2) + end; + old -> + %% no need to unpack, has been installed previously + ?INFO("Release ~s is marked old, switching to it.~n",[Version]); + unpacked -> + ?INFO("Release ~s is already unpacked, now installing.~n",[Version]); + current -> + ?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]); + permanent -> + ?INFO("Release ~s is already installed, and set permanent.~n",[Version]) + end; +main(["install", RelName, NameTypeArg, NodeName, Cookie, VersionArg]) -> + TargetNode = start_distribution(NodeName, NameTypeArg, Cookie), + WhichReleases = which_releases(TargetNode), + Version = parse_version(VersionArg), + case proplists:get_value(Version, WhichReleases) of + undefined -> + %% not installed, so unpack tarball: + ?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]), + ReleasePackage = Version ++ "/" ++ RelName, + case rpc:call(TargetNode, release_handler, unpack_release, + [ReleasePackage], ?TIMEOUT) of + {ok, Vsn} -> + ?INFO("Unpacked successfully: ~p~n", [Vsn]), + install_and_permafy(TargetNode, RelName, Vsn); + {error, UnpackReason} -> + print_existing_versions(TargetNode), + ?INFO("Unpack failed: ~p~n",[UnpackReason]), + erlang:halt(2) + end; + old -> + %% no need to unpack, has been installed previously + ?INFO("Release ~s is marked old, switching to it.~n",[Version]), + install_and_permafy(TargetNode, RelName, Version); + unpacked -> + ?INFO("Release ~s is already unpacked, now installing.~n",[Version]), + install_and_permafy(TargetNode, RelName, Version); + current -> %% installed and in-use, just needs to be permanent + ?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]), + permafy(TargetNode, RelName, Version); + permanent -> + ?INFO("Release ~s is already installed, and set permanent.~n",[Version]) + end; +main(_) -> + erlang:halt(1). + +parse_version(V) when is_list(V) -> + hd(string:tokens(V,"/")). + +install_and_permafy(TargetNode, RelName, Vsn) -> + case rpc:call(TargetNode, release_handler, check_install_release, [Vsn], ?TIMEOUT) of + {ok, _OtherVsn, _Desc} -> + ok; + {error, Reason} -> + ?INFO("ERROR: release_handler:check_install_release failed: ~p~n",[Reason]), + erlang:halt(3) + end, + case rpc:call(TargetNode, release_handler, install_release, [Vsn], ?TIMEOUT) of + {ok, _, _} -> + ?INFO("Installed Release: ~s~n", [Vsn]), + permafy(TargetNode, RelName, Vsn), + ok; + {error, {no_such_release, Vsn}} -> + VerList = + iolist_to_binary( + [io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]), + ?INFO("Installed versions:~n~s", [VerList]), + ?INFO("ERROR: Unable to revert to '~s' - not installed.~n", [Vsn]), + erlang:halt(2) + end. + +permafy(TargetNode, RelName, Vsn) -> + ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), + file:copy(filename:join(["bin", RelName++"-"++Vsn]), + filename:join(["bin", RelName])), + ?INFO("Made release permanent: ~p~n", [Vsn]), + ok. + +which_releases(TargetNode) -> + R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT), + [ {V, S} || {_,V,_, S} <- R ]. + +print_existing_versions(TargetNode) -> + VerList = iolist_to_binary([ + io_lib:format("* ~s\t~s~n",[V,S]) + || {V,S} <- which_releases(TargetNode) ]), + ?INFO("Installed versions:~n~s", [VerList]). + +start_distribution(NodeName, NameTypeArg, Cookie) -> + MyNode = make_script_node(NodeName), + {ok, _Pid} = net_kernel:start([MyNode, get_name_type(NameTypeArg)]), + erlang:set_cookie(node(), list_to_atom(Cookie)), + TargetNode = list_to_atom(NodeName), + case {net_kernel:connect_node(TargetNode), + net_adm:ping(TargetNode)} of + {true, pong} -> + ok; + {_, pang} -> + io:format("Node ~p not responding to pings.\n", [TargetNode]), + erlang:halt(1) + end, + {ok, Cwd} = file:get_cwd(), + ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT), + TargetNode. + +make_script_node(Node) -> + [Name, Host] = string:tokens(Node, "@"), + list_to_atom(lists:concat([Name, "_upgrader_", os:getpid(), "@", Host])). + +%% get name type from arg +get_name_type(NameTypeArg) -> + case NameTypeArg of + "-sname" -> + shortnames; + _ -> + longnames + end. diff --git a/bin/nodetool b/bin/nodetool index 09c2b86ef..d76c228b1 100755 --- a/bin/nodetool +++ b/bin/nodetool @@ -1,5 +1,4 @@ #!/usr/bin/env escript - %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et %% ------------------------------------------------------------------- @@ -38,8 +37,8 @@ main(Args) -> {error, {Line, Mod, Term}} -> io:format(standard_error, ["Error on line ", file:format_error({Line, Mod, Term}), "\n"], []), halt(1); - {error, R} -> - io:format(standard_error, ["Error reading config file: ", file:format_error(R), "\n"], []), + {error, Error} -> + io:format(standard_error, ["Error reading config file: ", file:format_error(Error), "\n"], []), halt(1) end; _ -> @@ -94,20 +93,48 @@ main(Args) -> end; ["rpcterms", Module, Function, ArgsAsString] -> case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), - consult(ArgsAsString), 60000) of + consult(lists:flatten(ArgsAsString)), 60000) of {badrpc, Reason} -> io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), halt(1); Other -> io:format("~p\n", [Other]) end; + ["eval" | ListOfArgs] -> + % shells may process args into more than one, and end up stripping + % spaces, so this converts all of that to a single string to parse + String = binary_to_list( + list_to_binary( + string:join(ListOfArgs," ") + ) + ), + + % then just as a convenience to users, if they forgot a trailing + % '.' add it for them. + Normalized = + case lists:reverse(String) of + [$. | _] -> String; + R -> lists:reverse([$. | R]) + end, + + % then scan and parse the string + {ok, Scanned, _} = erl_scan:string(Normalized), + {ok, Parsed } = erl_parse:parse_exprs(Scanned), + + % and evaluate it on the remote node + case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of + {value, Value, _} -> + io:format ("~p\n",[Value]); + {badrpc, Reason} -> + io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), + halt(1) + end; Other -> io:format("Other: ~p\n", [Other]), - io:format("Usage: nodetool {chkconfig|getpid|ping|stop|restart|reboot|rpc|rpc_infinity|rpcterms}\n") + io:format("Usage: nodetool {chkconfig|getpid|ping|stop|restart|reboot|rpc|rpc_infinity|rpcterms|eval [Terms]} [RPC]\n") end, net_kernel:stop(). - process_args([], Acc, TargetNode) -> {lists:reverse(Acc), TargetNode}; process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> @@ -126,7 +153,7 @@ process_args([Arg | Rest], Acc, Opts) -> start_epmd() -> - [] = os:cmd(epmd_path() ++ " -daemon"), + [] = os:cmd("\"" ++ epmd_path() ++ "\" -daemon"), ok. epmd_path() -> @@ -163,7 +190,6 @@ append_node_suffix(Name, Suffix) -> list_to_atom(lists:concat([Node, Suffix, os:getpid()])) end. - %% %% Given a string or binary, parse it into a list of terms, ala file:consult/0 %% @@ -188,7 +214,6 @@ consult(Cont, Str, Acc) -> consult(Cont1, eof, Acc) end. - %% %% Validation functions for checking the emqttd.config %% @@ -211,4 +236,3 @@ print_issue({warning, Warning}) -> print_issue({error, Error}) -> io:format(standard_error, "Error in emqttd.config: ~s~n", [Error]). - diff --git a/etc/emqttd.conf b/etc/emqttd.conf index 04daa1712..927bc1d78 100644 --- a/etc/emqttd.conf +++ b/etc/emqttd.conf @@ -1,3 +1,27 @@ +%%=================================================================== +%% +%% Config file for emqttd 2.0 +%% +%% Erlang Term Syntax: +%% +%% {}: Tuple, usually {Key, Value} +%% []: List, seperated by comma +%% %%: comment +%% +%%=================================================================== + +%%-------------------------------------------------------------------- +%% MQTT Protocol +%%-------------------------------------------------------------------- + +%% Max ClientId Length Allowed. +{mqtt_max_clientid_len, 512}. + +%% Max Packet Size Allowed, 64K by default. +{mqtt_max_packet_size, 65536}. + +%% Client Idle Timeout. +{mqtt_client_idle_timeout, 30}. % Second %%-------------------------------------------------------------------- %% Authentication @@ -11,7 +35,7 @@ {auth, username, [{passwd, "etc/passwd.conf"}, {passwd_hash, plain}]}. %% Authentication with clientId -{auth, clientid, [{clients, "etc/client.config"}, {password, no}]}. +{auth, clientid, [{config, "etc/client.config"}, {password, no}]}. %%-------------------------------------------------------------------- %% ACL @@ -42,25 +66,12 @@ {retained_max_playload_size, 65536}. %%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% Max ClientId Length Allowed. -{mqtt_max_clientid_len, 512}. - -%% Max Packet Size Allowed, 64K by default. -{mqtt_max_packet_size, 65536}. - -%% Socket Idle Timeout. -{mqtt_client_idle_timeout, 30}. % Seconds - -%%-------------------------------------------------------------------- -%% MQTT Session +%% Session %%-------------------------------------------------------------------- %% Max number of QoS 1 and 2 messages that can be “inflight” at one time. %% 0 means no limit -{session_max_inflight, 100}. +{session_max_inflight, 100}. %% Retry interval for redelivering QoS1/2 messages. {session_unack_retry_interval, 60}. @@ -101,7 +112,13 @@ {queue_qos0, true}. %%-------------------------------------------------------------------- -%% Listeners +%% Zone +%%-------------------------------------------------------------------- + +{zone, admin, []}. + +%%-------------------------------------------------------------------- +%% Listener %%-------------------------------------------------------------------- %% Plain MQTT @@ -112,6 +129,9 @@ %% Maximum number of concurrent clients {max_clients, 512}, + %% Mount point prefix + %% {mount_point, "prefix/"}, + %% Socket Access Control {access, [{allow, all}]}, @@ -132,7 +152,7 @@ ]} ]}. -%% MQTT SSL +%% MQTT/SSL {listener, mqtts, 8883, [ %% Size of acceptor pool {acceptors, 4}, @@ -179,8 +199,12 @@ %% PubSub and Router. Default should be scheduler numbers. {pubsub_pool_size, 8}. +%%-------------------------------------------------------------------- +%% Routing +%%-------------------------------------------------------------------- + %% Route aging time(seconds) -{pubsub_routing_age, 5}. +{routing_age, 5}. %%-------------------------------------------------------------------- %% Bridge @@ -196,35 +220,22 @@ %% Plugins %%------------------------------------------------------------------- -%% Plugins Dir -{plugins_dir, "./plugins"}. - %% File to store loaded plugin names. -{plugins_loaded_file, "./data/loaded_plugins"}. +{plugins_loaded_file, "data/loaded_plugins"}. -%%------------------------------------------------------------------- +%%-------------------------------------------------------------------- %% Modules -%%------------------------------------------------------------------- +%%-------------------------------------------------------------------- -%% Client presence management module. Publish presence messages when client connected or disconnected +%% Client presence management module. Publish presence messages when +%% client connected or disconnected. {module, presence, [{qos, 0}]}. %% Subscribe topics automatically when client connected {module, subscription, [{"$queue/clients/$c", 1}, backend]}. %% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) -{module, rewrite, [ - - %{topic, "x/#", [ - % {rewrite, "^x/y/(.+)$", "z/y/$1"}, - % {rewrite, "^x/(.+)$", "y/$1"} - %]}, - - %{topic, "y/+/z/#", [ - % {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} - %]} - -]}. +{module, rewrite, [{config, "etc/rewrite.conf"}]}. %%------------------------------------------------------------------- %% Erlang System Monitor diff --git a/etc/rewrite.conf b/etc/rewrite.conf new file mode 100644 index 000000000..7c1a9094f --- /dev/null +++ b/etc/rewrite.conf @@ -0,0 +1,14 @@ + +%%-------------------------------------------------------------------- +%% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) +%%-------------------------------------------------------------------- + +%{topic, "x/#", [ +% {rewrite, "^x/y/(.+)$", "z/y/$1"}, +% {rewrite, "^x/(.+)$", "y/$1"} +%]}. + +%{topic, "y/+/z/#", [ +% {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} +%]}. + diff --git a/rel/vars.config b/rel/vars.config new file mode 100644 index 000000000..9e248b663 --- /dev/null +++ b/rel/vars.config @@ -0,0 +1,19 @@ +%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ft=erlang ts=4 sw=4 et + +%% Platform-specific installation paths +{platform_bin_dir, "./bin"}. +{platform_data_dir, "./data"}. +{platform_etc_dir, "./etc"}. +{platform_lib_dir, "./lib"}. +{platform_log_dir, "./log"}. + +%% +%% bin/emqttd +%% +%% {runner_script_dir, "$(cd ${0%/*} && pwd)"}. +{runner_base_dir, "${RUNNER_SCRIPT_DIR%/*}"}. +{runner_etc_dir, "$RUNNER_BASE_DIR/etc"}. +{runner_log_dir, "$RUNNER_BASE_DIR/log"}. +{pipe_dir, "/tmp/$RUNNER_SCRIPT/"}. +{runner_user, ""}. diff --git a/relx b/relx new file mode 100755 index 0000000000000000000000000000000000000000..24466871b4b2fd64e4a4194141b97cd887971e76 GIT binary patch literal 181078 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 S5Qq=>5d%h(Z>7LM4Elc|?1o7I literal 0 HcmV?d00001 diff --git a/relx.config b/relx.config new file mode 100644 index 000000000..817c82fa7 --- /dev/null +++ b/relx.config @@ -0,0 +1,41 @@ + +{release, {emqttd, "2.0"}, [ + sasl, + os_mon, + runtime_tools, + {mnesia, load}, + emqttd +]}. + +{include_src, false}. + +{extended_start_script, false}. + +{sys_config, "rel/sys.config"}. + +{vm_args, "rel/vm.args"}. + +{overlay_vars, "./rel/vars.config"}. + +{overlay, [ + {mkdir, "etc/"}, + {mkdir, "etc/ssl/"}, + {mkdir, "data/"}, + {mkdir, "data/mnesia"}, + {mkdir, "log/"}, + {copy, "etc/ssl/ssl.crt", "etc/ssl/ssl.crt"}, + {copy, "etc/ssl/ssl.key", "etc/ssl/ssl.key"}, + + {template, "bin/emqttd", "bin/emqttd"}, + {template, "bin/emqttd_ctl", "bin/emqttd_ctl"}, + {copy, "bin/nodetool", "bin/nodetool"}, + {copy, "bin/nodetool", "erts-\{\{erts_vsn\}\}/bin/nodetool"}, + {copy, "bin/install_upgrade_escript", "bin/install_upgrade_escript"}, + + {template, "etc/acl.conf", "etc/acl.conf"}, + {template, "etc/client.conf", "etc/client.conf"}, + {template, "etc/emqttd.conf", "etc/emqttd.conf"}, + {template, "etc/passwd.conf", "etc/passwd.conf"}, + {template, "etc/rewrite.conf", "etc/rewrite.conf"} +]}. + diff --git a/src/emqttd.app.src b/src/emqttd.app.src index 9b58ce3df..e4f9fcc49 100644 --- a/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -5,7 +5,7 @@ {id, "emqttd"}, {modules, []}, {registered, []}, - {applications, [kernel, stdlib, gproc, esockd, mochiweb]}, + {applications, [kernel, stdlib, gproc, esockd, mochiweb, gen_logger, gen_conf]}, {mod, {emqttd_app, []}}, {env, []} ]}. diff --git a/src/emqttd_client.erl b/src/emqttd_client.erl index 9d29f4c68..6a3bee488 100644 --- a/src/emqttd_client.erl +++ b/src/emqttd_client.erl @@ -39,7 +39,7 @@ %% Client State -record(client_state, {connection, connname, peername, peerhost, peerport, await_recv, conn_state, rate_limit, parser_fun, - proto_state, packet_opts, keepalive}). + proto_state, packet_opts, keepalive, mountpoint}). -define(INFO_KEYS, [peername, peerhost, peerport, await_recv, conn_state]). From 2370e7dfcbc07158968550b08b775b1521b7c385 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:18:27 +0800 Subject: [PATCH 019/116] 2.0 - replace 'now/0' with 'erlang:monotonic_time/0' --- src/gen_server2.erl | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/gen_server2.erl b/src/gen_server2.erl index 5c04f3f9e..d5127f8d6 100644 --- a/src/gen_server2.erl +++ b/src/gen_server2.erl @@ -624,7 +624,7 @@ unregister_name(_Name) -> ok. extend_backoff(undefined) -> undefined; extend_backoff({backoff, InitialTimeout, MinimumTimeout, DesiredHibPeriod}) -> - {backoff, InitialTimeout, MinimumTimeout, DesiredHibPeriod, now()}. + {backoff, InitialTimeout, MinimumTimeout, DesiredHibPeriod, rand:seed(exsplus)}. %%%======================================================================== %%% Internal functions @@ -636,8 +636,10 @@ loop(GS2State = #gs2_state { time = hibernate, timeout_state = undefined, queue = Queue }) -> case priority_queue:is_empty(Queue) of - true -> pre_hibernate(GS2State); - false -> process_next_msg(GS2State) + true -> + pre_hibernate(GS2State); + false -> + process_next_msg(GS2State) end; loop(GS2State) -> @@ -693,7 +695,9 @@ wake_hib(GS2State = #gs2_state { timeout_state = TS }) -> undefined -> undefined; {SleptAt, TimeoutState} -> - adjust_timeout_state(SleptAt, now(), TimeoutState) + adjust_timeout_state(SleptAt, + erlang:monotonic_time(), + TimeoutState) end, post_hibernate( drain(GS2State #gs2_state { timeout_state = TimeoutState1 })). @@ -701,7 +705,8 @@ wake_hib(GS2State = #gs2_state { timeout_state = TS }) -> hibernate(GS2State = #gs2_state { timeout_state = TimeoutState }) -> TS = case TimeoutState of undefined -> undefined; - {backoff, _, _, _, _} -> {now(), TimeoutState} + {backoff, _, _, _, _} -> {erlang:monotonic_time(), + TimeoutState} end, proc_lib:hibernate(?MODULE, wake_hib, [GS2State #gs2_state { timeout_state = TS }]). @@ -746,7 +751,8 @@ post_hibernate(GS2State = #gs2_state { state = State, adjust_timeout_state(SleptAt, AwokeAt, {backoff, CurrentTO, MinimumTO, DesiredHibPeriod, RandomState}) -> - NapLengthMicros = timer:now_diff(AwokeAt, SleptAt), + NapLengthMicros = erlang:convert_time_unit(AwokeAt - SleptAt, + native, micro_seconds), CurrentMicros = CurrentTO * 1000, MinimumMicros = MinimumTO * 1000, DesiredHibMicros = DesiredHibPeriod * 1000, @@ -758,7 +764,7 @@ adjust_timeout_state(SleptAt, AwokeAt, {backoff, CurrentTO, MinimumTO, true -> lists:max([MinimumTO, CurrentTO div 2]); false -> CurrentTO end, - {Extra, RandomState1} = random:uniform_s(Base, RandomState), + {Extra, RandomState1} = rand:uniform_s(Base, RandomState), CurrentTO1 = Base + Extra, {backoff, CurrentTO1, MinimumTO, DesiredHibPeriod, RandomState1}. From 1602b44267554b0bf0126c38f5121a9efa4eb3cf Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:19:08 +0800 Subject: [PATCH 020/116] 2.0 - gen_conf:value/2 to read config --- src/emqttd.erl | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index 9bc455fbd..c5b697105 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -20,7 +20,7 @@ -include("emqttd_protocol.hrl"). --export([start/0, env/1, env/2, is_running/1]). +-export([start/0, conf/1, conf/2, env/1, env/2, is_running/1]). %% PubSub API -export([create/2, lookup/2, publish/1, subscribe/1, subscribe/3, @@ -39,13 +39,20 @@ -spec(start() -> ok | {error, any()}). start() -> application:start(?APP). -%% @doc Group environment --spec(env(Group :: atom()) -> list()). -env(Group) -> application:get_env(?APP, Group, []). +%% @doc Get Config +-spec(conf(Key :: atom()) -> any()). +conf(Key) -> gen_conf:value(?APP, Key). + +-spec(conf(Key :: atom(), Default :: any()) -> any()). +conf(Key, Default) -> gen_conf:value(?APP, Key, Default). + +%% @doc Environment +-spec(env(Key:: atom()) -> any()). +env(Key) -> application:get_env(?APP, Key). %% @doc Get environment --spec(env(Group :: atom(), Name :: atom()) -> undefined | any()). -env(Group, Name) -> proplists:get_value(Name, env(Group)). +-spec(env(Key:: atom(), Default:: any()) -> undefined | any()). +env(Key, Default) -> application:get_env(?APP, Key, Default). %% @doc Is running? -spec(is_running(node()) -> boolean()). From 0dbbe7d0df5441e52e3ec7ef617055e6bbe9ce1a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:23:34 +0800 Subject: [PATCH 021/116] 2.0 - read config from etc/emqttd.conf --- etc/client.conf | 6 +-- etc/emqttd.conf | 13 +++-- etc/passwd.conf | 4 +- src/emqttd_access_control.erl | 31 +++++------ src/emqttd_acl_anonymous.erl | 35 +++++++++++++ src/emqttd_acl_internal.erl | 27 ++++++---- src/emqttd_app.erl | 41 +++++++-------- src/emqttd_auth_clientid.erl | 34 ++++-------- src/emqttd_auth_username.erl | 26 ++++++--- src/emqttd_bridge_sup.erl | 2 +- src/emqttd_broker.erl | 8 +-- src/emqttd_client.erl | 10 ++-- src/emqttd_conf.erl | 99 +++++++++++++++++++++++++++++++++++ src/emqttd_mod_rewrite.erl | 15 +++--- src/emqttd_pubsub_sup.erl | 4 +- src/emqttd_retainer.erl | 2 +- src/emqttd_session.erl | 7 ++- src/emqttd_sysmon_sup.erl | 12 ++--- src/emqttd_ws.erl | 2 +- src/emqttd_ws_client.erl | 6 +-- src/emqttd_ws_client_sup.erl | 2 +- 21 files changed, 257 insertions(+), 129 deletions(-) create mode 100644 src/emqttd_acl_anonymous.erl create mode 100644 src/emqttd_conf.erl diff --git a/etc/client.conf b/etc/client.conf index 2c880c365..45d62d440 100644 --- a/etc/client.conf +++ b/etc/client.conf @@ -1,3 +1,3 @@ -testclientid0 -testclientid1 127.0.0.1 -testclientid2 192.168.0.1/24 +"testclientid0". +{"testclientid1", "127.0.0.1"}. +{"testclientid2", "192.168.0.1/24"}. diff --git a/etc/emqttd.conf b/etc/emqttd.conf index 927bc1d78..6e195fb3b 100644 --- a/etc/emqttd.conf +++ b/etc/emqttd.conf @@ -6,7 +6,7 @@ %% %% {}: Tuple, usually {Key, Value} %% []: List, seperated by comma -%% %%: comment +%% %%: Comment %% %%=================================================================== @@ -31,11 +31,10 @@ {auth, anonymous, []}. %% Authentication with username, password -%% Passwd Hash: plain | md5 | sha | sha256 -{auth, username, [{passwd, "etc/passwd.conf"}, {passwd_hash, plain}]}. +{auth, username, [{passwd, "etc/passwd.conf"}]}. %% Authentication with clientId -{auth, clientid, [{config, "etc/client.config"}, {password, no}]}. +{auth, clientid, [{config, "etc/client.conf"}, {password, no}]}. %%-------------------------------------------------------------------- %% ACL @@ -45,6 +44,9 @@ {acl, internal, [{config, "etc/acl.conf"}, {nomatch, allow}]}. +%% Cache ACL result for PUBLISH +{cache_acl, true}. + %%-------------------------------------------------------------------- %% Broker %%-------------------------------------------------------------------- @@ -220,6 +222,9 @@ %% Plugins %%------------------------------------------------------------------- +%% Dir of plugins' config +{plugins_etc_dir, "etc/plugins/"}. + %% File to store loaded plugin names. {plugins_loaded_file, "data/loaded_plugins"}. diff --git a/etc/passwd.conf b/etc/passwd.conf index e6998746d..89ac8ffe2 100644 --- a/etc/passwd.conf +++ b/etc/passwd.conf @@ -1,2 +1,2 @@ -user1:passwd1 -user2:passwd2 +{"user1", "passwd1"}. +{"user2", "passwd2"}. diff --git a/src/emqttd_access_control.erl b/src/emqttd_access_control.erl index 47d6417d4..079bdbec3 100644 --- a/src/emqttd_access_control.erl +++ b/src/emqttd_access_control.erl @@ -23,7 +23,7 @@ -define(SERVER, ?MODULE). %% API Function Exports --export([start_link/0, start_link/1, +-export([start_link/0, auth/2, % authentication check_acl/3, % acl check reload_acl/0, % reload acl @@ -48,11 +48,8 @@ %% @doc Start access control server. -spec(start_link() -> {ok, pid()} | ignore | {error, any()}). -start_link() -> start_link(emqttd:env(access)). - --spec(start_link(Opts :: list()) -> {ok, pid()} | ignore | {error, any()}). -start_link(Opts) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [Opts], []). +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% @doc Authenticate MQTT Client. -spec(auth(Client :: mqtt_client(), Password :: password()) -> ok | {error, any()}). @@ -125,17 +122,14 @@ stop() -> gen_server:call(?MODULE, stop). %% gen_server callbacks %%-------------------------------------------------------------------- -init([Opts]) -> +init([]) -> ets:new(?ACCESS_CONTROL_TAB, [set, named_table, protected, {read_concurrency, true}]), - ets:insert(?ACCESS_CONTROL_TAB, {auth_modules, init_mods(auth, proplists:get_value(auth, Opts))}), - ets:insert(?ACCESS_CONTROL_TAB, {acl_modules, init_mods(acl, proplists:get_value(acl, Opts))}), + ets:insert(?ACCESS_CONTROL_TAB, {auth_modules, init_mods(gen_conf:list(emqttd, auth))}), + ets:insert(?ACCESS_CONTROL_TAB, {acl_modules, init_mods(gen_conf:list(emqttd, acl))}), {ok, #state{}}. -init_mods(auth, AuthMods) -> - [init_mod(authmod(Name), Opts) || {Name, Opts} <- AuthMods]; - -init_mods(acl, AclMods) -> - [init_mod(aclmod(Name), Opts) || {Name, Opts} <- AclMods]. +init_mods(Mods) -> + [init_mod(mod_name(Type, Name), Opts) || {Type, Name, Opts} <- Mods]. init_mod(Mod, Opts) -> {ok, State} = Mod:init(Opts), {Mod, State, 0}. @@ -191,15 +185,14 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -authmod(Name) when is_atom(Name) -> - mod(emqttd_auth_, Name). - -aclmod(Name) when is_atom(Name) -> - mod(emqttd_acl_, Name). +mod_name(auth, Name) -> mod(emqttd_auth_, Name); +mod_name(acl, Name) -> mod(emqttd_acl_, Name). + mod(Prefix, Name) -> list_to_atom(lists:concat([Prefix, Name])). if_existed(false, Fun) -> Fun(); + if_existed(_Mod, _Fun) -> {error, already_existed}. diff --git a/src/emqttd_acl_anonymous.erl b/src/emqttd_acl_anonymous.erl new file mode 100644 index 000000000..ef80457fd --- /dev/null +++ b/src/emqttd_acl_anonymous.erl @@ -0,0 +1,35 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_acl_anonymous). + +-behaviour(emqttd_acl_mod). + +%% ACL callbacks +-export([init/1, check_acl/2, reload_acl/1, description/0]). + +init(Opts) -> + {ok, Opts}. + +check_acl(_Who, _State) -> + allow. + +reload_acl(_State) -> + ok. + +description() -> + "Anonymous ACL". + diff --git a/src/emqttd_acl_internal.erl b/src/emqttd_acl_internal.erl index eba5bd905..ff1efcd16 100644 --- a/src/emqttd_acl_internal.erl +++ b/src/emqttd_acl_internal.erl @@ -27,7 +27,7 @@ -define(ACL_RULE_TAB, mqtt_acl_rule). --record(state, {acl_file, nomatch = allow}). +-record(state, {config, nomatch = allow}). %%-------------------------------------------------------------------- %% API @@ -46,16 +46,20 @@ all_rules() -> %%-------------------------------------------------------------------- %% @doc Init internal ACL --spec(init(AclOpts :: list()) -> {ok, State :: any()}). -init(AclOpts) -> +-spec(init(Opts :: list()) -> {ok, State :: any()}). +init(Opts) -> ets:new(?ACL_RULE_TAB, [set, public, named_table, {read_concurrency, true}]), - AclFile = proplists:get_value(file, AclOpts), - Default = proplists:get_value(nomatch, AclOpts, allow), - State = #state{acl_file = AclFile, nomatch = Default}, - true = load_rules_from_file(State), - {ok, State}. + case proplists:get_value(config, Opts) of + undefined -> + {ok, #state{}}; + File -> + Default = proplists:get_value(nomatch, Opts, allow), + State = #state{config = File, nomatch = Default}, + true = load_rules_from_file(State), + {ok, State} + end. -load_rules_from_file(#state{acl_file = AclFile}) -> +load_rules_from_file(#state{config = AclFile}) -> {ok, Terms} = file:consult(AclFile), Rules = [emqttd_access_rule:compile(Term) || Term <- Terms], lists:foreach(fun(PubSub) -> @@ -83,6 +87,8 @@ filter(_PubSub, {_AllowDeny, _Who, _, _Topics}) -> PubSub :: pubsub(), Topic :: binary(), State :: #state{}). +check_acl(_Who, #state{config = undefined}) -> + allow; check_acl({Client, PubSub, Topic}, #state{nomatch = Default}) -> case match(Client, Topic, lookup(PubSub)) of {matched, allow} -> allow; @@ -115,5 +121,6 @@ reload_acl(State) -> %% @doc ACL Module Description -spec(description() -> string()). -description() -> "Internal ACL with etc/acl.config". +description() -> + "Internal ACL with etc/acl.conf". diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 277d0fd4d..870a349a6 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -1,4 +1,4 @@ -%%-------------------------------------------------------------------- +%-------------------------------------------------------------------- %% Copyright (c) 2012-2016 Feng Lee . %% %% Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,12 +26,8 @@ -export([start_listener/1, stop_listener/1, is_mod_enabled/1]). %% MQTT SockOpts --define(MQTT_SOCKOPTS, [ - binary, - {packet, raw}, - {reuseaddr, true}, - {backlog, 512}, - {nodelay, true}]). +-define(MQTT_SOCKOPTS, [binary, {packet, raw}, {reuseaddr, true}, + {backlog, 512}, {nodelay, true}]). -type listener() :: {atom(), esockd:listen_on(), [esockd:option()]}. @@ -102,17 +98,17 @@ start_servers(Sup) -> start_server(_Sup, {Name, F}) when is_function(F) -> ?PRINT("~s is starting...", [Name]), F(), - ?PRINT_MSG("[done]~n"); + ?PRINT_MSG("[ok]~n"); start_server(Sup, {Name, Server}) -> ?PRINT("~s is starting...", [Name]), start_child(Sup, Server), - ?PRINT_MSG("[done]~n"); + ?PRINT_MSG("[ok]~n"); start_server(Sup, {Name, Server, Opts}) -> ?PRINT("~s is starting...", [ Name]), start_child(Sup, Server, Opts), - ?PRINT_MSG("[done]~n"). + ?PRINT_MSG("[ok]~n"). start_child(Sup, {supervisor, Module}) -> supervisor:start_child(Sup, supervisor_spec(Module)); @@ -150,9 +146,9 @@ worker_spec(M, F, A) -> %% @doc load all modules load_all_mods() -> - lists:foreach(fun load_mod/1, emqttd:env(modules)). + lists:foreach(fun load_mod/1, gen_conf:list(emqttd, module)). -load_mod({Name, Opts}) -> +load_mod({module, Name, Opts}) -> Mod = list_to_atom("emqttd_mod_" ++ atom_to_list(Name)), case catch Mod:load(Opts) of ok -> lager:info("Load module ~s successfully", [Name]); @@ -162,7 +158,8 @@ load_mod({Name, Opts}) -> %% @doc Is module enabled? -spec(is_mod_enabled(Name :: atom()) -> boolean()). -is_mod_enabled(Name) -> emqttd:env(modules, Name) =/= undefined. +is_mod_enabled(Name) -> + lists:keyfind(Name, 2, gen_conf:list(emqttd, module)). %%-------------------------------------------------------------------- %% Start Listeners @@ -170,25 +167,27 @@ is_mod_enabled(Name) -> emqttd:env(modules, Name) =/= undefined. %% @doc Start Listeners of the broker. -spec(start_listeners() -> any()). -start_listeners() -> lists:foreach(fun start_listener/1, emqttd:env(listeners)). +start_listeners() -> lists:foreach(fun start_listener/1, gen_conf:list(emqttd, listener)). %% Start mqtt listener -spec(start_listener(listener()) -> any()). -start_listener({mqtt, ListenOn, Opts}) -> start_listener(mqtt, ListenOn, Opts); +start_listener({listener, mqtt, ListenOn, Opts}) -> + start_listener(mqtt, ListenOn, Opts); %% Start mqtt(SSL) listener -start_listener({mqtts, ListenOn, Opts}) -> start_listener(mqtts, ListenOn, Opts); +start_listener({listener, mqtts, ListenOn, Opts}) -> + start_listener(mqtts, ListenOn, Opts); %% Start http listener -start_listener({http, ListenOn, Opts}) -> +start_listener({listener, http, ListenOn, Opts}) -> mochiweb:start_http(http, ListenOn, Opts, {emqttd_http, handle_request, []}); %% Start https listener -start_listener({https, ListenOn, Opts}) -> +start_listener({listener, https, ListenOn, Opts}) -> mochiweb:start_http(https, ListenOn, Opts, {emqttd_http, handle_request, []}). start_listener(Protocol, ListenOn, Opts) -> - MFArgs = {emqttd_client, start_link, [emqttd:env(mqtt)]}, + MFArgs = {emqttd_client, start_link, [emqttd_conf:mqtt()]}, {ok, _} = esockd:open(Protocol, ListenOn, merge_sockopts(Opts), MFArgs). merge_sockopts(Options) -> @@ -201,8 +200,8 @@ merge_sockopts(Options) -> %%-------------------------------------------------------------------- %% @doc Stop Listeners -stop_listeners() -> lists:foreach(fun stop_listener/1, emqttd:env(listeners)). +stop_listeners() -> lists:foreach(fun stop_listener/1, gen_conf:list(listener)). %% @private -stop_listener({Protocol, ListenOn, _Opts}) -> esockd:close(Protocol, ListenOn). +stop_listener({listener, Protocol, ListenOn, _Opts}) -> esockd:close(Protocol, ListenOn). diff --git a/src/emqttd_auth_clientid.erl b/src/emqttd_auth_clientid.erl index 35b71035b..15a751ea8 100644 --- a/src/emqttd_auth_clientid.erl +++ b/src/emqttd_auth_clientid.erl @@ -69,7 +69,8 @@ init(Opts) -> {ram_copies, [node()]}, {attributes, record_info(fields, ?AUTH_CLIENTID_TAB)}]), mnesia:add_table_copy(?AUTH_CLIENTID_TAB, node(), ram_copies), - load(proplists:get_value(file, Opts)), + Clients = load_client_from(proplists:get_value(config, Opts)), + mnesia:transaction(fun() -> [mnesia:write(C) || C<- Clients] end), {ok, Opts}. check(#mqtt_client{client_id = undefined}, _Password, _Opts) -> @@ -93,32 +94,19 @@ description() -> "ClientId authentication module". %% Internal functions %%-------------------------------------------------------------------- -load(undefined) -> +load_client_from(undefined) -> ok; -load(File) -> - {ok, Fd} = file:open(File, [read]), - load(Fd, file:read_line(Fd), []). +load_client_from(File) -> + {ok, Clients} = file:consult(File), + [client(Client) || Client <- Clients]. -load(Fd, {ok, Line}, Clients) when is_list(Line) -> - Clients1 = - case string:tokens(Line, " ") of - [ClientIdS] -> - ClientId = list_to_binary(string:strip(ClientIdS, right, $\n)), - [#mqtt_auth_clientid{client_id = ClientId} | Clients]; - [ClientId, IpAddr0] -> - IpAddr = string:strip(IpAddr0, right, $\n), - [#mqtt_auth_clientid{client_id = list_to_binary(ClientId), - ipaddr = esockd_cidr:parse(IpAddr, true)} | Clients]; - BadLine -> - lager:error("BadLine in clients.config: ~s", [BadLine]), - Clients - end, - load(Fd, file:read_line(Fd), Clients1); +client(ClientId) when is_list(ClientId) -> + #mqtt_auth_clientid{client_id = list_to_binary(ClientId)}; -load(Fd, eof, Clients) -> - mnesia:transaction(fun() -> [mnesia:write(C) || C<- Clients] end), - file:close(Fd). +client({ClientId, IpAddr}) when is_list(ClientId) -> + #mqtt_auth_clientid{client_id = iolist_to_binary(ClientId), + ipaddr = esockd_cidr:parse(IpAddr, true)}. check_clientid_only(ClientId, IpAddr) -> case mnesia:dirty_read(?AUTH_CLIENTID_TAB, ClientId) of diff --git a/src/emqttd_auth_username.erl b/src/emqttd_auth_username.erl index 3c59b7c20..6a0b1d17f 100644 --- a/src/emqttd_auth_username.erl +++ b/src/emqttd_auth_username.erl @@ -68,7 +68,7 @@ if_enabled(Fun) -> end. hint() -> - ?PRINT_MSG("Please enable '{username, []}' authentication in etc/emqttd.config first.~n"). + ?PRINT_MSG("Please enable '{auth, username, []}' in etc/emqttd.conf first.~n"). %%-------------------------------------------------------------------- %% API @@ -81,7 +81,13 @@ is_enabled() -> -spec(add_user(binary(), binary()) -> ok | {error, any()}). add_user(Username, Password) -> User = #?AUTH_USERNAME_TAB{username = Username, password = hash(Password)}, - ret(mnesia:transaction(fun mnesia:write/1, [User])). + ret(mnesia:transaction(fun insert_user/1, [User])). + +insert_user(User = #?AUTH_USERNAME_TAB{username = Username}) -> + case mnesia:read(?AUTH_USERNAME_TAB, Username) of + [] -> mnesia:write(User); + [_|_] -> mnesia:abort(existed) + end. add_default_user(Username, Password) when is_atom(Username) -> add_default_user(atom_to_list(Username), Password); @@ -110,16 +116,20 @@ all_users() -> mnesia:dirty_all_keys(?AUTH_USERNAME_TAB). %% emqttd_auth_mod callbacks %%-------------------------------------------------------------------- -init(DefautUsers) -> +init(Opts) -> mnesia:create_table(?AUTH_USERNAME_TAB, [ {disc_copies, [node()]}, {attributes, record_info(fields, ?AUTH_USERNAME_TAB)}]), mnesia:add_table_copy(?AUTH_USERNAME_TAB, node(), disc_copies), - lists:foreach(fun({Username, Password}) -> - add_default_user(Username, Password) - end, DefautUsers), + case proplists:get_value(passwd, Opts) of + undefined -> ok; + File -> {ok, DefaultUsers} = file:consult(File), + lists:foreach(fun({Username, Password}) -> + add_default_user(Username, Password) + end, DefaultUsers) + end, emqttd_ctl:register_cmd(users, {?MODULE, cli}, []), - {ok, []}. + {ok, Opts}. check(#mqtt_client{username = undefined}, _Password, _Opts) -> {error, username_undefined}; @@ -127,7 +137,7 @@ check(_User, undefined, _Opts) -> {error, password_undefined}; check(#mqtt_client{username = Username}, Password, _Opts) -> case mnesia:dirty_read(?AUTH_USERNAME_TAB, Username) of - [] -> + [] -> {error, username_not_found}; [#?AUTH_USERNAME_TAB{password = <>}] -> case Hash =:= md5_hash(Salt, Password) of diff --git a/src/emqttd_bridge_sup.erl b/src/emqttd_bridge_sup.erl index 2d7da927c..dca66a8b6 100644 --- a/src/emqttd_bridge_sup.erl +++ b/src/emqttd_bridge_sup.erl @@ -47,7 +47,7 @@ start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> start_bridge(Node, _Topic, _Options) when Node =:= node() -> {error, bridge_to_self}; start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> - Options1 = emqttd_opts:merge(emqttd_broker:env(bridge), Options), + Options1 = emqttd_opts:merge(emqttd_conf:bridge(), Options), supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). %% @doc Stop a bridge diff --git a/src/emqttd_broker.erl b/src/emqttd_broker.erl index d95800954..0bc27afa1 100644 --- a/src/emqttd_broker.erl +++ b/src/emqttd_broker.erl @@ -29,7 +29,7 @@ -export([subscribe/1, notify/2]). %% Broker API --export([env/1, version/0, uptime/0, datetime/0, sysdescr/0]). +-export([version/0, uptime/0, datetime/0, sysdescr/0]). %% Tick API -export([start_tick/1, stop_tick/1]). @@ -71,10 +71,6 @@ subscribe(EventType) -> notify(EventType, Event) -> gproc:send({p, l, {broker, EventType}}, {notify, EventType, self(), Event}). -%% @doc Get broker env -env(Name) -> - proplists:get_value(Name, emqttd:env(broker)). - %% @doc Get broker version -spec(version() -> string()). version() -> @@ -99,7 +95,7 @@ datetime() -> %% @doc Start a tick timer start_tick(Msg) -> - start_tick(timer:seconds(env(sys_interval)), Msg). + start_tick(timer:seconds(emqttd:conf(broker_sys_interval, 60)), Msg). start_tick(0, _Msg) -> undefined; diff --git a/src/emqttd_client.erl b/src/emqttd_client.erl index 6a3bee488..abca547c7 100644 --- a/src/emqttd_client.erl +++ b/src/emqttd_client.erl @@ -94,9 +94,8 @@ init([OriginConn, MqttEnv]) -> error:Error -> Self ! {shutdown, Error} end end, - PktOpts = proplists:get_value(packet, MqttEnv), - ParserFun = emqttd_parser:new(PktOpts), - ProtoState = emqttd_protocol:init(PeerName, SendFun, PktOpts), + ParserFun = emqttd_parser:new(MqttEnv), + ProtoState = emqttd_protocol:init(PeerName, SendFun, MqttEnv), RateLimit = proplists:get_value(rate_limit, Connection:opts()), State = run_socket(#client_state{connection = Connection, connname = ConnName, @@ -108,9 +107,8 @@ init([OriginConn, MqttEnv]) -> rate_limit = RateLimit, parser_fun = ParserFun, proto_state = ProtoState, - packet_opts = PktOpts}), - ClientOpts = proplists:get_value(client, MqttEnv), - IdleTimout = proplists:get_value(idle_timeout, ClientOpts, 10), + packet_opts = MqttEnv}), + IdleTimout = proplists:get_value(client_idle_timeout, MqttEnv, 30), gen_server:enter_loop(?MODULE, [], State, timer:seconds(IdleTimout)). handle_call(session, _From, State = #client_state{proto_state = ProtoState}) -> diff --git a/src/emqttd_conf.erl b/src/emqttd_conf.erl new file mode 100644 index 000000000..c4a78fd9b --- /dev/null +++ b/src/emqttd_conf.erl @@ -0,0 +1,99 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_conf). + +-export([mqtt/0, retained/0, session/0, queue/0, bridge/0, pubsub/0]). + +mqtt() -> + [ + %% Max ClientId Length Allowed. + {max_clientid_len, emqttd:conf(mqtt_max_clientid_len, 512)}, + %% Max Packet Size Allowed, 64K by default. + {max_packet_size, emqttd:conf(mqtt_max_packet_size, 65536)}, + %% Client Idle Timeout. + {client_idle_timeout, emqttd:conf(mqtt_client_idle_timeout, 30)} + ]. + +retained() -> + [ + %% Expired after seconds, never expired if 0 + {expired_after, emqttd:conf(retained_expired_after, 0)}, + %% Max number of retained messages + {max_message_num, emqttd:conf(retained_max_message_num, 100000)}, + %% Max Payload Size of retained message + {max_playload_size, emqttd:conf(retained_max_playload_size, 65536)} + ]. + +session() -> + [ + %% Max number of QoS 1 and 2 messages that can be “inflight” at one time. + %% 0 means no limit + {max_inflight, emqttd:conf(session_max_inflight, 100)}, + + %% Retry interval for redelivering QoS1/2 messages. + {unack_retry_interval, emqttd:conf(session_unack_retry_interval, 60)}, + + %% Awaiting PUBREL Timeout + {await_rel_timeout, emqttd:conf(session_await_rel_timeout, 20)}, + + %% Max Packets that Awaiting PUBREL, 0 means no limit + {max_awaiting_rel, emqttd:conf(session_max_awaiting_rel, 0)}, + + %% Statistics Collection Interval(seconds) + {collect_interval, emqttd:conf(session_collect_interval, 0)}, + + %% Expired after 2 day (unit: minute) + {expired_after, emqttd:conf(session_expired_after, 2880)} + ]. + +queue() -> + [ + %% Type: simple | priority + {type, emqttd:conf(queue_type, simple)}, + + %% Topic Priority: 0~255, Default is 0 + {priority, emqttd:conf(queue_priority, [])}, + + %% Max queue length. Enqueued messages when persistent client disconnected, + %% or inflight window is full. + {max_length, emqttd:conf(queue_max_length, infinity)}, + + %% Low-water mark of queued messages + {low_watermark, emqttd:conf(queue_low_watermark, 0.2)}, + + %% High-water mark of queued messages + {high_watermark, emqttd:conf(queue_high_watermark, 0.6)}, + + %% Queue Qos0 messages? + {queue_qos0, emqttd:conf(queue_qos0, true)} + ]. + +bridge() -> + [ + %% TODO: Bridge Queue Size + {max_queue_len, emqttd:conf(bridge_max_queue_len, 10000)}, + + %% Ping Interval of bridge node + {ping_down_interval, emqttd:conf(bridge_ping_down_interval, 1)} + ]. + +pubsub() -> + [ + %% PubSub and Router. Default should be scheduler numbers. + {pool_size, emqttd:conf(pubsub_pool_size, 8)} + ]. + diff --git a/src/emqttd_mod_rewrite.erl b/src/emqttd_mod_rewrite.erl index 9109d2155..d23654bdc 100644 --- a/src/emqttd_mod_rewrite.erl +++ b/src/emqttd_mod_rewrite.erl @@ -30,12 +30,15 @@ %%-------------------------------------------------------------------- load(Opts) -> - File = proplists:get_value(file, Opts), - {ok, Terms} = file:consult(File), - Sections = compile(Terms), - emqttd:hook('client.subscribe', fun ?MODULE:rewrite_subscribe/3, [Sections]), - emqttd:hook('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3, [Sections]), - emqttd:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Sections]). + case proplists:get_value(config, Opts) of + undefined -> + ok; + File -> + {ok, Terms} = file:consult(File), Sections = compile(Terms), + emqttd:hook('client.subscribe', fun ?MODULE:rewrite_subscribe/3, [Sections]), + emqttd:hook('client.unsubscribe', fun ?MODULE:rewrite_unsubscribe/3, [Sections]), + emqttd:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Sections]) + end. rewrite_subscribe(_ClientId, TopicTable, Sections) -> lager:info("Rewrite subscribe: ~p", [TopicTable]), diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 28ff8033b..1677cec8d 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -30,10 +30,10 @@ -export([init/1]). start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [emqttd_broker:env(pubsub)]). + supervisor:start_link({local, ?MODULE}, ?MODULE, [emqttd_conf:pubsub()]). pubsub_pool() -> - hd([Pid|| {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). + hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). init([Env]) -> diff --git a/src/emqttd_retainer.erl b/src/emqttd_retainer.erl index e0bb631ba..0b239bec5 100644 --- a/src/emqttd_retainer.erl +++ b/src/emqttd_retainer.erl @@ -71,7 +71,7 @@ limit(payload) -> env(max_playload_size). env(Key) -> case get({retained, Key}) of undefined -> - Env = emqttd_broker:env(retained), + Env = emqttd_conf:retained(), Val = proplists:get_value(Key, Env), put({retained, Key}, Val), Val; Val -> diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index ac7af465b..6e0586b94 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -214,8 +214,7 @@ unsubscribe(SessPid, Topics) -> init([CleanSess, ClientId, ClientPid]) -> process_flag(trap_exit, true), true = link(ClientPid), - QEnv = emqttd:env(mqtt, queue), - SessEnv = emqttd:env(mqtt, session), + SessEnv = emqttd_conf:session(), Session = #session{ clean_sess = CleanSess, client_id = ClientId, @@ -223,7 +222,7 @@ init([CleanSess, ClientId, ClientPid]) -> subscriptions = dict:new(), inflight_queue = [], max_inflight = get_value(max_inflight, SessEnv, 0), - message_queue = emqttd_mqueue:new(ClientId, QEnv, emqttd_alarm:alarm_fun()), + message_queue = emqttd_mqueue:new(ClientId, emqttd_conf:queue(), emqttd_alarm:alarm_fun()), awaiting_rel = #{}, awaiting_ack = #{}, awaiting_comp = #{}, @@ -234,7 +233,7 @@ init([CleanSess, ClientId, ClientPid]) -> collect_interval = get_value(collect_interval, SessEnv, 0), timestamp = os:timestamp()}, emqttd_sm:register_session(CleanSess, ClientId, sess_info(Session)), - %% start statistics + %% Start statistics {ok, start_collector(Session), hibernate}. prioritise_call(Msg, _From, _Len, _State) -> diff --git a/src/emqttd_sysmon_sup.erl b/src/emqttd_sysmon_sup.erl index 883dab20e..3ed8e36a5 100644 --- a/src/emqttd_sysmon_sup.erl +++ b/src/emqttd_sysmon_sup.erl @@ -33,12 +33,10 @@ init([]) -> {ok, {{one_for_one, 10, 100}, [Sysmon]}}. opts() -> - Opts = [{long_gc, config(sysmon_long_gc)}, - {long_schedule, config(sysmon_long_schedule)}, - {large_heap, config(sysmon_large_heap)}, - {busy_port, config(busy_port)}, - {busy_dist_port, config(sysmon_busy_dist_port)}], + Opts = [{long_gc, emqttd:conf(sysmon_long_gc)}, + {long_schedule, emqttd:conf(sysmon_long_schedule)}, + {large_heap, emqttd:conf(sysmon_large_heap)}, + {busy_port, emqttd:conf(busy_port)}, + {busy_dist_port, emqttd:conf(sysmon_busy_dist_port)}], [{Key, Val} || {Key, {ok, Val}} <- Opts]. -config(Key) -> gen_conf:value(emqttd, Key). - diff --git a/src/emqttd_ws.erl b/src/emqttd_ws.erl index 60371457f..56a4d92d9 100644 --- a/src/emqttd_ws.erl +++ b/src/emqttd_ws.erl @@ -31,7 +31,7 @@ %% @doc Handle WebSocket Request. handle_request(Req) -> Peer = Req:get(peer), - PktOpts = emqttd:env(mqtt, packet), + PktOpts = emqttd_conf:mqtt(), ParserFun = emqttd_parser:new(PktOpts), {ReentryWs, ReplyChannel} = upgrade(Req), {ok, ClientPid} = emqttd_ws_client_sup:start_client(self(), Req, ReplyChannel), diff --git a/src/emqttd_ws_client.erl b/src/emqttd_ws_client.erl index ff4c16e79..6803c668c 100644 --- a/src/emqttd_ws_client.erl +++ b/src/emqttd_ws_client.erl @@ -66,17 +66,15 @@ init([MqttEnv, WsPid, Req, ReplyChannel]) -> {ok, Peername} = Req:get(peername), Headers = mochiweb_headers:to_list( mochiweb_request:get(headers, Req)), - PktOpts = proplists:get_value(packet, MqttEnv), SendFun = fun(Payload) -> ReplyChannel({binary, Payload}) end, ProtoState = emqttd_protocol:init(Peername, SendFun, - [{ws_initial_headers, Headers} | PktOpts]), + [{ws_initial_headers, Headers} | MqttEnv]), {ok, #wsclient_state{ws_pid = WsPid, peer = Req:get(peer), connection = Req:get(connection), proto_state = ProtoState}, idle_timeout(MqttEnv)}. idle_timeout(MqttEnv) -> - ClientOpts = proplists:get_value(client, MqttEnv), - timer:seconds(proplists:get_value(idle_timeout, ClientOpts, 10)). + timer:seconds(proplists:get_value(client_idle_timeout, MqttEnv, 10)). handle_call(session, _From, State = #wsclient_state{proto_state = ProtoState}) -> {reply, emqttd_protocol:session(ProtoState), State}; diff --git a/src/emqttd_ws_client_sup.erl b/src/emqttd_ws_client_sup.erl index 3577527a6..33983fd8c 100644 --- a/src/emqttd_ws_client_sup.erl +++ b/src/emqttd_ws_client_sup.erl @@ -27,7 +27,7 @@ %% @doc Start websocket client supervisor -spec(start_link() -> {ok, pid()}). start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [emqttd:env(mqtt)]). + supervisor:start_link({local, ?MODULE}, ?MODULE, [emqttd_conf:mqtt()]). %% @doc Start a WebSocket Client -spec(start_client(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}). From 9555b0bf055a264c4ff7f040c15129af79999dfc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:51:02 +0800 Subject: [PATCH 022/116] 2.0 - support 'cache_acl' option --- src/emqttd_protocol.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 7c8a87146..63a6ec821 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -397,13 +397,16 @@ validate_qos(_) -> %% PUBLISH ACL is cached in process dictionary. check_acl(publish, Topic, Client) -> - case get({acl, publish, Topic}) of - undefined -> + IfCache = emqttd:conf(cache_acl, true), + case {IfCache, get({acl, publish, Topic})} of + {true, undefined} -> AllowDeny = emqttd_access_control:check_acl(Client, publish, Topic), put({acl, publish, Topic}, AllowDeny), AllowDeny; - AllowDeny -> - AllowDeny + {true, AllowDeny} -> + AllowDeny; + {false, _} -> + emqttd_access_control:check_acl(Client, publish, Topic) end; check_acl(subscribe, Topic, Client) -> From 68910aa403280cf2509c860a8c2ec68222ad8a27 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:51:28 +0800 Subject: [PATCH 023/116] 2.0 - gen_conf, gen_logger applications --- src/emqttd.app.src | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqttd.app.src b/src/emqttd.app.src index e4f9fcc49..1a8dc1d15 100644 --- a/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -5,7 +5,8 @@ {id, "emqttd"}, {modules, []}, {registered, []}, - {applications, [kernel, stdlib, gproc, esockd, mochiweb, gen_logger, gen_conf]}, + {applications, [kernel, stdlib, gproc, esockd, mochiweb, + gen_logger, gen_conf]}, {mod, {emqttd_app, []}}, {env, []} ]}. From 3e42f57bc6359d6916985ef1d43cf74b4c5da47a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:52:08 +0800 Subject: [PATCH 024/116] 2.0 - emqttd:conf/1 to read config --- src/emqttd_plugins.erl | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/emqttd_plugins.erl b/src/emqttd_plugins.erl index f0d31be0a..b67810732 100644 --- a/src/emqttd_plugins.erl +++ b/src/emqttd_plugins.erl @@ -27,14 +27,18 @@ %% @doc Load all plugins when the broker started. -spec(load() -> list() | {error, any()}). load() -> - case env(loaded_file) of + case emqttd:conf(plugins_loaded_file) of {ok, File} -> + ensure_file(File), with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); undefined -> %% No plugins available ignore end. +ensure_file(File) -> + case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. + with_loaded_file(File, SuccFun) -> case read_loaded(File) of {ok, Names} -> @@ -56,7 +60,7 @@ load_plugins(Names, Persistent) -> %% @doc Unload all plugins before broker stopped. -spec(unload() -> list() | {error, any()}). unload() -> - case env(loaded_file) of + case emqttd:conf(plugins_loaded_file) of {ok, File} -> with_loaded_file(File, fun stop_plugins/1); undefined -> @@ -70,9 +74,9 @@ stop_plugins(Names) -> %% @doc List all available plugins -spec(list() -> [mqtt_plugin()]). list() -> - case env(plugins_etc) of + case emqttd:conf(plugins_etc_dir) of {ok, PluginsEtc} -> - CfgFiles = filelib:wildcard("*.config", PluginsEtc), + CfgFiles = filelib:wildcard("*.conf", PluginsEtc), Plugins = [plugin(PluginsEtc, CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), lists:map(fun(Plugin = #mqtt_plugin{name = Name}) -> @@ -223,14 +227,15 @@ plugin_unloaded(Name, true) -> end. read_loaded() -> - {ok, File} = env(loaded_file), - read_loaded(File). + case emqttd:conf(plugins_loaded_file) of + {ok, File} -> read_loaded(File); + undefined -> {error, not_found} + end. -read_loaded(File) -> - file:consult(File). +read_loaded(File) -> file:consult(File). write_loaded(AppNames) -> - {ok, File} = env(loaded_file), + {ok, File} = emqttd:conf(plugins_loaded_file), case file:open(File, [binary, write]) of {ok, Fd} -> lists:foreach(fun(Name) -> @@ -241,16 +246,3 @@ write_loaded(AppNames) -> {error, Error} end. -env(Name) -> - case application:get_env(emqttd, plugins) of - {ok, PluginsEnv} -> - case proplists:get_value(Name, PluginsEnv) of - undefined -> - undefined; - Val -> - {ok, Val} - end; - undefined -> - undefined - end. - From 819b37ab70ff43b6562cc7dcabc4e0f0001e2904 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 17:52:40 +0800 Subject: [PATCH 025/116] 2.0 - add release applications --- relx.config | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/relx.config b/relx.config index 817c82fa7..926f59c63 100644 --- a/relx.config +++ b/relx.config @@ -2,7 +2,23 @@ {release, {emqttd, "2.0"}, [ sasl, os_mon, + asn1, + syntax_tools, + ssl, + crypto, + xmerl, + os_mon, + inets, + goldrush, + compiler, runtime_tools, + {observer, load}, + {wx, load}, + lager, + gen_logger, + gproc, + esockd, + mochiweb, {mnesia, load}, emqttd ]}. From 55097d9aa9759418c612a6c6e6fc2751e968f27e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 18:05:46 +0800 Subject: [PATCH 026/116] 2.0 - move to emqttd-relx --- bin/emqttd | 473 ------------------------------------ bin/emqttd.cmd | 108 -------- bin/emqttd_ctl | 83 ------- bin/install_upgrade_escript | 143 ----------- bin/nodetool | 238 ------------------ bin/start_erl.cmd | 50 ---- 6 files changed, 1095 deletions(-) delete mode 100755 bin/emqttd delete mode 100644 bin/emqttd.cmd delete mode 100755 bin/emqttd_ctl delete mode 100755 bin/install_upgrade_escript delete mode 100755 bin/nodetool delete mode 100644 bin/start_erl.cmd diff --git a/bin/emqttd b/bin/emqttd deleted file mode 100755 index e04524fd4..000000000 --- a/bin/emqttd +++ /dev/null @@ -1,473 +0,0 @@ -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -set -e - -SCRIPT=$(readlink $0 || true) -if [ -z $SCRIPT ]; then - SCRIPT=$0 -fi; -SCRIPT_DIR="$(cd `dirname "$SCRIPT"` && pwd -P)" -RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" -REL_NAME="emqttd" -REL_VSN="{{ rel_vsn }}" -ERTS_VSN="{{ erts_vsn }}" -CODE_LOADING_MODE="${CODE_LOADING_MODE:-embedded}" -REL_DIR="$RELEASE_ROOT_DIR/releases/$REL_VSN" -ERL_OPTS="{{ erl_opts }}" -RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-$RELEASE_ROOT_DIR/log}" - -# Warn the user if ulimit -n is less than 1024 -ULIMIT_F=`ulimit -n` -if [ "$ULIMIT_F" -lt 1024 ]; then - echo "!!!!" - echo "!!!! WARNING: ulimit -n is ${ULIMIT_F}; 1024 is the recommended minimum." - echo "!!!!" -fi - -find_erts_dir() { - __erts_dir="$RELEASE_ROOT_DIR/erts-$ERTS_VSN" - if [ -d "$__erts_dir" ]; then - ERTS_DIR="$__erts_dir"; - ROOTDIR="$RELEASE_ROOT_DIR" - else - __erl="$(which erl)" - code="io:format(\"~s\", [code:root_dir()]), halt()." - __erl_root="$("$__erl" -noshell -eval "$code")" - ERTS_DIR="$__erl_root/erts-$ERTS_VSN" - ROOTDIR="$__erl_root" - fi -} - -# Get node pid -relx_get_pid() { - if output="$(relx_nodetool rpcterms os getpid)" - then - echo "$output" | sed -e 's/"//g' - return 0 - else - echo "$output" - return 1 - fi -} - -relx_get_nodename() { - id="longname$(relx_gen_id)-${NAME}" - "$BINDIR/erl" -boot start_clean -eval '[Host] = tl(string:tokens(atom_to_list(node()),"@")), io:format("~s~n", [Host]), halt()' -noshell ${NAME_TYPE} $id -} - -# Connect to a remote node -relx_rem_sh() { - # Generate a unique id used to allow multiple remsh to the same node - # transparently - id="remsh$(relx_gen_id)-${NAME}" - - # Get the node's ticktime so that we use the same thing. - TICKTIME="$(relx_nodetool rpcterms net_kernel get_net_ticktime)" - - # Setup remote shell command to control node - exec "$BINDIR/erl" "$NAME_TYPE" "$id" -remsh "$NAME" -boot start_clean \ - -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ - -setcookie "$COOKIE" -hidden -kernel net_ticktime $TICKTIME -} - -# Generate a random id -relx_gen_id() { - od -t x -N 4 /dev/urandom | head -n1 | awk '{print $2}' -} - -# Control a node -relx_nodetool() { - command="$1"; shift - - "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" "$NAME_TYPE" "$NAME" \ - -setcookie "$COOKIE" "$command" $@ -} - -# Run an escript in the node's environment -relx_escript() { - shift; scriptpath="$1"; shift - export RELEASE_ROOT_DIR - - "$ERTS_DIR/bin/escript" "$ROOTDIR/$scriptpath" $@ -} - -# Output a start command for the last argument of run_erl -relx_start_command() { - printf "exec \"%s\" \"%s\"" "$RELEASE_ROOT_DIR/bin/$REL_NAME" \ - "$START_OPTION" -} - -# Use $CWD/vm.args if exists, otherwise releases/VSN/vm.args -if [ -z "$VMARGS_PATH" ]; then - if [ -f "$RELEASE_ROOT_DIR/vm.args" ]; then - VMARGS_PATH="$RELEASE_ROOT_DIR/vm.args" - else - VMARGS_PATH="$REL_DIR/vm.args" - fi -fi - -orig_vmargs_path="$VMARGS_PATH.orig" -if [ $RELX_REPLACE_OS_VARS ]; then - #Make sure we don't break dev mode by keeping the symbolic link to - #the user's vm.args - if [ ! -L "$orig_vmargs_path" ]; then - #we're in copy mode, rename the vm.args file to vm.args.orig - mv "$VMARGS_PATH" "$orig_vmargs_path" - fi - - awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < "$orig_vmargs_path" > "$VMARGS_PATH" - else - #We don't need to replace env. vars, just rename the - #symlink vm.args.orig to vm.args, and keep it as a - #symlink. - if [ -L "$orig_vmargs_path" ]; then - mv "$orig_vmargs_path" "$VMARGS_PATH" - fi -fi - -# Make sure log directory exists -mkdir -p "$RUNNER_LOG_DIR" - -# Use $CWD/sys.config if exists, otherwise releases/VSN/sys.config -if [ -z "$RELX_CONFIG_PATH" ]; then - if [ -f "$RELEASE_ROOT_DIR/sys.config" ]; then - RELX_CONFIG_PATH="$RELEASE_ROOT_DIR/sys.config" - else - RELX_CONFIG_PATH="$REL_DIR/sys.config" - fi -fi - -orig_relx_config_path="$RELX_CONFIG_PATH.orig" -if [ $RELX_REPLACE_OS_VARS ]; then - #Make sure we don't break dev mode by keeping the symbolic link to - #the user's sys.config - if [ ! -L "$orig_relx_config_path" ]; then - #We're in copy mode, rename sys.config to sys.config.orig - mv "$RELX_CONFIG_PATH" "$orig_relx_config_path" - fi - - awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < "$orig_relx_config_path" > "$RELX_CONFIG_PATH" - else - #We don't need to replace env. vars, just rename the - #symlink sys.config.orig to sys.config. Keep it as - #a symlink. - if [ -L "$orig_relx_config_path" ]; then - mv "$orig_relx_config_path" "$RELX_CONFIG_PATH" - fi -fi - -# Extract the target node name from node.args -NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH" || true) -if [ -z "$NAME_ARG" ]; then - echo "vm.args needs to have either -name or -sname parameter." - exit 1 -fi - -# Extract the name type and name from the NAME_ARG for REMSH -NAME_TYPE="$(echo "$NAME_ARG" | awk '{print $1}')" -NAME="$(echo "$NAME_ARG" | awk '{print $2}')" - -PIPE_DIR="${PIPE_DIR:-/tmp/erl_pipes/$NAME/}" - -# Extract the target cookie -COOKIE_ARG="$(grep '^-setcookie' "$VMARGS_PATH" || true)" -if [ -z "$COOKIE_ARG" ]; then - echo "vm.args needs to have a -setcookie parameter." - exit 1 -fi - -# Extract cookie name from COOKIE_ARG -COOKIE="$(echo "$COOKIE_ARG" | awk '{print $2}')" - -find_erts_dir -export ROOTDIR="$RELEASE_ROOT_DIR" -export BINDIR="$ERTS_DIR/bin" -export EMU="beam" -export PROGNAME="erl" -export LD_LIBRARY_PATH="$ERTS_DIR/lib:$LD_LIBRARY_PATH" -ERTS_LIB_DIR="$ERTS_DIR/../lib" -MNESIA_DATA_DIR="$ROOTDIR/data/mnesia/$NAME" - -cd "$ROOTDIR" - -# User can specify an sname without @hostname -# This will fail when creating remote shell -# So here we check for @ and add @hostname if missing -case $NAME in - *@*) - # Nothing to do - ;; - *) - NAME=$NAME@$(relx_get_nodename) - ;; -esac - -# Check the first argument for instructions -case "$1" in - start|start_boot) - - # Make sure there is not already a node running - #RES=`$NODETOOL ping` - #if [ "$RES" = "pong" ]; then - # echo "Node is already running!" - # exit 1 - #fi - # Save this for later. - CMD=$1 - case "$1" in - start) - shift - START_OPTION="console" - HEART_OPTION="start" - ;; - start_boot) - shift - START_OPTION="console_boot" - HEART_OPTION="start_boot" - ;; - esac - RUN_PARAM="$@" - - # Set arguments for the heart command - set -- "$SCRIPT_DIR/$REL_NAME" "$HEART_OPTION" - [ "$RUN_PARAM" ] && set -- "$@" "$RUN_PARAM" - - # Export the HEART_COMMAND - HEART_COMMAND="$RELEASE_ROOT_DIR/bin/$REL_NAME $CMD" - export HEART_COMMAND - - mkdir -p "$PIPE_DIR" - - "$BINDIR/run_erl" -daemon "$PIPE_DIR" "$RUNNER_LOG_DIR" \ - "$(relx_start_command)" - ;; - - stop) - # Wait for the node to completely stop... - PID="$(relx_get_pid)" - if ! relx_nodetool "stop"; then - exit 1 - fi - while $(kill -s 0 "$PID" 2>/dev/null); - do - sleep 1 - done - ;; - - restart) - ## Restart the VM without exiting the process - if ! relx_nodetool "restart"; then - exit 1 - fi - ;; - - reboot) - ## Restart the VM completely (uses heart to restart it) - if ! relx_nodetool "reboot"; then - exit 1 - fi - ;; - - pid) - ## Get the VM's pid - if ! relx_get_pid; then - exit 1 - fi - ;; - - ping) - ## See if the VM is alive - if ! relx_nodetool "ping"; then - exit 1 - fi - ;; - - escript) - ## Run an escript under the node's environment - if ! relx_escript $@; then - exit 1 - fi - ;; - - attach) - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - shift - exec "$BINDIR/to_erl" "$PIPE_DIR" - ;; - - remote_console) - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - shift - relx_rem_sh - ;; - - upgrade|downgrade|install) - if [ -z "$2" ]; then - echo "Missing package argument" - echo "Usage: $REL_NAME $1 {package base name}" - echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" - exit 1 - fi - - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ - "install" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" - ;; - - unpack) - if [ -z "$2" ]; then - echo "Missing package argument" - echo "Usage: $REL_NAME $1 {package base name}" - echo "NOTE {package base name} MUST NOT include the .tar.gz suffix" - exit 1 - fi - - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - exec "$BINDIR/escript" "$ROOTDIR/bin/install_upgrade.escript" \ - "unpack" "$REL_NAME" "$NAME_TYPE" "$NAME" "$COOKIE" "$2" - ;; - - console|console_clean|console_boot) - # .boot file typically just $REL_NAME (ie, the app name) - # however, for debugging, sometimes start_clean.boot is useful. - # For e.g. 'setup', one may even want to name another boot script. - case "$1" in - console) - if [ -f "$REL_DIR/$REL_NAME.boot" ]; then - BOOTFILE="$REL_DIR/$REL_NAME" - else - BOOTFILE="$REL_DIR/start" - fi - ;; - console_clean) - BOOTFILE="$ROOTDIR/bin/start_clean" - ;; - console_boot) - shift - BOOTFILE="$1" - shift - ;; - esac - # Setup beam-required vars - EMU="beam" - PROGNAME="${0#*/}" - - export EMU - export PROGNAME - - # Store passed arguments since they will be erased by `set` - ARGS="$@" - - # Build an array of arguments to pass to exec later on - # Build it here because this command will be used for logging. - set -- "$BINDIR/erlexec" -boot "$BOOTFILE" -mode "$CODE_LOADING_MODE" \ - -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ - -config "$RELX_CONFIG_PATH" \ - -mnesia dir "\"${MNESIA_DATA_DIR}\"" \ - -args_file "$VMARGS_PATH" - - # Dump environment info for logging purposes - echo "Exec: $@" -- ${1+$ARGS} - echo "Root: $ROOTDIR" - - # Log the startup - echo "$RELEASE_ROOT_DIR" - logger -t "$REL_NAME[$$]" "Starting up" - - # Start the VM - exec "$@" -- ${1+$ARGS} - ;; - - foreground) - # start up the release in the foreground for use by runit - # or other supervision services - - [ -f "$REL_DIR/$REL_NAME.boot" ] && BOOTFILE="$REL_NAME" || BOOTFILE=start - FOREGROUNDOPTIONS="-noshell -noinput +Bd" - - # Setup beam-required vars - EMU=beam - PROGNAME="${0#*/}" - - export EMU - export PROGNAME - - # Store passed arguments since they will be erased by `set` - ARGS="$@" - - # Build an array of arguments to pass to exec later on - # Build it here because this command will be used for logging. - set -- "$BINDIR/erlexec" $FOREGROUNDOPTIONS \ - -boot "$REL_DIR/$BOOTFILE" -mode "$CODE_LOADING_MODE" -config "$RELX_CONFIG_PATH" \ - -boot_var ERTS_LIB_DIR "$ERTS_LIB_DIR" \ - -mnesia dir "\"${MNESIA_DATA_DIR}\"" \ - -args_file "$VMARGS_PATH" - - # Dump environment info for logging purposes - echo "Exec: $@" -- ${1+$ARGS} - echo "Root: $ROOTDIR" - - # Start the VM - exec "$@" -- ${1+$ARGS} - ;; - rpc) - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - shift - - relx_nodetool rpc $@ - ;; - rpcterms) - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - shift - - relx_nodetool rpcterms $@ - ;; - eval) - # Make sure a node IS running - if ! relx_nodetool "ping" > /dev/null; then - echo "Node is not running!" - exit 1 - fi - - shift - relx_nodetool "eval" $@ - ;; - *) - echo "Usage: $REL_NAME {start|start_boot |foreground|stop|restart|reboot|pid|ping|console|console_clean|console_boot |attach|remote_console|upgrade|escript|rpc|rpcterms|eval}" - exit 1 - ;; -esac - -exit 0 diff --git a/bin/emqttd.cmd b/bin/emqttd.cmd deleted file mode 100644 index effa49536..000000000 --- a/bin/emqttd.cmd +++ /dev/null @@ -1,108 +0,0 @@ -@echo off -@setlocal -@setlocal enabledelayedexpansion - -@set node_name=emqttd - -@rem Get the absolute path to the parent directory, -@rem which is assumed to be the node root. -@for /F "delims=" %%I in ("%~dp0..") do @set node_root=%%~fI - -@set releases_dir=%node_root%\releases -@set runner_etc_dir=%node_root%\etc - -@rem Parse ERTS version and release version from start_erl.data -@for /F "usebackq tokens=1,2" %%I in ("%releases_dir%\start_erl.data") do @( - @call :set_trim erts_version %%I - @call :set_trim release_version %%J -) - -@set vm_args=%runner_etc_dir%\vm.args -@set sys_config=%runner_etc_dir%\emqttd.config -@set node_boot_script=%releases_dir%\%release_version%\%node_name% -@set clean_boot_script=%releases_dir%\%release_version%\start_clean - -@rem extract erlang cookie from vm.args -@for /f "usebackq tokens=1-2" %%I in (`findstr /b \-setcookie "%vm_args%"`) do @set erlang_cookie=%%J - -@set erts_bin=%node_root%\erts-%erts_version%\bin - -@set service_name=%node_name%_%release_version% - -@set erlsrv="%erts_bin%\erlsrv.exe" -@set epmd="%erts_bin%\epmd.exe" -@set escript="%erts_bin%\escript.exe" -@set werl="%erts_bin%\werl.exe" - -@if "%1"=="usage" @goto usage -@if "%1"=="install" @goto install -@if "%1"=="uninstall" @goto uninstall -@if "%1"=="start" @goto start -@if "%1"=="stop" @goto stop -@if "%1"=="restart" @call :stop && @goto start -@if "%1"=="console" @goto console -@if "%1"=="query" @goto query -@if "%1"=="attach" @goto attach -@if "%1"=="upgrade" @goto upgrade -@echo Unknown command: "%1" - -:usage -@echo Usage: %~n0 [install^|uninstall^|start^|stop^|restart^|console^|query^|attach^|upgrade] -@goto :EOF - -:install -@set description=Erlang node %node_name% in %node_root% -@set start_erl=%node_root%\bin\start_erl.cmd -@set args= ++ %node_name% ++ %node_root% -@%erlsrv% add %service_name% -c "%description%" -sname %node_name% -w "%node_root%" -m "%start_erl%" -args "%args%" -stopaction "init:stop()." -@goto :EOF - -:uninstall -@%erlsrv% remove %service_name% -@%epmd% -kill -@goto :EOF - -:start -@%erlsrv% start %service_name% -@goto :EOF - -:stop -@%erlsrv% stop %service_name% -@goto :EOF - -:console -set dest_path=%~dp0 -cd /d !dest_path!..\plugins -set current_path=%cd% -set plugins= -for /d %%P in (*) do ( -set "plugins=!plugins!"!current_path!\%%P\ebin" " -) -cd /d %node_root% - -@start "%node_name% console" %werl% -boot "%node_boot_script%" -config "%sys_config%" -args_file "%vm_args%" -sname %node_name% -pa %plugins% -@goto :EOF - -:query -@%erlsrv% list %service_name% -@exit %ERRORLEVEL% -@goto :EOF - -:attach -@for /f "usebackq" %%I in (`hostname`) do @set hostname=%%I -start "%node_name% attach" %werl% -boot "%clean_boot_script%" -remsh %node_name%@%hostname% -sname console -setcookie %erlang_cookie% -@goto :EOF - -:upgrade -@if "%2"=="" ( - @echo Missing upgrade package argument - @echo Usage: %~n0 upgrade {package base name} - @echo NOTE {package base name} MUST NOT include the .tar.gz suffix - @goto :EOF -) -@%escript% %node_root%\bin\install_upgrade.escript %node_name% %erlang_cookie% %2 -@goto :EOF - -:set_trim -@set %1=%2 -@goto :EOF diff --git a/bin/emqttd_ctl b/bin/emqttd_ctl deleted file mode 100755 index a2d1a28bc..000000000 --- a/bin/emqttd_ctl +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -set -e - -SCRIPT=$(readlink $0 || true) -if [ -z $SCRIPT ]; then - SCRIPT=$0 -fi; -SCRIPT_DIR="$(cd `dirname "$SCRIPT"` && pwd -P)" -RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" -REL_NAME="emqttd" -REL_VSN="{{ rel_vsn }}" -ERTS_VSN="{{ erts_vsn }}" -REL_DIR="$RELEASE_ROOT_DIR/releases/$REL_VSN" -ERL_OPTS="{{ erl_opts }}" -RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-$RELEASE_ROOT_DIR/log}" - -find_erts_dir() { - __erts_dir="$RELEASE_ROOT_DIR/erts-$ERTS_VSN" - if [ -d "$__erts_dir" ]; then - ERTS_DIR="$__erts_dir"; - ROOTDIR="$RELEASE_ROOT_DIR" - else - __erl="$(which erl)" - code="io:format(\"~s\", [code:root_dir()]), halt()." - __erl_root="$("$__erl" -noshell -eval "$code")" - ERTS_DIR="$__erl_root/erts-$ERTS_VSN" - ROOTDIR="$__erl_root" - fi -} - -relx_get_nodename() { - id="longname$(relx_gen_id)-${NAME}" - "$BINDIR/erl" -boot start_clean -eval '[Host] = tl(string:tokens(atom_to_list(node()),"@")), io:format("~s~n", [Host]), halt()' -noshell ${NAME_TYPE} $id -} - -# Control a node -relx_nodetool() { - command="$1"; shift - - "$ERTS_DIR/bin/escript" "$ROOTDIR/bin/nodetool" "$NAME_TYPE" "$NAME" \ - -setcookie "$COOKIE" "$command" $@ -} - -# Use $CWD/vm.args if exists, otherwise releases/VSN/vm.args -if [ -z "$VMARGS_PATH" ]; then - if [ -f "$RELEASE_ROOT_DIR/vm.args" ]; then - VMARGS_PATH="$RELEASE_ROOT_DIR/vm.args" - else - VMARGS_PATH="$REL_DIR/vm.args" - fi -fi - -# Extract the target node name from node.args -NAME_ARG=$(egrep '^-s?name' "$VMARGS_PATH" || true) -if [ -z "$NAME_ARG" ]; then - echo "vm.args needs to have either -name or -sname parameter." - exit 1 -fi - -# Extract the name type and name from the NAME_ARG for REMSH -NAME_TYPE="$(echo "$NAME_ARG" | awk '{print $1}')" -NAME="$(echo "$NAME_ARG" | awk '{print $2}')" - -# Extract the target cookie -COOKIE_ARG="$(grep '^-setcookie' "$VMARGS_PATH" || true)" -if [ -z "$COOKIE_ARG" ]; then - echo "vm.args needs to have a -setcookie parameter." - exit 1 -fi - -# Extract cookie name from COOKIE_ARG -COOKIE="$(echo "$COOKIE_ARG" | awk '{print $2}')" - -find_erts_dir -export ROOTDIR="$RELEASE_ROOT_DIR" -export BINDIR="$ERTS_DIR/bin" -cd "$ROOTDIR" - -relx_nodetool rpc emqttd_ctl run $@ - diff --git a/bin/install_upgrade_escript b/bin/install_upgrade_escript deleted file mode 100755 index 4abce858d..000000000 --- a/bin/install_upgrade_escript +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env escript -%%! -noshell -noinput -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et - --define(TIMEOUT, 300000). --define(INFO(Fmt,Args), io:format(Fmt,Args)). - -%% Unpack or upgrade to a new tar.gz release -main(["unpack", RelName, NameTypeArg, NodeName, Cookie, VersionArg]) -> - TargetNode = start_distribution(NodeName, NameTypeArg, Cookie), - WhichReleases = which_releases(TargetNode), - Version = parse_version(VersionArg), - case proplists:get_value(Version, WhichReleases) of - undefined -> - %% not installed, so unpack tarball: - ?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]), - ReleasePackage = Version ++ "/" ++ RelName, - case rpc:call(TargetNode, release_handler, unpack_release, - [ReleasePackage], ?TIMEOUT) of - {ok, Vsn} -> - ?INFO("Unpacked successfully: ~p~n", [Vsn]); - {error, UnpackReason} -> - print_existing_versions(TargetNode), - ?INFO("Unpack failed: ~p~n",[UnpackReason]), - erlang:halt(2) - end; - old -> - %% no need to unpack, has been installed previously - ?INFO("Release ~s is marked old, switching to it.~n",[Version]); - unpacked -> - ?INFO("Release ~s is already unpacked, now installing.~n",[Version]); - current -> - ?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]); - permanent -> - ?INFO("Release ~s is already installed, and set permanent.~n",[Version]) - end; -main(["install", RelName, NameTypeArg, NodeName, Cookie, VersionArg]) -> - TargetNode = start_distribution(NodeName, NameTypeArg, Cookie), - WhichReleases = which_releases(TargetNode), - Version = parse_version(VersionArg), - case proplists:get_value(Version, WhichReleases) of - undefined -> - %% not installed, so unpack tarball: - ?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]), - ReleasePackage = Version ++ "/" ++ RelName, - case rpc:call(TargetNode, release_handler, unpack_release, - [ReleasePackage], ?TIMEOUT) of - {ok, Vsn} -> - ?INFO("Unpacked successfully: ~p~n", [Vsn]), - install_and_permafy(TargetNode, RelName, Vsn); - {error, UnpackReason} -> - print_existing_versions(TargetNode), - ?INFO("Unpack failed: ~p~n",[UnpackReason]), - erlang:halt(2) - end; - old -> - %% no need to unpack, has been installed previously - ?INFO("Release ~s is marked old, switching to it.~n",[Version]), - install_and_permafy(TargetNode, RelName, Version); - unpacked -> - ?INFO("Release ~s is already unpacked, now installing.~n",[Version]), - install_and_permafy(TargetNode, RelName, Version); - current -> %% installed and in-use, just needs to be permanent - ?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]), - permafy(TargetNode, RelName, Version); - permanent -> - ?INFO("Release ~s is already installed, and set permanent.~n",[Version]) - end; -main(_) -> - erlang:halt(1). - -parse_version(V) when is_list(V) -> - hd(string:tokens(V,"/")). - -install_and_permafy(TargetNode, RelName, Vsn) -> - case rpc:call(TargetNode, release_handler, check_install_release, [Vsn], ?TIMEOUT) of - {ok, _OtherVsn, _Desc} -> - ok; - {error, Reason} -> - ?INFO("ERROR: release_handler:check_install_release failed: ~p~n",[Reason]), - erlang:halt(3) - end, - case rpc:call(TargetNode, release_handler, install_release, [Vsn], ?TIMEOUT) of - {ok, _, _} -> - ?INFO("Installed Release: ~s~n", [Vsn]), - permafy(TargetNode, RelName, Vsn), - ok; - {error, {no_such_release, Vsn}} -> - VerList = - iolist_to_binary( - [io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]), - ?INFO("Installed versions:~n~s", [VerList]), - ?INFO("ERROR: Unable to revert to '~s' - not installed.~n", [Vsn]), - erlang:halt(2) - end. - -permafy(TargetNode, RelName, Vsn) -> - ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), - file:copy(filename:join(["bin", RelName++"-"++Vsn]), - filename:join(["bin", RelName])), - ?INFO("Made release permanent: ~p~n", [Vsn]), - ok. - -which_releases(TargetNode) -> - R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT), - [ {V, S} || {_,V,_, S} <- R ]. - -print_existing_versions(TargetNode) -> - VerList = iolist_to_binary([ - io_lib:format("* ~s\t~s~n",[V,S]) - || {V,S} <- which_releases(TargetNode) ]), - ?INFO("Installed versions:~n~s", [VerList]). - -start_distribution(NodeName, NameTypeArg, Cookie) -> - MyNode = make_script_node(NodeName), - {ok, _Pid} = net_kernel:start([MyNode, get_name_type(NameTypeArg)]), - erlang:set_cookie(node(), list_to_atom(Cookie)), - TargetNode = list_to_atom(NodeName), - case {net_kernel:connect_node(TargetNode), - net_adm:ping(TargetNode)} of - {true, pong} -> - ok; - {_, pang} -> - io:format("Node ~p not responding to pings.\n", [TargetNode]), - erlang:halt(1) - end, - {ok, Cwd} = file:get_cwd(), - ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT), - TargetNode. - -make_script_node(Node) -> - [Name, Host] = string:tokens(Node, "@"), - list_to_atom(lists:concat([Name, "_upgrader_", os:getpid(), "@", Host])). - -%% get name type from arg -get_name_type(NameTypeArg) -> - case NameTypeArg of - "-sname" -> - shortnames; - _ -> - longnames - end. diff --git a/bin/nodetool b/bin/nodetool deleted file mode 100755 index d76c228b1..000000000 --- a/bin/nodetool +++ /dev/null @@ -1,238 +0,0 @@ -#!/usr/bin/env escript -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -%% ------------------------------------------------------------------- -%% -%% nodetool: Helper Script for interacting with live nodes -%% -%% ------------------------------------------------------------------- --mode(compile). - -main(Args) -> - ok = start_epmd(), - %% Extract the args - {RestArgs, TargetNode} = process_args(Args, [], undefined), - - %% process_args() has side-effects (e.g. when processing "-name"), - %% so take care of app-starting business first. - [application:start(App) || App <- [crypto, public_key, ssl]], - - %% any commands that don't need a running node - case RestArgs of - ["chkconfig", File] -> - case file:consult(File) of - {ok, Terms} -> - case validate(Terms) of - ok -> - io:format("ok\n"), - halt(0); - {error, Problems} -> - lists:foreach(fun print_issue/1, Problems), - %% halt(1) if any problems were errors - halt(case [x || {error, _} <- Problems] of - [] -> 0; - _ -> 1 - end) - end; - {error, {Line, Mod, Term}} -> - io:format(standard_error, ["Error on line ", file:format_error({Line, Mod, Term}), "\n"], []), - halt(1); - {error, Error} -> - io:format(standard_error, ["Error reading config file: ", file:format_error(Error), "\n"], []), - halt(1) - end; - _ -> - ok - end, - - %% See if the node is currently running -- if it's not, we'll bail - case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of - {true, pong} -> - ok; - {false,pong} -> - io:format("Failed to connect to node ~p .\n", [TargetNode]), - halt(1); - {_, pang} -> - io:format("Node ~p not responding to pings.\n", [TargetNode]), - halt(1) - end, - - case RestArgs of - ["getpid"] -> - io:format("~p\n", [list_to_integer(rpc:call(TargetNode, os, getpid, []))]); - ["ping"] -> - %% If we got this far, the node already responsed to a ping, so just dump - %% a "pong" - io:format("pong\n"); - ["stop"] -> - io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]); - ["restart"] -> - io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]); - ["reboot"] -> - io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]); - ["rpc", Module, Function | RpcArgs] -> - case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), - [RpcArgs], 60000) of - ok -> - ok; - {badrpc, Reason} -> - io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), - halt(1); - _ -> - halt(1) - end; - ["rpc_infinity", Module, Function | RpcArgs] -> - case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), [RpcArgs], infinity) of - ok -> - ok; - {badrpc, Reason} -> - io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), - halt(1); - _ -> - halt(1) - end; - ["rpcterms", Module, Function, ArgsAsString] -> - case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function), - consult(lists:flatten(ArgsAsString)), 60000) of - {badrpc, Reason} -> - io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), - halt(1); - Other -> - io:format("~p\n", [Other]) - end; - ["eval" | ListOfArgs] -> - % shells may process args into more than one, and end up stripping - % spaces, so this converts all of that to a single string to parse - String = binary_to_list( - list_to_binary( - string:join(ListOfArgs," ") - ) - ), - - % then just as a convenience to users, if they forgot a trailing - % '.' add it for them. - Normalized = - case lists:reverse(String) of - [$. | _] -> String; - R -> lists:reverse([$. | R]) - end, - - % then scan and parse the string - {ok, Scanned, _} = erl_scan:string(Normalized), - {ok, Parsed } = erl_parse:parse_exprs(Scanned), - - % and evaluate it on the remote node - case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of - {value, Value, _} -> - io:format ("~p\n",[Value]); - {badrpc, Reason} -> - io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]), - halt(1) - end; - Other -> - io:format("Other: ~p\n", [Other]), - io:format("Usage: nodetool {chkconfig|getpid|ping|stop|restart|reboot|rpc|rpc_infinity|rpcterms|eval [Terms]} [RPC]\n") - end, - net_kernel:stop(). - -process_args([], Acc, TargetNode) -> - {lists:reverse(Acc), TargetNode}; -process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) -> - erlang:set_cookie(node(), list_to_atom(Cookie)), - process_args(Rest, Acc, TargetNode); -process_args(["-name", TargetName | Rest], Acc, _) -> - ThisNode = append_node_suffix(TargetName, "_maint_"), - {ok, _} = net_kernel:start([ThisNode, longnames]), - process_args(Rest, Acc, nodename(TargetName)); -process_args(["-sname", TargetName | Rest], Acc, _) -> - ThisNode = append_node_suffix(TargetName, "_maint_"), - {ok, _} = net_kernel:start([ThisNode, shortnames]), - process_args(Rest, Acc, nodename(TargetName)); -process_args([Arg | Rest], Acc, Opts) -> - process_args(Rest, [Arg | Acc], Opts). - - -start_epmd() -> - [] = os:cmd("\"" ++ epmd_path() ++ "\" -daemon"), - ok. - -epmd_path() -> - ErtsBinDir = filename:dirname(escript:script_name()), - Name = "epmd", - case os:find_executable(Name, ErtsBinDir) of - false -> - case os:find_executable(Name) of - false -> - io:format("Could not find epmd.~n"), - halt(1); - GlobalEpmd -> - GlobalEpmd - end; - Epmd -> - Epmd - end. - - -nodename(Name) -> - case string:tokens(Name, "@") of - [_Node, _Host] -> - list_to_atom(Name); - [Node] -> - [_, Host] = string:tokens(atom_to_list(node()), "@"), - list_to_atom(lists:concat([Node, "@", Host])) - end. - -append_node_suffix(Name, Suffix) -> - case string:tokens(Name, "@") of - [Node, Host] -> - list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host])); - [Node] -> - list_to_atom(lists:concat([Node, Suffix, os:getpid()])) - end. - -%% -%% Given a string or binary, parse it into a list of terms, ala file:consult/0 -%% -consult(Str) when is_list(Str) -> - consult([], Str, []); -consult(Bin) when is_binary(Bin)-> - consult([], binary_to_list(Bin), []). - -consult(Cont, Str, Acc) -> - case erl_scan:tokens(Cont, Str, 0) of - {done, Result, Remaining} -> - case Result of - {ok, Tokens, _} -> - {ok, Term} = erl_parse:parse_term(Tokens), - consult([], Remaining, [Term | Acc]); - {eof, _Other} -> - lists:reverse(Acc); - {error, Info, _} -> - {error, Info} - end; - {more, Cont1} -> - consult(Cont1, eof, Acc) - end. - -%% -%% Validation functions for checking the emqttd.config -%% -validate([Terms]) -> - Results = [ValidateFun(Terms) || ValidateFun <- get_validation_funs()], - Failures = [Res || Res <- Results, Res /= true], - case Failures of - [] -> - ok; - _ -> - {error, Failures} - end. - -%% Some initial and basic checks for the app.config file -get_validation_funs() -> - [ ]. - -print_issue({warning, Warning}) -> - io:format(standard_error, "Warning in emqttd.config: ~s~n", [Warning]); -print_issue({error, Error}) -> - io:format(standard_error, "Error in emqttd.config: ~s~n", [Error]). - diff --git a/bin/start_erl.cmd b/bin/start_erl.cmd deleted file mode 100644 index 8285c0534..000000000 --- a/bin/start_erl.cmd +++ /dev/null @@ -1,50 +0,0 @@ -@setlocal -@echo off -@setlocal enabledelayedexpansion - -@rem Parse arguments. erlsrv.exe prepends erl arguments prior to first ++. -@rem Other args are position dependent. -@set args="%*" -@for /F "delims=++ tokens=1,2,3" %%I in (%args%) do @( - @set erl_args=%%I - @call :set_trim node_name %%J - @rem Trim spaces from the left of %%K (node_root), which may have spaces inside - @for /f "tokens=* delims= " %%a in ("%%K") do @set node_root=%%a -) - -@set releases_dir=%node_root%\releases - -@rem parse ERTS version and release version from start_erl.dat -@for /F "usebackq tokens=1,2" %%I in ("%releases_dir%\start_erl.data") do @( - @call :set_trim erts_version %%I - @call :set_trim release_version %%J -) - -@set erl_exe="%node_root%\erts-%erts_version%\bin\erl.exe" -@set boot_file="%releases_dir%\%release_version%\%node_name%" - -@if exist "%releases_dir%\%release_version%\sys.config" ( - @set app_config="%releases_dir%\%release_version%\sys.config" -) else ( - @set app_config="%node_root%\etc\emqttd.config" -) - -@if exist "%releases_dir%\%release_version%\vm.args" ( - @set vm_args="%releases_dir%\%release_version%\vm.args" -) else ( - @set vm_args="%node_root%\etc\vm.args" -) - -set dest_path=%~dp0 -cd /d !dest_path!..\plugins -set current_path=%cd% -set plugins= -for /d %%P in (*) do ( -set "plugins=!plugins!"!current_path!\%%P\ebin" " -) -cd /d %node_root% -@%erl_exe% %erl_args% -boot %boot_file% -config %app_config% -args_file %vm_args% -pa %plugins% - -:set_trim -@set %1=%2 -@goto :EOF From c84598dae99fed4e102f2432f34197227679640c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 18:29:04 +0800 Subject: [PATCH 027/116] 2.0 - move rel/ to emqttd-relx project --- rel/sys.config | 38 ------------------------------ rel/vars.config | 19 --------------- rel/vm.args | 61 ------------------------------------------------- 3 files changed, 118 deletions(-) delete mode 100644 rel/sys.config delete mode 100644 rel/vars.config delete mode 100644 rel/vm.args diff --git a/rel/sys.config b/rel/sys.config deleted file mode 100644 index f8f4c82d0..000000000 --- a/rel/sys.config +++ /dev/null @@ -1,38 +0,0 @@ -% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -[{kernel, [ - {start_timer, true}, - {start_pg2, true} - ]}, - {sasl, [ - {sasl_error_logger, {file, "log/emqttd_sasl.log"}} - ]}, - {ssl, [ - %{versions, ['tlsv1.2', 'tlsv1.1']} - ]}, - {esockd, [ - {logger, {lager, error}} - ]}, - {lager, [ - {colored, true}, - {async_threshold, 5000}, - {error_logger_redirect, false}, - {crash_log, "log/emqttd_crash.log"}, - {handlers, [ - {lager_console_backend, error}, - %%NOTICE: Level >= error - %%{lager_emqtt_backend, error}, - {lager_file_backend, [ - {formatter_config, [time, " ", pid, " [",severity,"] ", message, "\n"]}, - {file, "log/emqttd_error.log"}, - {level, error}, - {size, 104857600}, - {date, "$D0"}, - {count, 30} - ]} - ]} - ]}, - {emqttd, [ - {gen_conf, "etc/emqttd.conf"} - ]} -]. diff --git a/rel/vars.config b/rel/vars.config deleted file mode 100644 index 9e248b663..000000000 --- a/rel/vars.config +++ /dev/null @@ -1,19 +0,0 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et - -%% Platform-specific installation paths -{platform_bin_dir, "./bin"}. -{platform_data_dir, "./data"}. -{platform_etc_dir, "./etc"}. -{platform_lib_dir, "./lib"}. -{platform_log_dir, "./log"}. - -%% -%% bin/emqttd -%% -%% {runner_script_dir, "$(cd ${0%/*} && pwd)"}. -{runner_base_dir, "${RUNNER_SCRIPT_DIR%/*}"}. -{runner_etc_dir, "$RUNNER_BASE_DIR/etc"}. -{runner_log_dir, "$RUNNER_BASE_DIR/log"}. -{pipe_dir, "/tmp/$RUNNER_SCRIPT/"}. -{runner_user, ""}. diff --git a/rel/vm.args b/rel/vm.args deleted file mode 100644 index a2ad9302c..000000000 --- a/rel/vm.args +++ /dev/null @@ -1,61 +0,0 @@ -##------------------------------------------------------------------------- -## Name of the emqttd node: Name@Host -## -## NOTICE: The Host should be IP address or the fully qualified host name. -## The short hostname cannot work! -##------------------------------------------------------------------------- - --name emqttd@127.0.0.1 -# or -#-name emqttd@localhost. - -## Cookie for distributed erlang --setcookie emqttdsecretcookie - -##------------------------------------------------------------------------- -## Flags -##------------------------------------------------------------------------- - -## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive -## (Disabled by default..use with caution!) -##-heart --smp true - -## Enable kernel poll and a few async threads -+K true - -## 12 threads/core. -+A 48 - -## max process numbers -+P 8192 - -## Sets the maximum number of simultaneously existing ports for this system -+Q 8192 - -## max atom number -## +t - -## Set the distribution buffer busy limit (dist_buf_busy_limit) in kilobytes. -## Valid range is 1-2097151. Default is 1024. -## +zdbbl 8192 - -## Set scheduler bind type. -## +sbt db - -##------------------------------------------------------------------------- -## Env -##------------------------------------------------------------------------- - -## Increase number of concurrent ports/sockets, deprecated in R17 --env ERL_MAX_PORTS 8192 - --env ERTS_MAX_PORTS 8192 - -## Mnesia and SSL will create temporary ets tables. --env ERL_MAX_ETS_TABLES 1024 - -## Tweak GC to run more often --env ERL_FULLSWEEP_AFTER 1000 - --env ERL_CRASH_DUMP log/emqttd_crash.dump From 83effbcaaa29e2990dc1c6327e83150aab609ba0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 18:29:21 +0800 Subject: [PATCH 028/116] 2.0 - move relx to emqttd-relx project --- relx | Bin 181078 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 relx diff --git a/relx b/relx deleted file mode 100755 index 24466871b4b2fd64e4a4194141b97cd887971e76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181078 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 S5Qq=>5d%h(Z>7LM4Elc|?1o7I From 1bcc2272b55f588fb4435852f92de634e1dc82ed Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 18:29:31 +0800 Subject: [PATCH 029/116] fix compile warning --- src/emqttd_sysmon.erl | 8 ++++---- src/emqttd_vm.erl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emqttd_sysmon.erl b/src/emqttd_sysmon.erl index a262ffc5e..43db85de6 100644 --- a/src/emqttd_sysmon.erl +++ b/src/emqttd_sysmon.erl @@ -162,10 +162,10 @@ publish(Sysmon, WarnMsg) -> topic(Sysmon) -> emqttd_topic:systop(list_to_binary(lists:concat(['sysmon/', Sysmon]))). -start_tracelog(undefined) -> - {ok, undefined}; -start_tracelog(LogFile) -> - lager:trace_file(LogFile, [{sysmon, true}], info, ?LOG_FMT). +%% start_tracelog(undefined) -> +%% {ok, undefined}; +%% start_tracelog(LogFile) -> +%% lager:trace_file(LogFile, [{sysmon, true}], info, ?LOG_FMT). cancel_tracelog(undefined) -> ok; diff --git a/src/emqttd_vm.erl b/src/emqttd_vm.erl index bad4d7c96..bdc6cef40 100644 --- a/src/emqttd_vm.erl +++ b/src/emqttd_vm.erl @@ -152,7 +152,7 @@ schedulers() -> erlang:system_info(schedulers). microsecs() -> - {Mega, Sec, Micro} = erlang:now(), + {Mega, Sec, Micro} = os:timestamp(), (Mega * 1000000 + Sec) * 1000000 + Micro. loads() -> From 31f2d1589d67d1118d9bca49574943ecac43d790 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 18:30:42 +0800 Subject: [PATCH 030/116] 2.0 - move relx.config to emqttd-relx project --- relx.config | 57 ----------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 relx.config diff --git a/relx.config b/relx.config deleted file mode 100644 index 926f59c63..000000000 --- a/relx.config +++ /dev/null @@ -1,57 +0,0 @@ - -{release, {emqttd, "2.0"}, [ - sasl, - os_mon, - asn1, - syntax_tools, - ssl, - crypto, - xmerl, - os_mon, - inets, - goldrush, - compiler, - runtime_tools, - {observer, load}, - {wx, load}, - lager, - gen_logger, - gproc, - esockd, - mochiweb, - {mnesia, load}, - emqttd -]}. - -{include_src, false}. - -{extended_start_script, false}. - -{sys_config, "rel/sys.config"}. - -{vm_args, "rel/vm.args"}. - -{overlay_vars, "./rel/vars.config"}. - -{overlay, [ - {mkdir, "etc/"}, - {mkdir, "etc/ssl/"}, - {mkdir, "data/"}, - {mkdir, "data/mnesia"}, - {mkdir, "log/"}, - {copy, "etc/ssl/ssl.crt", "etc/ssl/ssl.crt"}, - {copy, "etc/ssl/ssl.key", "etc/ssl/ssl.key"}, - - {template, "bin/emqttd", "bin/emqttd"}, - {template, "bin/emqttd_ctl", "bin/emqttd_ctl"}, - {copy, "bin/nodetool", "bin/nodetool"}, - {copy, "bin/nodetool", "erts-\{\{erts_vsn\}\}/bin/nodetool"}, - {copy, "bin/install_upgrade_escript", "bin/install_upgrade_escript"}, - - {template, "etc/acl.conf", "etc/acl.conf"}, - {template, "etc/client.conf", "etc/client.conf"}, - {template, "etc/emqttd.conf", "etc/emqttd.conf"}, - {template, "etc/passwd.conf", "etc/passwd.conf"}, - {template, "etc/rewrite.conf", "etc/rewrite.conf"} -]}. - From 9f9a9ebc302ba27fb0c8a0e3e7cbc2007b90b228 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 21 Jul 2016 18:33:32 +0800 Subject: [PATCH 031/116] emqttd-relx --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c4f4b1eff..fd4ff4457 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,11 @@ unzip emqttd-ubuntu64-0.16.0-beta-20160216.zip && cd emqttd Installing from source: ``` -git clone https://github.com/emqtt/emqttd.git +git clone https://github.com/emqtt/emqttd-relx.git -cd emqttd && make && make dist +cd emqttd-relx && make -cd rel/emqttd && ./bin/emqttd console +cd _rel/emqttd && ./bin/emqttd console ``` ## Documents From c40d59d3cc101ca71be26d370abc022836c0d3d0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 22 Jul 2016 10:09:50 +0800 Subject: [PATCH 032/116] mv etc/ to emqttd-relx project --- ebin/.placeholder | 1 - etc/acl.conf | 28 ----- etc/client.conf | 3 - etc/emqttd.conf | 266 ---------------------------------------------- etc/passwd.conf | 2 - etc/rewrite.conf | 14 --- etc/ssl/ssl.crt | 17 --- etc/ssl/ssl.key | 15 --- 8 files changed, 346 deletions(-) delete mode 100644 ebin/.placeholder delete mode 100644 etc/acl.conf delete mode 100644 etc/client.conf delete mode 100644 etc/emqttd.conf delete mode 100644 etc/passwd.conf delete mode 100644 etc/rewrite.conf delete mode 100644 etc/ssl/ssl.crt delete mode 100644 etc/ssl/ssl.key diff --git a/ebin/.placeholder b/ebin/.placeholder deleted file mode 100644 index 5a885e5ce..000000000 --- a/ebin/.placeholder +++ /dev/null @@ -1 +0,0 @@ -emqttd plugin cannot include "emqttd/include/emqttd.hrl" without this directory:( diff --git a/etc/acl.conf b/etc/acl.conf deleted file mode 100644 index c818c64f0..000000000 --- a/etc/acl.conf +++ /dev/null @@ -1,28 +0,0 @@ -%%-------------------------------------------------------------------- -%% -%% [ACL](https://github.com/emqtt/emqttd/wiki/ACL) -%% -%% -type who() :: all | binary() | -%% {ipaddr, esockd_access:cidr()} | -%% {client, binary()} | -%% {user, binary()}. -%% -%% -type access() :: subscribe | publish | pubsub. -%% -%% -type topic() :: binary(). -%% -%% -type rule() :: {allow, all} | -%% {allow, who(), access(), list(topic())} | -%% {deny, all} | -%% {deny, who(), access(), list(topic())}. -%% -%%-------------------------------------------------------------------- - -{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. - -{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}. - -{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. - -{allow, all}. - diff --git a/etc/client.conf b/etc/client.conf deleted file mode 100644 index 45d62d440..000000000 --- a/etc/client.conf +++ /dev/null @@ -1,3 +0,0 @@ -"testclientid0". -{"testclientid1", "127.0.0.1"}. -{"testclientid2", "192.168.0.1/24"}. diff --git a/etc/emqttd.conf b/etc/emqttd.conf deleted file mode 100644 index 6e195fb3b..000000000 --- a/etc/emqttd.conf +++ /dev/null @@ -1,266 +0,0 @@ -%%=================================================================== -%% -%% Config file for emqttd 2.0 -%% -%% Erlang Term Syntax: -%% -%% {}: Tuple, usually {Key, Value} -%% []: List, seperated by comma -%% %%: Comment -%% -%%=================================================================== - -%%-------------------------------------------------------------------- -%% MQTT Protocol -%%-------------------------------------------------------------------- - -%% Max ClientId Length Allowed. -{mqtt_max_clientid_len, 512}. - -%% Max Packet Size Allowed, 64K by default. -{mqtt_max_packet_size, 65536}. - -%% Client Idle Timeout. -{mqtt_client_idle_timeout, 30}. % Second - -%%-------------------------------------------------------------------- -%% Authentication -%%-------------------------------------------------------------------- - -%% Anonymous: Allow all -{auth, anonymous, []}. - -%% Authentication with username, password -{auth, username, [{passwd, "etc/passwd.conf"}]}. - -%% Authentication with clientId -{auth, clientid, [{config, "etc/client.conf"}, {password, no}]}. - -%%-------------------------------------------------------------------- -%% ACL -%%-------------------------------------------------------------------- - -{acl, anonymous, []}. - -{acl, internal, [{config, "etc/acl.conf"}, {nomatch, allow}]}. - -%% Cache ACL result for PUBLISH -{cache_acl, true}. - -%%-------------------------------------------------------------------- -%% Broker -%%-------------------------------------------------------------------- - -%% System interval of publishing broker $SYS messages -{broker_sys_interval, 60}. - -%%-------------------------------------------------------------------- -%% Retained message -%%-------------------------------------------------------------------- - -%% Expired after seconds, never expired if 0 -{retained_expired_after, 0}. - -%% Max number of retained messages -{retained_max_message_num, 100000}. - -%% Max Payload Size of retained message -{retained_max_playload_size, 65536}. - -%%-------------------------------------------------------------------- -%% Session -%%-------------------------------------------------------------------- - -%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. -%% 0 means no limit -{session_max_inflight, 100}. - -%% Retry interval for redelivering QoS1/2 messages. -{session_unack_retry_interval, 60}. - -%% Awaiting PUBREL Timeout -{session_await_rel_timeout, 20}. - -%% Max Packets that Awaiting PUBREL, 0 means no limit -{session_max_awaiting_rel, 0}. - -%% Statistics Collection Interval(seconds) -{session_collect_interval, 0}. - -%% Expired after 2 day (unit: minute) -{session_expired_after, 2880}. - -%%-------------------------------------------------------------------- -%% Queue -%%-------------------------------------------------------------------- - -%% Type: simple | priority -{queue_type, simple}. - -%% Topic Priority: 0~255, Default is 0 -%% {queue_priority, [{"topic/1", 10}, {"topic/2", 8}]}. - -%% Max queue length. Enqueued messages when persistent client disconnected, -%% or inflight window is full. -{queue_max_length, infinity}. - -%% Low-water mark of queued messages -{queue_low_watermark, 0.2}. - -%% High-water mark of queued messages -{queue_high_watermark, 0.6}. - -%% Queue Qos0 messages? -{queue_qos0, true}. - -%%-------------------------------------------------------------------- -%% Zone -%%-------------------------------------------------------------------- - -{zone, admin, []}. - -%%-------------------------------------------------------------------- -%% Listener -%%-------------------------------------------------------------------- - -%% Plain MQTT -{listener, mqtt, 1883, [ - %% Size of acceptor pool - {acceptors, 16}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Mount point prefix - %% {mount_point, "prefix/"}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% Connection Options - {connopts, [ - %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec - %% {rate_limit, "100,10"} %% 100K burst, 10K rate - ]}, - - %% Socket Options - {sockopts, [ - %Set buffer if hight thoughtput - %{recbuf, 4096}, - %{sndbuf, 4096}, - %{buffer, 4096}, - %{nodelay, true}, - {backlog, 1024} - ]} -]}. - -%% MQTT/SSL -{listener, mqtts, 8883, [ - %% Size of acceptor pool - {acceptors, 4}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% SSL certificate and key files - {ssl, [{certfile, "etc/ssl/ssl.crt"}, - {keyfile, "etc/ssl/ssl.key"}]}, - - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} -]}. - -%% HTTP and WebSocket Listener -{listener, http, 8083, [ - %% Size of acceptor pool - {acceptors, 4}, - - %% Maximum number of concurrent clients - {max_clients, 64}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} -]}. - -%%-------------------------------------------------------------------- -%% PubSub -%%-------------------------------------------------------------------- - -%% PubSub and Router. Default should be scheduler numbers. -{pubsub_pool_size, 8}. - -%%-------------------------------------------------------------------- -%% Routing -%%-------------------------------------------------------------------- - -%% Route aging time(seconds) -{routing_age, 5}. - -%%-------------------------------------------------------------------- -%% Bridge -%%-------------------------------------------------------------------- - -%% TODO: Bridge Queue Size -{bridge_max_queue_len, 10000}. - -%% Ping Interval of bridge node -{bridge_ping_down_interval, 1}. % second - -%%------------------------------------------------------------------- -%% Plugins -%%------------------------------------------------------------------- - -%% Dir of plugins' config -{plugins_etc_dir, "etc/plugins/"}. - -%% File to store loaded plugin names. -{plugins_loaded_file, "data/loaded_plugins"}. - -%%-------------------------------------------------------------------- -%% Modules -%%-------------------------------------------------------------------- - -%% Client presence management module. Publish presence messages when -%% client connected or disconnected. -{module, presence, [{qos, 0}]}. - -%% Subscribe topics automatically when client connected -{module, subscription, [{"$queue/clients/$c", 1}, backend]}. - -%% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) -{module, rewrite, [{config, "etc/rewrite.conf"}]}. - -%%------------------------------------------------------------------- -%% Erlang System Monitor -%%------------------------------------------------------------------- - -%% Long GC, don't monitor in production mode for: -%% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 - -{sysmon_long_gc, false}. - -%% Long Schedule(ms) -{sysmon_long_schedule, 240}. - -%% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. -%% 8 * 1024 * 1024 -{sysmon_large_heap, 8388608}. - -%% Busy Port -{sysmon_busy_port, false}. - -%% Busy Dist Port -{sysmon_busy_dist_port, true}. - diff --git a/etc/passwd.conf b/etc/passwd.conf deleted file mode 100644 index 89ac8ffe2..000000000 --- a/etc/passwd.conf +++ /dev/null @@ -1,2 +0,0 @@ -{"user1", "passwd1"}. -{"user2", "passwd2"}. diff --git a/etc/rewrite.conf b/etc/rewrite.conf deleted file mode 100644 index 7c1a9094f..000000000 --- a/etc/rewrite.conf +++ /dev/null @@ -1,14 +0,0 @@ - -%%-------------------------------------------------------------------- -%% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) -%%-------------------------------------------------------------------- - -%{topic, "x/#", [ -% {rewrite, "^x/y/(.+)$", "z/y/$1"}, -% {rewrite, "^x/(.+)$", "y/$1"} -%]}. - -%{topic, "y/+/z/#", [ -% {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} -%]}. - diff --git a/etc/ssl/ssl.crt b/etc/ssl/ssl.crt deleted file mode 100644 index 001844674..000000000 --- a/etc/ssl/ssl.crt +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICuTCCAiICCQC8+3PPaqATfDANBgkqhkiG9w0BAQUFADCBoDELMAkGA1UEBhMC -Q0gxETAPBgNVBAgTCFpoZUppYW5nMREwDwYDVQQHEwhIYW5nWmhvdTEUMBIGA1UE -ChMLWGlhb0xpIFRlY2gxHzAdBgNVBAsTFkluZm9ybWF0aW9uIFRlY2hub2xvZ3kx -EzARBgNVBAMTCnQuZW1xdHQuaW8xHzAdBgkqhkiG9w0BCQEWEGZlbmcgYXQgZW1x -dHQuaW8wHhcNMTUwMjI1MTc0NjQwWhcNMTYwMjI1MTc0NjQwWjCBoDELMAkGA1UE -BhMCQ0gxETAPBgNVBAgTCFpoZUppYW5nMREwDwYDVQQHEwhIYW5nWmhvdTEUMBIG -A1UEChMLWGlhb0xpIFRlY2gxHzAdBgNVBAsTFkluZm9ybWF0aW9uIFRlY2hub2xv -Z3kxEzARBgNVBAMTCnQuZW1xdHQuaW8xHzAdBgkqhkiG9w0BCQEWEGZlbmcgYXQg -ZW1xdHQuaW8wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALAtN2OHsvltOk+9 -AtlwMtKuaWW2WpV/S0lRRG9x9k8pyd5PJeeYAr2jVsoWnZInb1CoEOHFcwxZLjv3 -gEvz+X+//W02YyI9hnvCJUpT/+6P0gJEbmTmqL078M6vbtwtiF1YC7mdo0nGAZuK -qedpIoEZbVJavf4S0vXWTsb3s5unAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAgUR3 -z4uDUsAl+xUorPMBIOS/ncHHVk1XucVv9Wi4chzzZ+4/Y77/fFqP6oxhQ59C9Q8i -iT5wjaE4R1eCge18lPSw3yb1tsTe5B3WkRTzziPq/Q/AsC+DifkkE1YW67leuJV/ -vz74sEi0dudmOVoe6peYxjEH8xXoIUqhnwXt/4Q= ------END CERTIFICATE----- diff --git a/etc/ssl/ssl.key b/etc/ssl/ssl.key deleted file mode 100644 index 5d5786fac..000000000 --- a/etc/ssl/ssl.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQCwLTdjh7L5bTpPvQLZcDLSrmlltlqVf0tJUURvcfZPKcneTyXn -mAK9o1bKFp2SJ29QqBDhxXMMWS4794BL8/l/v/1tNmMiPYZ7wiVKU//uj9ICRG5k -5qi9O/DOr27cLYhdWAu5naNJxgGbiqnnaSKBGW1SWr3+EtL11k7G97ObpwIDAQAB -AoGBAKU1cbiLG0GdtU3rME3ZUj+RQNMZ4u5IVcBmTie4FcN8q4ombKQ2P3O4RX3z -IUZaZp+bS2F8uHt+8cVYPl57Zp5fwbIlv6jWgGpvXLsX8JBQl2OTw38B+hVwJvAM -h0mBzprUOs3KGZyF5cyA4osrZ4QvCZhwId9fAjwLGBF9i1yBAkEA4jWAF1sWQiwF -vY476m+0ihpRwGKjldKHWFZmvoB/AnNV/rXO+HRl3MB5wmO+Dqg3gJZrjGBgDeaV -g9hoQjK6ZwJBAMdg57iKLd8uUb7c4pR8fDdDbeeI5X7WDf2k9emT3BMPJPQ3EiSf -CStn1hRfp31U9CXEnw94rKHhrdMFrYjdzMECQCcWD3f5qTLt4GAMf5XWj199hLq1 -UIbGxdQhuccY9Nk7jJRiXczYb/Fg4KkSCvkFX/G8DAFJdc9xFEyfzAQEN+kCQH3a -nMrvZn9gBLffRKOIZPyZctHZp0xGIHTA4X39GMlrIN+Lt8coIKimlgssSlSiAK+q -iuFAQnC5PXlcNyuTHsECQAMNMY6jXikgSUZfVXitAFX3g9+IbjT9eJ92f60QneW8 -mxWQoqP3fqCSbTEysb7NojEEwppSZtaNgnBb5R4E+mU= ------END RSA PRIVATE KEY----- From 3f9de8c073e57a243ad3d271a4a2e2fc375e82de Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 22 Jul 2016 11:38:48 +0800 Subject: [PATCH 033/116] fix plugins --- include/emqttd.hrl | 1 - src/emqttd_plugins.erl | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 43f9e36b4..56c595002 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -150,7 +150,6 @@ name, version, descr, - config, active = false }). diff --git a/src/emqttd_plugins.erl b/src/emqttd_plugins.erl index b67810732..78304ba00 100644 --- a/src/emqttd_plugins.erl +++ b/src/emqttd_plugins.erl @@ -77,7 +77,7 @@ list() -> case emqttd:conf(plugins_etc_dir) of {ok, PluginsEtc} -> CfgFiles = filelib:wildcard("*.conf", PluginsEtc), - Plugins = [plugin(PluginsEtc, CfgFile) || CfgFile <- CfgFiles], + Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], StartedApps = names(started_app), lists:map(fun(Plugin = #mqtt_plugin{name = Name}) -> case lists:member(Name, StartedApps) of @@ -89,13 +89,12 @@ list() -> [] end. -plugin(PluginsEtc, CfgFile0) -> - CfgFile = filename:join(PluginsEtc, CfgFile0), - {ok, [[{AppName, AppEnv} | _]]} = file:consult(CfgFile), +plugin(CfgFile) -> + [AppName | _] = sting:tokens(CfgFile, "."), {ok, Attrs} = application:get_all_key(AppName), Ver = proplists:get_value(vsn, Attrs, "0"), Descr = proplists:get_value(description, Attrs, ""), - #mqtt_plugin{name = AppName, version = Ver, config = AppEnv, descr = Descr}. + #mqtt_plugin{name = AppName, version = Ver, descr = Descr}. %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, any()}). @@ -114,15 +113,15 @@ load(PluginName) when is_atom(PluginName) -> end end. -load_plugin(#mqtt_plugin{name = Name, config = Config}, Persistent) -> - case load_app(Name, Config) of +load_plugin(#mqtt_plugin{name = Name}, Persistent) -> + case load_app(Name) of ok -> start_app(Name, fun(App) -> plugin_loaded(App, Persistent) end); {error, Error} -> {error, Error} end. -load_app(App, _Config) -> +load_app(App) -> case application:load(App) of ok -> ok; From 67637408fce5da199e05afbf4f0626414c0ec9b5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 22 Jul 2016 11:41:11 +0800 Subject: [PATCH 034/116] string:tokens/2 --- src/emqttd_plugins.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_plugins.erl b/src/emqttd_plugins.erl index 78304ba00..e10c0c5d6 100644 --- a/src/emqttd_plugins.erl +++ b/src/emqttd_plugins.erl @@ -90,7 +90,7 @@ list() -> end. plugin(CfgFile) -> - [AppName | _] = sting:tokens(CfgFile, "."), + [AppName | _] = string:tokens(CfgFile, "."), {ok, Attrs} = application:get_all_key(AppName), Ver = proplists:get_value(vsn, Attrs, "0"), Descr = proplists:get_value(description, Attrs, ""), From 3c33bdddc792465e012364dcf920a3619d3acecb Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 22 Jul 2016 11:43:40 +0800 Subject: [PATCH 035/116] list_to_atom --- src/emqttd_plugins.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqttd_plugins.erl b/src/emqttd_plugins.erl index e10c0c5d6..360e828f0 100644 --- a/src/emqttd_plugins.erl +++ b/src/emqttd_plugins.erl @@ -91,10 +91,10 @@ list() -> plugin(CfgFile) -> [AppName | _] = string:tokens(CfgFile, "."), - {ok, Attrs} = application:get_all_key(AppName), + {ok, Attrs} = application:get_all_key(list_to_atom(AppName)), Ver = proplists:get_value(vsn, Attrs, "0"), Descr = proplists:get_value(description, Attrs, ""), - #mqtt_plugin{name = AppName, version = Ver, descr = Descr}. + #mqtt_plugin{name = list_to_atom(AppName), version = Ver, descr = Descr}. %% @doc Load a Plugin -spec(load(atom()) -> ok | {error, any()}). From efd13555970409a3f5a439cc7adc581e24500465 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 23 Jul 2016 08:57:36 +0800 Subject: [PATCH 036/116] add test_data --- Makefile | 4 +- test_data/ct_emqttd.conf | 266 +++++++++++++++++++++++++++++++++++++++ test_data/ct_sys.config | 6 + test_data/ct_vm.args | 61 +++++++++ 4 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 test_data/ct_emqttd.conf create mode 100644 test_data/ct_sys.config create mode 100644 test_data/ct_vm.args diff --git a/Makefile b/Makefile index 77cdfdef1..d9a44f484 100644 --- a/Makefile +++ b/Makefile @@ -14,11 +14,11 @@ dep_mochiweb = git https://github.com/emqtt/mochiweb.git ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -EUNIT_ERL_OPTS = -args_file rel/vm.args -config rel/sys.config +EUNIT_ERL_OPTS = -args_file test_data/ct_vm.args -config test_data/ct_sys.config CT_SUITES = emqttd emqttd_access emqttd_backend emqttd_lib emqttd_mod emqttd_net \ emqttd_mqueue emqttd_protocol emqttd_topic emqttd_trie -CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 -config rel/sys.config +CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 -config test_data/ct_sys.config COVER = true diff --git a/test_data/ct_emqttd.conf b/test_data/ct_emqttd.conf new file mode 100644 index 000000000..562bd908f --- /dev/null +++ b/test_data/ct_emqttd.conf @@ -0,0 +1,266 @@ +%%=================================================================== +%% +%% Config file for emqttd 2.0 +%% +%% Erlang Term Syntax: +%% +%% {}: Tuple, usually {Key, Value} +%% []: List, seperated by comma +%% %%: Comment +%% +%%=================================================================== + +%%-------------------------------------------------------------------- +%% MQTT Protocol +%%-------------------------------------------------------------------- + +%% Max ClientId Length Allowed. +{mqtt_max_clientid_len, 512}. + +%% Max Packet Size Allowed, 64K by default. +{mqtt_max_packet_size, 65536}. + +%% Client Idle Timeout. +{mqtt_client_idle_timeout, 30}. % Second + +%%-------------------------------------------------------------------- +%% Authentication +%%-------------------------------------------------------------------- + +%% Anonymous: Allow all +{auth, anonymous, []}. + +%% Authentication with username, password +{auth, username, []}. + +%% Authentication with clientId +{auth, clientid, [{password, no}]}. + +%%-------------------------------------------------------------------- +%% ACL +%%-------------------------------------------------------------------- + +{acl, anonymous, []}. + +{acl, internal, [{nomatch, allow}]}. + +%% Cache ACL result for PUBLISH +{cache_acl, true}. + +%%-------------------------------------------------------------------- +%% Broker +%%-------------------------------------------------------------------- + +%% System interval of publishing broker $SYS messages +{broker_sys_interval, 60}. + +%%-------------------------------------------------------------------- +%% Retained message +%%-------------------------------------------------------------------- + +%% Expired after seconds, never expired if 0 +{retained_expired_after, 0}. + +%% Max number of retained messages +{retained_max_message_num, 100000}. + +%% Max Payload Size of retained message +{retained_max_playload_size, 65536}. + +%%-------------------------------------------------------------------- +%% Session +%%-------------------------------------------------------------------- + +%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. +%% 0 means no limit +{session_max_inflight, 100}. + +%% Retry interval for redelivering QoS1/2 messages. +{session_unack_retry_interval, 60}. + +%% Awaiting PUBREL Timeout +{session_await_rel_timeout, 20}. + +%% Max Packets that Awaiting PUBREL, 0 means no limit +{session_max_awaiting_rel, 0}. + +%% Statistics Collection Interval(seconds) +{session_collect_interval, 0}. + +%% Expired after 2 day (unit: minute) +{session_expired_after, 2880}. + +%%-------------------------------------------------------------------- +%% Queue +%%-------------------------------------------------------------------- + +%% Type: simple | priority +{queue_type, simple}. + +%% Topic Priority: 0~255, Default is 0 +%% {queue_priority, [{"topic/1", 10}, {"topic/2", 8}]}. + +%% Max queue length. Enqueued messages when persistent client disconnected, +%% or inflight window is full. +{queue_max_length, infinity}. + +%% Low-water mark of queued messages +{queue_low_watermark, 0.2}. + +%% High-water mark of queued messages +{queue_high_watermark, 0.6}. + +%% Queue Qos0 messages? +{queue_qos0, true}. + +%%-------------------------------------------------------------------- +%% Zone +%%-------------------------------------------------------------------- + +{zone, admin, []}. + +%%-------------------------------------------------------------------- +%% Listener +%%-------------------------------------------------------------------- + +%% Plain MQTT +{listener, mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 16}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Mount point prefix + %% {mount_point, "prefix/"}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + + %% Socket Options + {sockopts, [ + %Set buffer if hight thoughtput + %{recbuf, 4096}, + %{sndbuf, 4096}, + %{buffer, 4096}, + %{nodelay, true}, + {backlog, 1024} + ]} +]}. + +%% MQTT/SSL +{listener, mqtts, 8883, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% SSL certificate and key files + {ssl, [{certfile, "etc/ssl/ssl.crt"}, + {keyfile, "etc/ssl/ssl.key"}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} +]}. + +%% HTTP and WebSocket Listener +{listener, http, 8083, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 64}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} +]}. + +%%-------------------------------------------------------------------- +%% PubSub +%%-------------------------------------------------------------------- + +%% PubSub and Router. Default should be scheduler numbers. +{pubsub_pool_size, 8}. + +%%-------------------------------------------------------------------- +%% Routing +%%-------------------------------------------------------------------- + +%% Route aging time(seconds) +{routing_age, 5}. + +%%-------------------------------------------------------------------- +%% Bridge +%%-------------------------------------------------------------------- + +%% TODO: Bridge Queue Size +{bridge_max_queue_len, 10000}. + +%% Ping Interval of bridge node +{bridge_ping_down_interval, 1}. % second + +%%------------------------------------------------------------------- +%% Plugins +%%------------------------------------------------------------------- + +%% Dir of plugins' config +{plugins_etc_dir, "etc/plugins/"}. + +%% File to store loaded plugin names. +{plugins_loaded_file, "data/loaded_plugins"}. + +%%-------------------------------------------------------------------- +%% Modules +%%-------------------------------------------------------------------- + +%% Client presence management module. Publish presence messages when +%% client connected or disconnected. +{module, presence, [{qos, 0}]}. + +%% Subscribe topics automatically when client connected +{module, subscription, [{"$queue/clients/$c", 1}, backend]}. + +%% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) +{module, rewrite, []}. + +%%------------------------------------------------------------------- +%% Erlang System Monitor +%%------------------------------------------------------------------- + +%% Long GC, don't monitor in production mode for: +%% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 + +{sysmon_long_gc, false}. + +%% Long Schedule(ms) +{sysmon_long_schedule, 240}. + +%% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. +%% 8 * 1024 * 1024 +{sysmon_large_heap, 8388608}. + +%% Busy Port +{sysmon_busy_port, false}. + +%% Busy Dist Port +{sysmon_busy_dist_port, true}. + diff --git a/test_data/ct_sys.config b/test_data/ct_sys.config new file mode 100644 index 000000000..bce791354 --- /dev/null +++ b/test_data/ct_sys.config @@ -0,0 +1,6 @@ +% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +%% ex: ft=erlang ts=4 sw=4 et +[ + {kernel, [{start_timer, true}, {start_pg2, true}]}, + {emqttd, [{conf, "test_data/emqttd_ct.conf"}]} +]. diff --git a/test_data/ct_vm.args b/test_data/ct_vm.args new file mode 100644 index 000000000..e05e373a4 --- /dev/null +++ b/test_data/ct_vm.args @@ -0,0 +1,61 @@ +##------------------------------------------------------------------------- +## Name of the emqttd node: Name@Host +## +## NOTICE: The Host should be IP address or the fully qualified host name. +## The short hostname cannot work! +##------------------------------------------------------------------------- + +-name emqttd_ct@127.0.0.1 +# or +#-name emqttd@localhost. + +## Cookie for distributed erlang +-setcookie emqttdsecretcookie + +##------------------------------------------------------------------------- +## Flags +##------------------------------------------------------------------------- + +## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive +## (Disabled by default..use with caution!) +##-heart +-smp true + +## Enable kernel poll and a few async threads ++K true + +## 12 threads/core. ++A 24 + +## max process numbers +#+P 8192 + +## Sets the maximum number of simultaneously existing ports for this system +#+Q 8192 + +## max atom number +## +t + +## Set the distribution buffer busy limit (dist_buf_busy_limit) in kilobytes. +## Valid range is 1-2097151. Default is 1024. +## +zdbbl 8192 + +## Set scheduler bind type. +## +sbt db + +##------------------------------------------------------------------------- +## Env +##------------------------------------------------------------------------- + +## Increase number of concurrent ports/sockets, deprecated in R17 +#-env ERL_MAX_PORTS 8192 + +#-env ERTS_MAX_PORTS 8192 + +## Mnesia and SSL will create temporary ets tables. +-env ERL_MAX_ETS_TABLES 1024 + +## Tweak GC to run more often +-env ERL_FULLSWEEP_AFTER 1000 + +-env ERL_CRASH_DUMP log/emqttd_crash.dump From e5efa7707888345393dbd2c1634e1b2d50ff02bf Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 23 Jul 2016 09:31:51 +0800 Subject: [PATCH 037/116] make ct --- Makefile | 5 ++++- test_data/ct_sys.config | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d9a44f484..a8e753a95 100644 --- a/Makefile +++ b/Makefile @@ -13,12 +13,15 @@ dep_mochiweb = git https://github.com/emqtt/mochiweb.git ERLC_OPTS += +'{parse_transform, lager_transform}' +TEST_ERLC_OPTS += +debug_info +TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' + EUNIT_OPTS = verbose EUNIT_ERL_OPTS = -args_file test_data/ct_vm.args -config test_data/ct_sys.config CT_SUITES = emqttd emqttd_access emqttd_backend emqttd_lib emqttd_mod emqttd_net \ emqttd_mqueue emqttd_protocol emqttd_topic emqttd_trie -CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 -config test_data/ct_sys.config +CT_OPTS = -cover test/ct.cover.spec -erl_args -args_file test_data/ct_vm.args -config test_data/ct_sys.config COVER = true diff --git a/test_data/ct_sys.config b/test_data/ct_sys.config index bce791354..67f13b910 100644 --- a/test_data/ct_sys.config +++ b/test_data/ct_sys.config @@ -2,5 +2,5 @@ %% ex: ft=erlang ts=4 sw=4 et [ {kernel, [{start_timer, true}, {start_pg2, true}]}, - {emqttd, [{conf, "test_data/emqttd_ct.conf"}]} + {emqttd, [{conf, "test_data/ct_emqttd.conf"}]} ]. From f998d2a41d7abd4df07fc4df22b4a8a0ebbbd878 Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 23 Jul 2016 11:10:55 +0800 Subject: [PATCH 038/116] fix CT errors --- Makefile | 4 +- src/emqttd_acl_internal.erl | 2 + test/emqttd_SUITE.erl | 4 +- .../emqttd_SUITE_data/emqttd.conf | 0 test/emqttd_access_SUITE.erl | 34 ++++++++--- test/emqttd_access_SUITE_data/test_acl.config | 16 ----- test_data/ct_sys.config | 6 -- test_data/ct_vm.args | 61 ------------------- 8 files changed, 33 insertions(+), 94 deletions(-) rename test_data/ct_emqttd.conf => test/emqttd_SUITE_data/emqttd.conf (100%) delete mode 100644 test/emqttd_access_SUITE_data/test_acl.config delete mode 100644 test_data/ct_sys.config delete mode 100644 test_data/ct_vm.args diff --git a/Makefile b/Makefile index a8e753a95..e471d50ed 100644 --- a/Makefile +++ b/Makefile @@ -17,11 +17,11 @@ TEST_ERLC_OPTS += +debug_info TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose -EUNIT_ERL_OPTS = -args_file test_data/ct_vm.args -config test_data/ct_sys.config +# EUNIT_ERL_OPTS = CT_SUITES = emqttd emqttd_access emqttd_backend emqttd_lib emqttd_mod emqttd_net \ emqttd_mqueue emqttd_protocol emqttd_topic emqttd_trie -CT_OPTS = -cover test/ct.cover.spec -erl_args -args_file test_data/ct_vm.args -config test_data/ct_sys.config +CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 COVER = true diff --git a/src/emqttd_acl_internal.erl b/src/emqttd_acl_internal.erl index ff1efcd16..282fb77a4 100644 --- a/src/emqttd_acl_internal.erl +++ b/src/emqttd_acl_internal.erl @@ -113,6 +113,8 @@ match(Client, Topic, [Rule|Rules]) -> %% @doc Reload ACL -spec(reload_acl(State :: #state{}) -> ok | {error, Reason :: any()}). +reload_acl(#state{config = undefined}) -> + ok; reload_acl(State) -> case catch load_rules_from_file(State) of {'EXIT', Error} -> {error, Error}; diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index 3c507540d..afd5b28d1 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -82,6 +82,8 @@ groups() -> init_per_suite(Config) -> application:start(lager), + DataDir = proplists:get_value(data_dir, Config), + application:set_env(emqttd, conf, filename:join([DataDir, "emqttd.conf"])), application:ensure_all_started(emqttd), Config. @@ -177,7 +179,7 @@ pubsub_queue(_) -> Self = self(), Q = <<"$queue/abc">>, SubFun = fun() -> emqttd:subscribe(Q), - timer:sleep(1), + timer:sleep(10), {ok, Msgs} = loop_recv(Q, 10), Self ! {recv, self(), Msgs} end, diff --git a/test_data/ct_emqttd.conf b/test/emqttd_SUITE_data/emqttd.conf similarity index 100% rename from test_data/ct_emqttd.conf rename to test/emqttd_SUITE_data/emqttd.conf diff --git a/test/emqttd_access_SUITE.erl b/test/emqttd_access_SUITE.erl index 8a8d05766..074fd15df 100644 --- a/test/emqttd_access_SUITE.erl +++ b/test/emqttd_access_SUITE.erl @@ -38,9 +38,34 @@ groups() -> [compile_rule, match_rule]}]. +init_per_group(access_control, Config) -> + application:load(emqttd), + prepare_config(), + gen_conf:init(emqttd), + Config; + init_per_group(_Group, Config) -> Config. +prepare_config() -> + Rules = [{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}, + {allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]}, + {allow, {user, "admin"}, pubsub, ["a/b/c", "d/e/f/#"]}, + {allow, {client, "testClient"}, subscribe, ["testTopics/testClient"]}, + {allow, all, subscribe, ["clients/$c"]}, + {allow, all, pubsub, ["users/$u/#"]}, + {deny, all, subscribe, ["$SYS/#", "#"]}, + {deny, all}], + write_config("access_SUITE_acl.conf", Rules), + Config = [{auth, anonymous, []}, + {acl, internal, [{config, "access_SUITE_acl.conf"}, + {nomatch, allow}]}], + write_config("access_SUITE_emqttd.conf", Config), + application:set_env(emqttd, conf, "access_SUITE_emqttd.conf"). + +write_config(Filename, Terms) -> + file:write_file(Filename, [io_lib:format("~tp.~n", [Term]) || Term <- Terms]). + end_per_group(_Group, Config) -> Config. @@ -48,14 +73,7 @@ init_per_testcase(TestCase, Config) when TestCase =:= reload_acl; TestCase =:= register_mod; TestCase =:= unregister_mod; TestCase =:= check_acl -> - DataDir = proplists:get_value(data_dir, Config), - AclOpts = [ - {auth, [{anonymous, []}]}, - {acl, [{internal, [{file, filename:join([DataDir, "test_acl.config"])}, - {nomatch, allow}]}]} - ], - {ok, _Pid} = ?AC:start_link(AclOpts), - Config; + {ok, _Pid} = ?AC:start_link(), Config; init_per_testcase(_TestCase, Config) -> Config. diff --git a/test/emqttd_access_SUITE_data/test_acl.config b/test/emqttd_access_SUITE_data/test_acl.config deleted file mode 100644 index 4a2c6ea44..000000000 --- a/test/emqttd_access_SUITE_data/test_acl.config +++ /dev/null @@ -1,16 +0,0 @@ -{allow, {ipaddr, "127.0.0.1"}, subscribe, ["$SYS/#", "#"]}. - -{allow, {user, "testuser"}, subscribe, ["a/b/c", "d/e/f/#"]}. - -{allow, {user, "admin"}, pubsub, ["a/b/c", "d/e/f/#"]}. - -{allow, {client, "testClient"}, subscribe, ["testTopics/testClient"]}. - -{allow, all, subscribe, ["clients/$c"]}. - -{allow, all, pubsub, ["users/$u/#"]}. - -{deny, all, subscribe, ["$SYS/#", "#"]}. - -{deny, all}. - diff --git a/test_data/ct_sys.config b/test_data/ct_sys.config deleted file mode 100644 index 67f13b910..000000000 --- a/test_data/ct_sys.config +++ /dev/null @@ -1,6 +0,0 @@ -% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ft=erlang ts=4 sw=4 et -[ - {kernel, [{start_timer, true}, {start_pg2, true}]}, - {emqttd, [{conf, "test_data/ct_emqttd.conf"}]} -]. diff --git a/test_data/ct_vm.args b/test_data/ct_vm.args deleted file mode 100644 index e05e373a4..000000000 --- a/test_data/ct_vm.args +++ /dev/null @@ -1,61 +0,0 @@ -##------------------------------------------------------------------------- -## Name of the emqttd node: Name@Host -## -## NOTICE: The Host should be IP address or the fully qualified host name. -## The short hostname cannot work! -##------------------------------------------------------------------------- - --name emqttd_ct@127.0.0.1 -# or -#-name emqttd@localhost. - -## Cookie for distributed erlang --setcookie emqttdsecretcookie - -##------------------------------------------------------------------------- -## Flags -##------------------------------------------------------------------------- - -## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive -## (Disabled by default..use with caution!) -##-heart --smp true - -## Enable kernel poll and a few async threads -+K true - -## 12 threads/core. -+A 24 - -## max process numbers -#+P 8192 - -## Sets the maximum number of simultaneously existing ports for this system -#+Q 8192 - -## max atom number -## +t - -## Set the distribution buffer busy limit (dist_buf_busy_limit) in kilobytes. -## Valid range is 1-2097151. Default is 1024. -## +zdbbl 8192 - -## Set scheduler bind type. -## +sbt db - -##------------------------------------------------------------------------- -## Env -##------------------------------------------------------------------------- - -## Increase number of concurrent ports/sockets, deprecated in R17 -#-env ERL_MAX_PORTS 8192 - -#-env ERTS_MAX_PORTS 8192 - -## Mnesia and SSL will create temporary ets tables. --env ERL_MAX_ETS_TABLES 1024 - -## Tweak GC to run more often --env ERL_FULLSWEEP_AFTER 1000 - --env ERL_CRASH_DUMP log/emqttd_crash.dump From 0b7b9aff37106d7b936f05ebf45473b71e70b4ad Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 30 Jul 2016 21:23:15 +0800 Subject: [PATCH 039/116] #mqtt_credential{} --- include/emqttd.hrl | 53 +++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 56c595002..b10fc0475 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -26,13 +26,11 @@ -define(PROTOCOL_VERSION, "MQTT/3.1.1"). --define(ERTS_MINIMUM, "6.0"). +-define(ERTS_MINIMUM, "7.0"). -%% System Topics. --define(SYSTOP, <<"$SYS">>). - -%% Queue Topics. --define(QTop, <<"$Q">>). +-define(SYSTOP, <<"$SYS">>). %% System Topic +-define(QUEUE, <<"$queue">>). %% Queue +-define(SHARE, <<"$share">>). %% Shared %%-------------------------------------------------------------------- %% PubSub @@ -50,7 +48,17 @@ flags :: [retained | static] }). --type mqtt_topic() :: #mqtt_topic{}. +-type(mqtt_topic() :: #mqtt_topic{}). + +%%-------------------------------------------------------------------- +%% MQTT Route +%%-------------------------------------------------------------------- +-record(mqtt_route, { + topic :: binary(), + node :: node() +}). + +-type(mqtt_route() :: #mqtt_route{}). %%-------------------------------------------------------------------- %% MQTT Subscription @@ -63,15 +71,22 @@ -type mqtt_subscription() :: #mqtt_subscription{}. +%% {<<"a/b/c">>, '$queue', <<"client1">>} +%% {<<"a/b/c">>, undefined, <0.31.0>} +%% {<<"a/b/c">>, <<"group1">>, <<"client2">>} +-record(mqtt_subscription, {topic, share, destination :: pid() | binary()}). + %%-------------------------------------------------------------------- -%% MQTT Route +%<<"group1">>, <<"client2">>}% MQTT Credential %%-------------------------------------------------------------------- --record(mqtt_route, { - topic :: binary(), - node :: node() +-record(mqtt_credential, { + clientid :: binary() | undefined, %% ClientId + username :: binary() | undefined, %% Username + cookie :: binary() | undefined, + token :: binary() | undefined }). --type mqtt_route() :: #mqtt_route{}. +-type(mqtt_credential() || #mqtt_credential{}). %%-------------------------------------------------------------------- %% MQTT Client @@ -116,8 +131,6 @@ msgid :: mqtt_msgid(), %% Global unique message ID pktid :: mqtt_pktid(), %% PacketId topic :: binary(), %% Topic that the message is published to - from :: binary() | atom(), %% ClientId of the publisher - sender :: binary() | undefined, %% Username of the publisher qos = 0 :: 0 | 1 | 2, %% Message QoS flags = [] :: [retain | dup | sys], %% Message Flags retain = false :: boolean(), %% Retain flag @@ -128,7 +141,17 @@ extra = [] :: list() }). --type mqtt_message() :: #mqtt_message{}. +-type(mqtt_message() :: #mqtt_message{}). + +%%-------------------------------------------------------------------- +%% MQTT Delivery +%%-------------------------------------------------------------------- +-record(mqtt_delivery, { + sender :: pid(), %% Pid of the sender/publisher + credential :: mqtt_credential(), %% Credential of the sender/publisher + message :: mqtt_message(), %% Message + flow_through :: [node()] +}). %%-------------------------------------------------------------------- %% MQTT Alarm From 285421f073726137d7183c9ae4f734506985433f Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 1 Aug 2016 15:07:38 +0800 Subject: [PATCH 040/116] rm emqttd_backend.erl --- include/emqttd.hrl | 72 ++++++++------- src/emqttd.erl | 3 + src/emqttd_access_control.erl | 2 +- src/emqttd_backend.erl | 153 -------------------------------- src/emqttd_mod_subscription.erl | 16 +--- 5 files changed, 47 insertions(+), 199 deletions(-) delete mode 100644 src/emqttd_backend.erl diff --git a/include/emqttd.hrl b/include/emqttd.hrl index b10fc0475..1e5ed2d3a 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -28,9 +28,15 @@ -define(ERTS_MINIMUM, "7.0"). --define(SYSTOP, <<"$SYS">>). %% System Topic --define(QUEUE, <<"$queue">>). %% Queue --define(SHARE, <<"$share">>). %% Shared +%%-------------------------------------------------------------------- +%% Sys/Queue/Share Topics' Prefix +%%-------------------------------------------------------------------- + +-define(SYSTOP, <<"$SYS/">>). %% System Topic + +-define(QUEUE, <<"$queue/">>). %% Queue Topic + +-define(SHARE, <<"$share/">>). %% Shared Topic %%-------------------------------------------------------------------- %% PubSub @@ -38,7 +44,7 @@ -type pubsub() :: publish | subscribe. --define(IS_PUBSUB(PS), (PS =:= publish orelse PS =:= subscribe)). +-define(PUBSUB(PS), (PS =:= publish orelse PS =:= subscribe)). %%-------------------------------------------------------------------- %% MQTT Topic @@ -69,24 +75,24 @@ qos = 0 :: 0 | 1 | 2 }). --type mqtt_subscription() :: #mqtt_subscription{}. +-type(mqtt_subscription() :: #mqtt_subscription{}). %% {<<"a/b/c">>, '$queue', <<"client1">>} %% {<<"a/b/c">>, undefined, <0.31.0>} %% {<<"a/b/c">>, <<"group1">>, <<"client2">>} --record(mqtt_subscription, {topic, share, destination :: pid() | binary()}). +%% -record(mqtt_subscription, {topic, share, destination :: pid() | binary()}). %%-------------------------------------------------------------------- -%<<"group1">>, <<"client2">>}% MQTT Credential +%% MQTT Credential %%-------------------------------------------------------------------- -record(mqtt_credential, { clientid :: binary() | undefined, %% ClientId username :: binary() | undefined, %% Username - cookie :: binary() | undefined, - token :: binary() | undefined + token :: binary() | undefined, + cookie :: binary() | undefined }). --type(mqtt_credential() || #mqtt_credential{}). +-type(mqtt_credential() :: #mqtt_credential{}). %%-------------------------------------------------------------------- %% MQTT Client @@ -108,7 +114,7 @@ connected_at :: erlang:timestamp() }). --type mqtt_client() :: #mqtt_client{}. +-type(mqtt_client() :: #mqtt_client{}). %%-------------------------------------------------------------------- %% MQTT Session @@ -119,26 +125,29 @@ persistent :: boolean() }). --type mqtt_session() :: #mqtt_session{}. +-type(mqtt_session() :: #mqtt_session{}). %%-------------------------------------------------------------------- %% MQTT Message %%-------------------------------------------------------------------- --type mqtt_msgid() :: binary() | undefined. --type mqtt_pktid() :: 1..16#ffff | undefined. +-type(mqtt_msgid() :: binary() | undefined). +-type(mqtt_pktid() :: 1..16#ffff | undefined). -record(mqtt_message, { - msgid :: mqtt_msgid(), %% Global unique message ID - pktid :: mqtt_pktid(), %% PacketId - topic :: binary(), %% Topic that the message is published to - qos = 0 :: 0 | 1 | 2, %% Message QoS - flags = [] :: [retain | dup | sys], %% Message Flags - retain = false :: boolean(), %% Retain flag - dup = false :: boolean(), %% Dup flag - sys = false :: boolean(), %% $SYS flag - payload :: binary(), %% Payload - timestamp :: erlang:timestamp(), %% os:timestamp - extra = [] :: list() + msgid :: mqtt_msgid(), %% Global unique message ID + pktid :: mqtt_pktid(), %% PacketId + topic :: binary(), %% Topic that the message is published to + sender :: pid(), %% Pid of the sender/publisher + from, + credential :: mqtt_credential(), %% Credential of the sender/publisher + qos = 0 :: 0 | 1 | 2, %% Message QoS + flags = [] :: [retain | dup | sys], %% Message Flags + retain = false :: boolean(), %% Retain flag + dup = false :: boolean(), %% Dup flag + sys = false :: boolean(), %% $SYS flag + payload :: binary(), %% Payload + timestamp :: erlang:timestamp(), %% os:timestamp + extra = [] :: list() }). -type(mqtt_message() :: #mqtt_message{}). @@ -147,10 +156,9 @@ %% MQTT Delivery %%-------------------------------------------------------------------- -record(mqtt_delivery, { - sender :: pid(), %% Pid of the sender/publisher - credential :: mqtt_credential(), %% Credential of the sender/publisher - message :: mqtt_message(), %% Message - flow_through :: [node()] + message :: mqtt_message(), %% Message + dispatched = [] :: list(), + flow_through :: [node()] }). %%-------------------------------------------------------------------- @@ -164,7 +172,7 @@ timestamp :: erlang:timestamp() %% Timestamp }). --type mqtt_alarm() :: #mqtt_alarm{}. +-type(mqtt_alarm() :: #mqtt_alarm{}). %%-------------------------------------------------------------------- %% MQTT Plugin @@ -176,7 +184,7 @@ active = false }). --type mqtt_plugin() :: #mqtt_plugin{}. +-type(mqtt_plugin() :: #mqtt_plugin{}). %%-------------------------------------------------------------------- %% MQTT CLI Command @@ -191,5 +199,5 @@ descr }). --type mqtt_cli() :: #mqtt_cli{}. +-type(mqtt_cli() :: #mqtt_cli{}). diff --git a/src/emqttd.erl b/src/emqttd.erl index c5b697105..70b1459a3 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -26,6 +26,9 @@ -export([create/2, lookup/2, publish/1, subscribe/1, subscribe/3, unsubscribe/1, unsubscribe/3]). +%% Route and Forward API +%% -export([route/2, forward/2]). + %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/3]). diff --git a/src/emqttd_access_control.erl b/src/emqttd_access_control.erl index 079bdbec3..c2c8fb9b3 100644 --- a/src/emqttd_access_control.erl +++ b/src/emqttd_access_control.erl @@ -70,7 +70,7 @@ auth(Client, Password, [{Mod, State, _Seq} | Mods]) -> Client :: mqtt_client(), PubSub :: pubsub(), Topic :: binary()). -check_acl(Client, PubSub, Topic) when ?IS_PUBSUB(PubSub) -> +check_acl(Client, PubSub, Topic) when ?PUBSUB(PubSub) -> case lookup_mods(acl) of [] -> allow; AclMods -> check_acl(Client, PubSub, Topic, AclMods) diff --git a/src/emqttd_backend.erl b/src/emqttd_backend.erl deleted file mode 100644 index ead4c59d5..000000000 --- a/src/emqttd_backend.erl +++ /dev/null @@ -1,153 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_backend). - --include("emqttd.hrl"). - --include_lib("stdlib/include/ms_transform.hrl"). - -%% Mnesia Callbacks --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - -%% Retained Message API --export([retain_message/1, read_messages/1, match_messages/1, delete_message/1, - expire_messages/1, retained_count/0]). - -%% Static Subscription API --export([add_subscription/1, lookup_subscriptions/1, del_subscriptions/1, - del_subscription/2]). - --record(retained_message, {topic, msg}). - -%%-------------------------------------------------------------------- -%% Mnesia callbacks -%%-------------------------------------------------------------------- - -mnesia(boot) -> - ok = emqttd_mnesia:create_table(retained_message, [ - {type, ordered_set}, - {disc_copies, [node()]}, - {record_name, retained_message}, - {attributes, record_info(fields, retained_message)}, - {storage_properties, [{ets, [compressed]}, - {dets, [{auto_save, 1000}]}]}]), - ok = emqttd_mnesia:create_table(backend_subscription, [ - {type, bag}, - {disc_copies, [node()]}, - {record_name, mqtt_subscription}, - {attributes, record_info(fields, mqtt_subscription)}, - {storage_properties, [{ets, [compressed]}, - {dets, [{auto_save, 5000}]}]}]); - -mnesia(copy) -> - ok = emqttd_mnesia:copy_table(retained_message), - ok = emqttd_mnesia:copy_table(backend_subscription). - -%%-------------------------------------------------------------------- -%% Retained Message -%%-------------------------------------------------------------------- - --spec(retain_message(mqtt_message()) -> ok). -retain_message(Msg = #mqtt_message{topic = Topic}) -> - mnesia:dirty_write(#retained_message{topic = Topic, msg = Msg}). - --spec(read_messages(binary()) -> [mqtt_message()]). -read_messages(Topic) -> - [Msg || #retained_message{msg = Msg} <- mnesia:dirty_read(retained_message, Topic)]. - --spec(match_messages(binary()) -> [mqtt_message()]). -match_messages(Filter) -> - %% TODO: optimize later... - Fun = fun(#retained_message{topic = Name, msg = Msg}, Acc) -> - case emqttd_topic:match(Name, Filter) of - true -> [Msg|Acc]; - false -> Acc - end - end, - mnesia:async_dirty(fun mnesia:foldl/3, [Fun, [], retained_message]). - --spec(delete_message(binary()) -> ok). -delete_message(Topic) -> - mnesia:dirty_delete(retained_message, Topic). - --spec(expire_messages(pos_integer()) -> any()). -expire_messages(Time) when is_integer(Time) -> - mnesia:transaction( - fun() -> - Match = ets:fun2ms( - fun(#retained_message{topic = Topic, msg = #mqtt_message{timestamp = {MegaSecs, Secs, _}}}) - when Time > (MegaSecs * 1000000 + Secs) -> Topic - end), - Topics = mnesia:select(retained_message, Match, write), - lists:foreach(fun(<<"$SYS/", _/binary>>) -> ok; %% ignore $SYS/# messages - (Topic) -> mnesia:delete({retained_message, Topic}) - end, Topics) - end). - --spec(retained_count() -> non_neg_integer()). -retained_count() -> - mnesia:table_info(retained_message, size). - -%%-------------------------------------------------------------------- -%% Static Subscriptions -%%-------------------------------------------------------------------- - -%% @doc Add a static subscription manually. --spec(add_subscription(mqtt_subscription()) -> ok | {error, already_existed}). -add_subscription(Subscription = #mqtt_subscription{subid = SubId, topic = Topic}) -> - Pattern = match_pattern(SubId, Topic), - return(mnesia:transaction(fun() -> - case mnesia:match_object(backend_subscription, Pattern, write) of - [] -> - mnesia:write(backend_subscription, Subscription, write); - [Subscription] -> - mnesia:abort(already_existed); - [Subscription1] -> %% QoS is different - mnesia:delete_object(backend_subscription, Subscription1, write), - mnesia:write(backend_subscription, Subscription, write) - end - end)). - -%% @doc Lookup static subscriptions. --spec(lookup_subscriptions(binary()) -> list(mqtt_subscription())). -lookup_subscriptions(ClientId) when is_binary(ClientId) -> - mnesia:dirty_read(backend_subscription, ClientId). - -%% @doc Delete static subscriptions by ClientId manually. --spec(del_subscriptions(binary()) -> ok). -del_subscriptions(ClientId) when is_binary(ClientId) -> - return(mnesia:transaction(fun mnesia:delete/1, [{backend_subscription, ClientId}])). - -%% @doc Delete a static subscription manually. --spec(del_subscription(binary(), binary()) -> ok). -del_subscription(ClientId, Topic) when is_binary(ClientId) andalso is_binary(Topic) -> - return(mnesia:transaction(fun del_subscription_/1, [match_pattern(ClientId, Topic)])). - -del_subscription_(Pattern) -> - lists:foreach(fun(Subscription) -> - mnesia:delete_object(backend_subscription, Subscription, write) - end, mnesia:match_object(backend_subscription, Pattern, write)). - -match_pattern(SubId, Topic) -> - #mqtt_subscription{subid = SubId, topic = Topic, qos = '_'}. - -return({atomic, ok}) -> ok; -return({aborted, Reason}) -> {error, Reason}. - diff --git a/src/emqttd_mod_subscription.erl b/src/emqttd_mod_subscription.erl index c23ab6848..a545dcfaf 100644 --- a/src/emqttd_mod_subscription.erl +++ b/src/emqttd_mod_subscription.erl @@ -25,32 +25,22 @@ -export([load/1, on_client_connected/3, unload/1]). --record(state, {topics, backend = false}). - load(Opts) -> Topics = [{iolist_to_binary(Topic), QoS} || {Topic, QoS} <- Opts, ?IS_QOS(QoS)], - State = #state{topics = Topics, backend = lists:member(backend, Opts)}, - emqttd:hook('client.connected', fun ?MODULE:on_client_connected/3, [State]). + emqttd:hook('client.connected', fun ?MODULE:on_client_connected/3, [Topics]). on_client_connected(?CONNACK_ACCEPT, Client = #mqtt_client{client_id = ClientId, client_pid = ClientPid, - username = Username}, - #state{topics = Topics, backend = Backend}) -> + username = Username}, Topics) -> Replace = fun(Topic) -> rep(<<"$u">>, Username, rep(<<"$c">>, ClientId, Topic)) end, - TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- with_backend(Backend, ClientId, Topics)], + TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], emqttd_client:subscribe(ClientPid, TopicTable), {ok, Client}; on_client_connected(_ConnAck, _Client, _State) -> ok. -with_backend(false, _ClientId, TopicTable) -> - TopicTable; -with_backend(true, ClientId, TopicTable) -> - Fun = fun(#mqtt_subscription{topic = Topic, qos = Qos}) -> {Topic, Qos} end, - emqttd_opts:merge([Fun(Sub) || Sub <- emqttd_backend:lookup_subscriptions(ClientId)], TopicTable). - unload(_Opts) -> emqttd:unhook('client.connected', fun ?MODULE:on_client_connected/3). From 37bd5465bdb878095506fdd3551c80751374d0b2 Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 7 Aug 2016 18:09:07 +0800 Subject: [PATCH 041/116] support local route --- src/emqttd_pubsub.erl | 2 +- src/emqttd_router.erl | 62 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 5ccb6bc40..8bcaa7dc2 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -102,7 +102,7 @@ publish(Topic, Msg) -> ?MODULE:dispatch(To, Msg); (#mqtt_route{topic = To, node = Node}) -> rpc:cast(Node, ?MODULE, dispatch, [To, Msg]) - end, emqttd_router:lookup(Topic)). + end, emqttd_router:match(Topic)). %% @doc Dispatch Message to Subscribers -spec(dispatch(binary(), mqtt_message()) -> ok). diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl index 732cb448d..d946e945e 100644 --- a/src/emqttd_router.erl +++ b/src/emqttd_router.erl @@ -16,6 +16,8 @@ -module(emqttd_router). +-author("Feng Lee "). + -behaviour(gen_server). -include("emqttd.hrl"). @@ -27,18 +29,25 @@ -copy_mnesia({mnesia, [copy]}). %% Start/Stop --export([start_link/0, stop/0]). +-export([start_link/0, topics/0, local_topics/0, stop/0]). %% Route APIs --export([add_route/1, add_route/2, add_routes/1, lookup/1, print/1, +-export([add_route/1, add_route/2, add_routes/1, match/1, print/1, del_route/1, del_route/2, del_routes/1, has_route/1]). +%% Local Route API +-export([add_local_route/1, del_local_route/1, match_local/1]). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-export([dump/0]). + -record(state, {stats_timer}). +-define(ROUTER, ?MODULE). + %%-------------------------------------------------------------------- %% Mnesia Bootstrap %%-------------------------------------------------------------------- @@ -58,15 +67,21 @@ mnesia(copy) -> %%-------------------------------------------------------------------- start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + gen_server:start_link({local, ?ROUTER}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- -%% @doc Lookup Routes. --spec(lookup(Topic:: binary()) -> [mqtt_route()]). -lookup(Topic) when is_binary(Topic) -> +topics() -> + mnesia:dirty_all_keys(route). + +local_topics() -> + ets:select(local_route, [{{'$1', '_'}, [], ['$1']}]). + +%% @doc Match Routes. +-spec(match(Topic:: binary()) -> [mqtt_route()]). +match(Topic) when is_binary(Topic) -> Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [Topic]), %% Optimize: route table will be replicated to all nodes. lists:append([ets:lookup(route, To) || To <- [Topic | Matched]]). @@ -75,7 +90,7 @@ lookup(Topic) when is_binary(Topic) -> -spec(print(Topic :: binary()) -> [ok]). print(Topic) -> [io:format("~s -> ~s~n", [To, Node]) || - #mqtt_route{topic = To, node = Node} <- lookup(Topic)]. + #mqtt_route{topic = To, node = Node} <- match(Topic)]. %% @doc Add Route -spec(add_route(binary() | mqtt_route()) -> ok | {error, Reason :: any()}). @@ -166,13 +181,35 @@ trans(Fun) -> {aborted, Error} -> {error, Error} end. -stop() -> gen_server:call(?MODULE, stop). +%%-------------------------------------------------------------------- +%% Local Route API +%%-------------------------------------------------------------------- + +-spec(add_local_route(binary()) -> ok). +add_local_route(Topic) -> + gen_server:cast(?ROUTER, {add_local_route, Topic}). + +-spec(del_local_route(binary()) -> ok). +del_local_route(Topic) -> + gen_server:cast(?ROUTER, {del_local_route, Topic}). + +-spec(match_local(binary()) -> [mqtt_route()]). +match_local(Name) -> + [#mqtt_route{topic = {local, Filter}, node = Node} + || {Filter, Node} <- ets:tab2list(local_route), + emqttd_topic:match(Name, Filter)]. + +dump() -> + [{route, ets:tab2list(route)}, {local_route, ets:tab2list(local_route)}]. + +stop() -> gen_server:call(?ROUTER, stop). %%-------------------------------------------------------------------- %% gen_server Callbacks %%-------------------------------------------------------------------- init([]) -> + ets:new(local_route, [set, named_table, protected]), mnesia:subscribe(system), {ok, TRef} = timer:send_interval(timer:seconds(1), stats), {ok, #state{stats_timer = TRef}}. @@ -183,6 +220,15 @@ handle_call(stop, _From, State) -> handle_call(_Req, _From, State) -> {reply, ignore, State}. +handle_cast({add_local_route, Topic}, State) -> + %% why node()...? + ets:insert(local_route, {Topic, node()}), + {noreply, State}; + +handle_cast({del_local_route, Topic}, State) -> + ets:delete(local_route, Topic), + {noreply, State}; + handle_cast(_Msg, State) -> {noreply, State}. From e73df7f54f33c85838617d24bc3571d0293b2c25 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 8 Aug 2016 13:46:15 +0800 Subject: [PATCH 042/116] proc monitor module --- src/emqttd_pmon.erl | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/emqttd_pmon.erl diff --git a/src/emqttd_pmon.erl b/src/emqttd_pmon.erl new file mode 100644 index 000000000..dc9554015 --- /dev/null +++ b/src/emqttd_pmon.erl @@ -0,0 +1,50 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2016 Feng Lee . All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_pmon). + +-author("Feng Lee "). + +-type(pmon() :: {?MODULE, map()}). + +-export([new/0, monitor/2, demonitor/2, erase/2]). + +new() -> {?MODULE, [maps:new()]}. + +-spec(monitor(pid(), pmon()) -> pmon()). +monitor(Pid, PM = {?MODULE, [M]}) -> + case maps:is_key(Pid, M) of + true -> + PM; + false -> + Ref = erlang:monitor(process, Pid), + {?MODULE, [maps:put(Pid, Ref, M)]} + end. + +-spec(demonitor(pid(), pmon()) -> pmon()). +demonitor(Pid, PM = {?MODULE, [M]}) -> + case maps:find(Pid, M) of + {ok, Ref} -> + erlang:demonitor(Ref, [flush]), + {?MODULE, [maps:remove(Pid, M)]}; + error -> + PM + end. + +-spec(erase(pid(), pmon()) -> pmon()). +erase(Pid, {?MODULE, [M]}) -> + {?MODULE, [maps:remove(Pid, M)]}. + From 226933018a6679f6df2060f510ec149ab7374522 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 8 Aug 2016 13:49:52 +0800 Subject: [PATCH 043/116] improve the design of pubsub and router --- include/emqttd.hrl | 7 +- src/emqttd.erl | 120 +++++++----- src/emqttd_app.erl | 1 + src/emqttd_backend.erl | 95 ++++++++++ src/emqttd_cluster.erl | 3 +- src/emqttd_pubsub.erl | 387 ++++++++++++++++++++++++-------------- src/emqttd_pubsub_sup.erl | 42 +---- src/emqttd_server.erl | 278 --------------------------- src/emqttd_session.erl | 4 +- 9 files changed, 431 insertions(+), 506 deletions(-) create mode 100644 src/emqttd_backend.erl delete mode 100644 src/emqttd_server.erl diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 1e5ed2d3a..6142407d1 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -156,11 +156,12 @@ %% MQTT Delivery %%-------------------------------------------------------------------- -record(mqtt_delivery, { - message :: mqtt_message(), %% Message - dispatched = [] :: list(), - flow_through :: [node()] + message :: mqtt_message(), %% Message + flows :: list() }). +-type(mqtt_delivery() :: #mqtt_delivery{}). + %%-------------------------------------------------------------------- %% MQTT Alarm %%-------------------------------------------------------------------- diff --git a/src/emqttd.erl b/src/emqttd.erl index 70b1459a3..6b73d9027 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -16,6 +16,8 @@ -module(emqttd). +-author("Feng Lee "). + -include("emqttd.hrl"). -include("emqttd_protocol.hrl"). @@ -23,19 +25,31 @@ -export([start/0, conf/1, conf/2, env/1, env/2, is_running/1]). %% PubSub API --export([create/2, lookup/2, publish/1, subscribe/1, subscribe/3, - unsubscribe/1, unsubscribe/3]). +-export([subscribe/1, subscribe/2, subscribe/3, publish/1, + unsubscribe/1, unsubscribe/2]). -%% Route and Forward API -%% -export([route/2, forward/2]). +%% PubSub Management API +-export([topics/0, subscribers/1, subscriptions/1]). %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/3]). +%% Debug API +-export([dump/0]). + +-type(subscriber() :: pid() | binary() | function()). + +-type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). + +-type(pubsub_error() :: {error, {already_subscribed, binary()} + | {subscription_not_found, binary()}}). + +-export_type([subscriber/0, suboption/0, pubsub_error/0]). + -define(APP, ?MODULE). %%-------------------------------------------------------------------- -%% Bootstrap, environment, is_running... +%% Bootstrap, environment, configuration, is_running... %%-------------------------------------------------------------------- %% @doc Start emqttd application. @@ -67,52 +81,62 @@ is_running(Node) -> end. %%-------------------------------------------------------------------- -%% PubSub APIs that wrap emqttd_server, emqttd_pubsub +%% PubSub APIs that wrap emqttd_pubsub %%-------------------------------------------------------------------- -%% @doc Lookup Topic or Subscription --spec(lookup(topic, binary()) -> [mqtt_topic()]; - (subscription, binary()) -> [mqtt_subscription()]). -lookup(topic, Topic) when is_binary(Topic) -> - emqttd_pubsub:lookup_topic(Topic); +%% @doc Subscribe +-spec(subscribe(iodata()) -> ok | {error, any()}). +subscribe(Topic) -> + subscribe(Topic, self()). -lookup(subscription, ClientId) when is_binary(ClientId) -> - emqttd_server:lookup_subscription(ClientId). +-spec(subscribe(iodata(), subscriber()) -> ok | {error, any()}). +subscribe(Topic, Subscriber) -> + subscribe(Topic, Subscriber, []). -%% @doc Create a Topic or Subscription --spec(create(topic | subscription, binary()) -> ok | {error, any()}). -create(topic, Topic) when is_binary(Topic) -> - emqttd_pubsub:create_topic(Topic); - -create(subscription, {ClientId, Topic, Qos}) -> - Subscription = #mqtt_subscription{subid = ClientId, topic = Topic, qos = ?QOS_I(Qos)}, - emqttd_backend:add_subscription(Subscription). +-spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | pubsub_error()). +subscribe(Topic, Subscriber, Options) -> + with_pubsub(fun(PubSub) -> PubSub:subscribe(iolist_to_binary(Topic), Subscriber, Options) end). %% @doc Publish MQTT Message --spec(publish(mqtt_message()) -> ok). -publish(Msg) when is_record(Msg, mqtt_message) -> - emqttd_server:publish(Msg), ok. - -%% @doc Subscribe --spec(subscribe(binary()) -> ok; - ({binary(), binary(), mqtt_qos()}) -> ok). -subscribe(Topic) when is_binary(Topic) -> - emqttd_server:subscribe(Topic); -subscribe({ClientId, Topic, Qos}) -> - subscribe(ClientId, Topic, Qos). - --spec(subscribe(binary(), binary(), mqtt_qos()) -> {ok, mqtt_qos()}). -subscribe(ClientId, Topic, Qos) -> - emqttd_server:subscribe(ClientId, Topic, Qos). +-spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). +publish(Msg = #mqtt_message{from = From}) -> + trace(publish, From, Msg), + case run_hooks('message.publish', [], Msg) of + {ok, Msg1 = #mqtt_message{topic = Topic}} -> + %% Retain message first. Don't create retained topic. + Msg2 = case emqttd_retainer:retain(Msg1) of + ok -> emqttd_message:unset_flag(Msg1); + ignore -> Msg1 + end, + with_pubsub(fun(PubSub) -> PubSub:publish(Topic, Msg2) end); + {stop, Msg1} -> + lager:warning("Stop publishing: ~s", [emqttd_message:format(Msg1)]), + ignore + end. %% @doc Unsubscribe --spec(unsubscribe(binary()) -> ok). -unsubscribe(Topic) when is_binary(Topic) -> - emqttd_server:unsubscribe(Topic). +-spec(unsubscribe(iodata()) -> ok | pubsub_error()). +unsubscribe(Topic) -> + unsubscribe(Topic, self()). --spec(unsubscribe(binary(), binary(), mqtt_qos()) -> ok). -unsubscribe(ClientId, Topic, Qos) -> - emqttd_server:unsubscribe(ClientId, Topic, Qos). +-spec(unsubscribe(iodata(), subscriber()) -> ok | pubsub_error()). +unsubscribe(Topic, Subscriber) -> + with_pubsub(fun(PubSub) -> PubSub:unsubscribe(iolist_to_binary(Topic), Subscriber) end). + +-spec(topics() -> [binary()]). +topics() -> with_pubsub(fun(PubSub) -> PubSub:topics() end). + +-spec(subscribers(iodata()) -> list(subscriber())). +subscribers(Topic) -> + with_pubsub(fun(PubSub) -> PubSub:subscribers(iolist_to_binary(Topic)) end). + +-spec(subscriptions(subscriber()) -> [{binary(), suboption()}]). +subscriptions(Subscriber) -> + with_pubsub(fun(PubSub) -> PubSub:subscriptions(Subscriber) end). + +with_pubsub(Fun) -> Fun(conf(pubsub_adapter)). + +dump() -> with_pubsub(fun(PubSub) -> lists:append(PubSub:dump(), zenmq_router:dump()) end). %%-------------------------------------------------------------------- %% Hooks API @@ -134,3 +158,15 @@ unhook(Hook, Function) -> run_hooks(Hook, Args, Acc) -> emqttd_hook:run(Hook, Args, Acc). +%%-------------------------------------------------------------------- +%% Trace Functions +%%-------------------------------------------------------------------- + +trace(publish, From, _Msg) when is_atom(From) -> + %% Dont' trace '$SYS' publish + ignore; + +trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> + lager:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 870a349a6..81640c884 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -77,6 +77,7 @@ print_vsn() -> start_servers(Sup) -> Servers = [{"emqttd ctl", emqttd_ctl}, {"emqttd hook", emqttd_hook}, + {"emqttd router", emqttd_router}, {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, diff --git a/src/emqttd_backend.erl b/src/emqttd_backend.erl new file mode 100644 index 000000000..5515b2ac8 --- /dev/null +++ b/src/emqttd_backend.erl @@ -0,0 +1,95 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_backend). + +-include("emqttd.hrl"). + +-include_lib("stdlib/include/ms_transform.hrl"). + +%% Mnesia Callbacks +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + +%% Retained Message API +-export([retain_message/1, read_messages/1, match_messages/1, delete_message/1, + expire_messages/1, retained_count/0]). + +-record(retained_message, {topic, msg}). + +%%-------------------------------------------------------------------- +%% Mnesia callbacks +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = emqttd_mnesia:create_table(retained_message, [ + {type, ordered_set}, + {disc_copies, [node()]}, + {record_name, retained_message}, + {attributes, record_info(fields, retained_message)}, + {storage_properties, [{ets, [compressed]}, + {dets, [{auto_save, 1000}]}]}]); + +mnesia(copy) -> + ok = emqttd_mnesia:copy_table(retained_message). + +%%-------------------------------------------------------------------- +%% Retained Message +%%-------------------------------------------------------------------- + +-spec(retain_message(mqtt_message()) -> ok). +retain_message(Msg = #mqtt_message{topic = Topic}) -> + mnesia:dirty_write(#retained_message{topic = Topic, msg = Msg}). + +-spec(read_messages(binary()) -> [mqtt_message()]). +read_messages(Topic) -> + [Msg || #retained_message{msg = Msg} <- mnesia:dirty_read(retained_message, Topic)]. + +-spec(match_messages(binary()) -> [mqtt_message()]). +match_messages(Filter) -> + %% TODO: optimize later... + Fun = fun(#retained_message{topic = Name, msg = Msg}, Acc) -> + case emqttd_topic:match(Name, Filter) of + true -> [Msg|Acc]; + false -> Acc + end + end, + mnesia:async_dirty(fun mnesia:foldl/3, [Fun, [], retained_message]). + +-spec(delete_message(binary()) -> ok). +delete_message(Topic) -> + mnesia:dirty_delete(retained_message, Topic). + +-spec(expire_messages(pos_integer()) -> any()). +expire_messages(Time) when is_integer(Time) -> + mnesia:transaction( + fun() -> + Match = ets:fun2ms( + fun(#retained_message{topic = Topic, msg = #mqtt_message{timestamp = {MegaSecs, Secs, _}}}) + when Time > (MegaSecs * 1000000 + Secs) -> Topic + end), + Topics = mnesia:select(retained_message, Match, write), + lists:foreach(fun(<<"$SYS/", _/binary>>) -> ok; %% ignore $SYS/# messages + (Topic) -> mnesia:delete({retained_message, Topic}) + end, Topics) + end). + +-spec(retained_count() -> non_neg_integer()). +retained_count() -> + mnesia:table_info(retained_message, size). + diff --git a/src/emqttd_cluster.erl b/src/emqttd_cluster.erl index 05c2ecf70..834de2d71 100644 --- a/src/emqttd_cluster.erl +++ b/src/emqttd_cluster.erl @@ -82,6 +82,5 @@ remove(Node) -> end. %% @doc Cluster status -status() -> - emqttd_mnesia:cluster_status(). +status() -> emqttd_mnesia:cluster_status(). diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 8bcaa7dc2..cdf5cba31 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -16,6 +16,8 @@ -module(emqttd_pubsub). +-author("Feng Lee "). + -behaviour(gen_server2). -include("emqttd.hrl"). @@ -24,121 +26,170 @@ -include("emqttd_internal.hrl"). -%% Mnesia Callbacks --export([mnesia/1]). +%% Init And Start +-export([init_tabs/0, start_link/3]). --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). +%% PubSub API. +-export([subscribe/1, subscribe/2, subscribe/3, publish/2, + unsubscribe/1, unsubscribe/2]). -%% API Exports --export([start_link/3, create_topic/1, lookup_topic/1]). +%% Async PubSub API. +-export([async_subscribe/1, async_subscribe/2, async_subscribe/3, + async_unsubscribe/1, async_unsubscribe/2]). --export([subscribe/2, unsubscribe/2, publish/2, dispatch/2, - async_subscribe/2, async_unsubscribe/2]). +%% Management API. +-export([setqos/3, topics/0, subscribers/1, is_subscribed/2, subscriptions/1]). + +%% Route API +-export([forward/3, dispatch/2]). + +%% Debug API +-export([dump/0]). %% gen_server. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, env}). +-record(state, {pool, id, env, submon :: emqttd_pmon:pmon()}). + +-define(PUBSUB, ?MODULE). + +-define(is_local(Options), lists:member(local, Options)). + +-define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). %%-------------------------------------------------------------------- -%% Mnesia callbacks +%% Init ETS Tables %%-------------------------------------------------------------------- -mnesia(boot) -> - ok = emqttd_mnesia:create_table(topic, [ - {ram_copies, [node()]}, - {record_name, mqtt_topic}, - {attributes, record_info(fields, mqtt_topic)}]); +init_tabs() -> + %% Create ETS Tabs + lists:foreach(fun create_tab/1, [subscriber, subscription, subproperty]). -mnesia(copy) -> - ok = emqttd_mnesia:copy_table(topic). +create_tab(subscriber) -> + %% Subscriber: Topic -> Sub1, {Share, Sub2}, {Share, Sub3}, ..., SubN + %% duplicate_bag: o(1) insert + ensure_tab(subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); + +create_tab(subscription) -> + %% Subscription: Sub -> Topic1, {Share, Topic2}, {Share, Topic3}, ..., TopicN + %% bag: o(n) insert + ensure_tab(subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]); + +create_tab(subproperty) -> + %% Subproperty: {Topic, Sub} -> [local, {qos, 1}, {share, <<"share">>}] + ensure_tab(subproperty, [public, named_table, ordered_set | ?CONCURRENCY_OPTS]). + +ensure_tab(Tab, Opts) -> + case ets:info(Tab, name) of undefined -> ets:new(Tab, Opts); _ -> ok end. %%-------------------------------------------------------------------- %% Start PubSub %%-------------------------------------------------------------------- %% @doc Start one pubsub --spec(start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when - Pool :: atom(), - Id :: pos_integer(), - Env :: list(tuple())). +-spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). - -%% @doc Create a Topic. --spec(create_topic(binary()) -> ok | {error, any()}). -create_topic(Topic) when is_binary(Topic) -> - case mnesia:transaction(fun add_topic_/2, [Topic, [static]]) of - {atomic, ok} -> ok; - {aborted, Error} -> {error, Error} - end. - -%% @doc Lookup a Topic. --spec(lookup_topic(binary()) -> list(mqtt_topic())). -lookup_topic(Topic) when is_binary(Topic) -> - mnesia:dirty_read(topic, Topic). + gen_server2:start_link({local, ?PROC_NAME(?PUBSUB, Id)}, ?MODULE, [Pool, Id, Env], []). %%-------------------------------------------------------------------- %% PubSub API %%-------------------------------------------------------------------- %% @doc Subscribe a Topic --spec(subscribe(binary(), pid()) -> ok). -subscribe(Topic, SubPid) when is_binary(Topic) -> - call(pick(Topic), {subscribe, Topic, SubPid}). +-spec(subscribe(binary()) -> ok | emqttd:pubsub_error()). +subscribe(Topic) when is_binary(Topic) -> + subscribe(Topic, self()). -%% @doc Asynchronous Subscribe --spec(async_subscribe(binary(), pid()) -> ok). -async_subscribe(Topic, SubPid) when is_binary(Topic) -> - cast(pick(Topic), {subscribe, Topic, SubPid}). +-spec(subscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). +subscribe(Topic, Subscriber) when is_binary(Topic) -> + subscribe(Topic, Subscriber, []). + +-spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> + ok | emqttd:pubsub_error()). +subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> + call(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). + +%% @doc Subscribe a Topic Asynchronously +-spec(async_subscribe(binary()) -> ok). +async_subscribe(Topic) when is_binary(Topic) -> + async_subscribe(Topic, self()). + +-spec(async_subscribe(binary(), emqttd:subscriber()) -> ok). +async_subscribe(Topic, Subscriber) when is_binary(Topic) -> + async_subscribe(Topic, Subscriber, []). + +-spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> + cast(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). %% @doc Publish message to Topic. --spec(publish(binary(), any()) -> any()). -publish(Topic, Msg) -> - lists:foreach( - fun(#mqtt_route{topic = To, node = Node}) when Node =:= node() -> - ?MODULE:dispatch(To, Msg); - (#mqtt_route{topic = To, node = Node}) -> - rpc:cast(Node, ?MODULE, dispatch, [To, Msg]) - end, emqttd_router:match(Topic)). +-spec(publish(binary(), any()) -> {ok, mqtt_delivery()} | ignore). +publish(Topic, Msg) when is_binary(Topic) -> + route(emqttd_router:match(Topic), delivery(Msg)). + +%% Dispatch on the local node +route([#mqtt_route{topic = To, node = Node}], + Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> + dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); +%% Forward to other nodes +route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> + forward(Node, To, Delivery#mqtt_delivery{flows = [{route, Node, To}|Flows]}); + +route(Routes, Delivery) -> + {ok, lists:foldl(fun(Route, DelAcc) -> + {ok, DelAcc1} = route([Route], DelAcc), DelAcc1 + end, Delivery, Routes)}. + +delivery(Msg) -> #mqtt_delivery{message = Msg, flows = []}. + +%% @doc Forward message to another node... +forward(Node, To, Delivery) -> + rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}. %% @doc Dispatch Message to Subscribers --spec(dispatch(binary(), mqtt_message()) -> ok). -dispatch(Queue = <<"$queue/", _Q/binary>>, Msg) -> - case subscribers(Queue) of - [] -> - dropped(Queue); - [SubPid] -> - SubPid ! {dispatch, Queue, Msg}; - SubPids -> - Idx = crypto:rand_uniform(1, length(SubPids) + 1), - SubPid = lists:nth(Idx, SubPids), - SubPid ! {dispatch, Queue, Msg} - end; - -dispatch(Topic, Msg) -> +-spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). +dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> case subscribers(Topic) of [] -> - dropped(Topic); - [SubPid] -> - SubPid ! {dispatch, Topic, Msg}; - SubPids -> - lists:foreach(fun(SubPid) -> - SubPid ! {dispatch, Topic, Msg} - end, SubPids) + dropped(Topic), {ok, Delivery}; + [Sub] -> + dispatch(Sub, Topic, Msg), + {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1}|Flows]}}; + Subscribers -> + Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], + lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), + {ok, Delivery#mqtt_delivery{flows = Flows1}} end. -%% @private -%% @doc Find all subscribers +dispatch(Pid, Topic, Msg) when is_pid(Pid) -> + Pid ! {dispatch, Topic, Msg}; +dispatch(SubId, Topic, Msg) when is_binary(SubId) -> + emqttd_sm:dispatch(SubId, Topic, Msg). + +topics() -> emqttd_router:topics(). + subscribers(Topic) -> - case ets:member(subscriber, Topic) of - true -> %% faster then lookup? - try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end; - false -> - [] - end. + try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. + +subscriptions(Subscriber) -> + lists:map(fun({_, Topic}) -> + subscription(Topic, Subscriber) + end, ets:lookup(subscription, Subscriber)). + +subscription(Topic, Subscriber) -> + {Topic, ets:lookup_element(subproperty, {Topic, Subscriber}, 2)}. + +is_subscribed(Topic, Subscriber) when is_binary(Topic) -> + ets:member(subproperty, {Topic, Subscriber}). + +setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> + call(pick(Subscriber), {setqos, Topic, Subscriber, Qos}). + +dump() -> + [{subscriber, ets:tab2list(subscriber)}, + {subscription, ets:tab2list(subscription)}, + {subproperty, ets:tab2list(subproperty)}]. %% @private %% @doc Ingore $SYS Messages. @@ -148,14 +199,23 @@ dropped(_Topic) -> emqttd_metrics:inc('messages/dropped'). %% @doc Unsubscribe --spec(unsubscribe(binary(), pid()) -> ok). -unsubscribe(Topic, SubPid) when is_binary(Topic) -> - call(pick(Topic), {unsubscribe, Topic, SubPid}). +-spec(unsubscribe(binary()) -> ok | emqttd:pubsub_error()). +unsubscribe(Topic) when is_binary(Topic) -> + unsubscribe(Topic, self()). -%% @doc Asynchronous Unsubscribe --spec(async_unsubscribe(binary(), pid()) -> ok). -async_unsubscribe(Topic, SubPid) when is_binary(Topic) -> - cast(pick(Topic), {unsubscribe, Topic, SubPid}). +%% @doc Unsubscribe +-spec(unsubscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). +unsubscribe(Topic, Subscriber) when is_binary(Topic) -> + call(pick(Subscriber), {unsubscribe, Topic, Subscriber}). + +%% @doc Async Unsubscribe +-spec(async_unsubscribe(binary()) -> ok). +async_unsubscribe(Topic) when is_binary(Topic) -> + async_unsubscribe(Topic, self()). + +-spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). +async_unsubscribe(Topic, Subscriber) when is_binary(Topic) -> + cast(pick(Subscriber), {unsubscribe, Topic, Subscriber}). call(PubSub, Req) when is_pid(PubSub) -> gen_server2:call(PubSub, Req, infinity). @@ -172,30 +232,56 @@ pick(Topic) -> init([Pool, Id, Env]) -> ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env}}. + {ok, #state{pool = Pool, id = Id, env = Env, submon = emqttd_pmon:new()}}. -handle_call({subscribe, Topic, SubPid}, _From, State) -> - add_subscriber_(Topic, SubPid), - {reply, ok, setstats(State)}; +handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> + case do_subscribe_(Topic, Subscriber, Options, State) of + {ok, NewState} -> {reply, ok, setstats(NewState)}; + {error, Error} -> {reply, {error, Error}, State} + end; -handle_call({unsubscribe, Topic, SubPid}, _From, State) -> - del_subscriber_(Topic, SubPid), - {reply, ok, setstats(State)}; +handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> + case do_unsubscribe_(Topic, Subscriber, State) of + {ok, NewState} -> {reply, ok, setstats(NewState), hibernate}; + {error, Error} -> {reply, {error, Error}, State} + end; + +handle_call({setqos, Topic, Subscriber, Qos}, _From, State) -> + Key = {Topic, Subscriber}, + case ets:lookup(subproperty, Key) of + [{_, Opts}] -> + Opts1 = lists:ukeymerge(1, [{qos, Qos}], Opts), + ets:insert(subproperty, {Key, Opts1}), + {reply, ok, State}; + [] -> + {reply, {error, {subscription_not_found, Topic}}, State} + end; handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({subscribe, Topic, SubPid}, State) -> - add_subscriber_(Topic, SubPid), - {noreply, setstats(State)}; +handle_cast({subscribe, Topic, Subscriber, Options}, State) -> + case do_subscribe_(Topic, Subscriber, Options, State) of + {ok, NewState} -> {noreply, setstats(NewState)}; + {error, _Error} -> {noreply, State} + end; -handle_cast({unsubscribe, Topic, SubPid}, State) -> - del_subscriber_(Topic, SubPid), - {noreply, setstats(State)}; +handle_cast({unsubscribe, Topic, Subscriber}, State) -> + case do_unsubscribe_(Topic, Subscriber, State) of + {ok, NewState} -> {noreply, setstats(NewState), hibernate}; + {error, _Error} -> {noreply, State} + end; handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{submon = PM}) -> + lists:foreach(fun({_, Topic}) -> + subscriber_down_(DownPid, Topic) + end, ets:lookup(subscription, DownPid)), + ets:delete(subscription, DownPid), + {noreply, setstats(State#state{submon = PM:erase(DownPid)}), hibernate}; + handle_info(Info, State) -> ?UNEXPECTED_INFO(Info, State). @@ -209,62 +295,79 @@ code_change(_OldVsn, State, _Extra) -> %% Internal Functions %%-------------------------------------------------------------------- -add_subscriber_(Topic, SubPid) -> - case ets:member(subscriber, Topic) of - false -> - mnesia:transaction(fun add_topic_route_/2, [Topic, node()]), - setstats(topic); - true -> - ok - end, - ets:insert(subscriber, {Topic, SubPid}). - -del_subscriber_(Topic, SubPid) -> - ets:delete_object(subscriber, {Topic, SubPid}), - case ets:lookup(subscriber, Topic) of +do_subscribe_(Topic, Subscriber, Options, State) -> + case ets:lookup(subproperty, {Topic, Subscriber}) of [] -> - mnesia:transaction(fun del_topic_route_/2, [Topic, node()]), - setstats(topic); - [_|_] -> - ok + do_subscribe2_(Topic, Subscriber, Options), + ets:insert(subproperty, {{Topic, Subscriber}, Options}), + {ok, monitor_subpid(Subscriber, State)}; + [_] -> + {error, {already_subscribed, Topic}} end. -add_topic_route_(Topic, Node) -> - add_topic_(Topic), emqttd_router:add_route(Topic, Node). +do_subscribe2_(Topic, Subscriber, _Options) -> + add_subscription_(Subscriber, Topic), + add_subscriber_(Topic, Subscriber). -add_topic_(Topic) -> - add_topic_(Topic, []). +add_subscription_(Subscriber, Topic) -> + ets:insert(subscription, {Subscriber, Topic}). -add_topic_(Topic, Flags) -> - Record = #mqtt_topic{topic = Topic, flags = Flags}, - case mnesia:wread({topic, Topic}) of - [] -> mnesia:write(topic, Record, write); - [_] -> ok +add_subscriber_(Topic, Subscriber) -> + %%TODO: LOCK here... + case ets:member(subscriber, Topic) of + false -> emqttd_router:add_route(Topic, node()); + true -> ok + end, + ets:insert(subscriber, {Topic, Subscriber}). + +do_unsubscribe_(Topic, Subscriber, State) -> + case ets:lookup(subproperty, {Topic, Subscriber}) of + [_] -> + del_subscriber_(Topic, Subscriber), + del_subscription(Subscriber, Topic), + ets:delete(subproperty, {Topic, Subscriber}), + {ok, case ets:member(subscription, Subscriber) of + true -> State; + false -> demonitor_subpid(Subscriber, State) + end}; + [] -> + {error, {subscription_not_found, Topic}} end. -del_topic_route_(Topic, Node) -> - emqttd_router:del_route(Topic, Node), del_topic_(Topic). +del_subscription(Subscriber, Topic) -> + ets:delete_object(subscription, {Subscriber, Topic}). -del_topic_(Topic) -> - case emqttd_router:has_route(Topic) of - true -> ok; - false -> do_del_topic_(Topic) +del_subscriber_(Topic, Subscriber) -> + ets:delete_object(subscriber, {Topic, Subscriber}), + %%TODO: LOCK TOPIC + case ets:member(subscriber, Topic) of + false -> emqttd_router:del_route(Topic, node()); + true -> ok end. -do_del_topic_(Topic) -> - case mnesia:wread({topic, Topic}) of - [#mqtt_topic{flags = []}] -> - mnesia:delete(topic, Topic, write); - _ -> - ok +subscriber_down_(DownPid, Topic) -> + case ets:lookup(subproperty, {Topic, DownPid}) of + [] -> del_subscriber_(Topic, DownPid); %%TODO: warning? + [_] -> del_subscriber_(Topic, DownPid), + ets:delete(subproperty, {Topic, DownPid}) end. +monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> + State#state{submon = PMon:monitor(SubPid)}; +monitor_subpid(_SubPid, State) -> + State. + +demonitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> + State#state{submon = PMon:demonitor(SubPid)}; +demonitor_subpid(_SubPid, State) -> + State. + setstats(State) when is_record(State, state) -> - setstats(subscriber), State; - -setstats(topic) -> - emqttd_stats:setstats('topics/count', 'topics/max', mnesia:table_info(topic, size)); + setstats(subscriber), setstats(subscription), State; setstats(subscriber) -> - emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(subscriber, size)). + emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(subscriber, size)); + +setstats(subscription) -> + emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', ets:info(subscription, size)). diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 1677cec8d..72d4fce68 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -19,10 +19,6 @@ -behaviour(supervisor). --include("emqttd.hrl"). - --define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). - %% API -export([start_link/0, pubsub_pool/0]). @@ -36,41 +32,13 @@ pubsub_pool() -> hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). init([Env]) -> - - %% Create ETS Tabs - create_tab(subscriber), create_tab(subscribed), - - %% Router - Router = {router, {emqttd_router, start_link, []}, - permanent, 5000, worker, [emqttd_router]}, - - %% PubSub Pool Sup - PubSubMFA = {emqttd_pubsub, start_link, [Env]}, - PubSubPoolSup = emqttd_pool_sup:spec(pubsub_pool, [pubsub, hash, pool_size(Env), PubSubMFA]), - - %% Server Pool Sup - ServerMFA = {emqttd_server, start_link, [Env]}, - ServerPoolSup = emqttd_pool_sup:spec(server_pool, [server, hash, pool_size(Env), ServerMFA]), - - {ok, {{one_for_all, 5, 60}, [Router, PubSubPoolSup, ServerPoolSup]}}. + PubSub = emqttd:conf(pubsub_adapter), + PubSubMFA = {PubSub, start_link, [Env]}, + PoolArgs = [pubsub, hash, pool_size(Env), PubSubMFA], + PubSubPoolSup = emqttd_pool_sup:spec(pubsub_pool, PoolArgs), + {ok, { {one_for_all, 10, 3600}, [PubSubPoolSup]} }. pool_size(Env) -> Schedulers = erlang:system_info(schedulers), proplists:get_value(pool_size, Env, Schedulers). -create_tab(subscriber) -> - %% subscriber: Topic -> Pid1, Pid2, ..., PidN - %% duplicate_bag: o(1) insert - ensure_tab(subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); - -create_tab(subscribed) -> - %% subscribed: Pid -> Topic1, Topic2, ..., TopicN - %% bag: o(n) insert - ensure_tab(subscribed, [public, named_table, bag | ?CONCURRENCY_OPTS]). - -ensure_tab(Tab, Opts) -> - case ets:info(Tab, name) of - undefined -> ets:new(Tab, Opts); - _ -> ok - end. - diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl deleted file mode 100644 index 1466d8a7a..000000000 --- a/src/emqttd_server.erl +++ /dev/null @@ -1,278 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_server). - --behaviour(gen_server2). - --include("emqttd.hrl"). - --include("emqttd_protocol.hrl"). - --include("emqttd_internal.hrl"). - -%% Mnesia Callbacks --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - -%% API Exports --export([start_link/3]). - -%% PubSub API --export([subscribe/1, subscribe/3, publish/1, unsubscribe/1, unsubscribe/3, - lookup_subscription/1, update_subscription/4]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id, env, monitors}). - -%%-------------------------------------------------------------------- -%% Mnesia callbacks -%%-------------------------------------------------------------------- - -mnesia(boot) -> - ok = emqttd_mnesia:create_table(subscription, [ - {type, bag}, - {ram_copies, [node()]}, - {local_content, true}, %% subscription table is local - {record_name, mqtt_subscription}, - {attributes, record_info(fields, mqtt_subscription)}]); - -mnesia(copy) -> - ok = emqttd_mnesia:copy_table(subscription). - -%%-------------------------------------------------------------------- -%% Start server -%%-------------------------------------------------------------------- - -%% @doc Start a Server --spec(start_link(Pool, Id, Env) -> {ok, pid()} | ignore | {error, any()} when - Pool :: atom(), - Id :: pos_integer(), - Env :: list(tuple())). -start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). - -%%-------------------------------------------------------------------- -%% PubSub API -%%-------------------------------------------------------------------- - -%% @doc Subscribe a Topic --spec(subscribe(binary()) -> ok). -subscribe(Topic) when is_binary(Topic) -> - From = self(), call(server(From), {subscribe, From, Topic}). - -%% @doc Subscribe from a MQTT session. --spec(subscribe(binary(), binary(), mqtt_qos()) -> ok). -subscribe(ClientId, Topic, Qos) -> - From = self(), call(server(From), {subscribe, From, ClientId, Topic, ?QOS_I(Qos)}). - -%% @doc Lookup subscriptions. --spec(lookup_subscription(binary()) -> [#mqtt_subscription{}]). -lookup_subscription(ClientId) -> - mnesia:dirty_read(subscription, ClientId). - -%% @doc Update a subscription. --spec(update_subscription(binary(), binary(), mqtt_qos(), mqtt_qos()) -> ok). -update_subscription(ClientId, Topic, OldQos, NewQos) -> - call(server(self()), {update_subscription, ClientId, Topic, ?QOS_I(OldQos), ?QOS_I(NewQos)}). - -%% @doc Publish a Message --spec(publish(Msg :: mqtt_message()) -> any()). -publish(Msg = #mqtt_message{from = From}) -> - trace(publish, From, Msg), - case emqttd:run_hooks('message.publish', [], Msg) of - {ok, Msg1 = #mqtt_message{topic = Topic}} -> - %% Retain message first. Don't create retained topic. - Msg2 = case emqttd_retainer:retain(Msg1) of - ok -> emqttd_message:unset_flag(Msg1); - ignore -> Msg1 - end, - emqttd_pubsub:publish(Topic, Msg2); - {stop, Msg1} -> - lager:warning("Stop publishing: ~s", [emqttd_message:format(Msg1)]) - end. - -%% @doc Unsubscribe a Topic --spec(unsubscribe(binary()) -> ok). -unsubscribe(Topic) when is_binary(Topic) -> - From = self(), call(server(From), {unsubscribe, From, Topic}). - -%% @doc Unsubscribe a Topic from a MQTT session --spec(unsubscribe(binary(), binary(), mqtt_qos()) -> ok). -unsubscribe(ClientId, Topic, Qos) -> - From = self(), call(server(From), {unsubscribe, From, ClientId, Topic, Qos}). - -call(Server, Req) -> - gen_server2:call(Server, Req, infinity). - -server(From) -> - gproc_pool:pick_worker(server, From). - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- - -init([Pool, Id, Env]) -> - ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env, monitors = dict:new()}}. - -handle_call({subscribe, SubPid, ClientId, Topic, Qos}, _From, State) -> - pubsub_subscribe_(SubPid, Topic), - if_subsciption(State, fun() -> - add_subscription_(ClientId, Topic, Qos), - set_subscription_stats() - end), - ok(monitor_subscriber_(ClientId, SubPid, State)); - -handle_call({subscribe, SubPid, Topic}, _From, State) -> - pubsub_subscribe_(SubPid, Topic), - ok(monitor_subscriber_(undefined, SubPid, State)); - -handle_call({update_subscription, ClientId, Topic, OldQos, NewQos}, _From, State) -> - if_subsciption(State, fun() -> - OldSub = #mqtt_subscription{subid = ClientId, topic = Topic, qos = OldQos}, - NewSub = #mqtt_subscription{subid = ClientId, topic = Topic, qos = NewQos}, - mnesia:transaction(fun update_subscription_/2, [OldSub, NewSub]), - set_subscription_stats() - end), ok(State); - -handle_call({unsubscribe, SubPid, ClientId, Topic, Qos}, _From, State) -> - pubsub_unsubscribe_(SubPid, Topic), - if_subsciption(State, fun() -> - del_subscription_(ClientId, Topic, Qos), - set_subscription_stats() - end), ok(State); - -handle_call({unsubscribe, SubPid, Topic}, _From, State) -> - pubsub_unsubscribe_(SubPid, Topic), ok(State); - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{monitors = Monitors}) -> - %% unsubscribe - lists:foreach(fun({_, Topic}) -> - emqttd_pubsub:async_unsubscribe(Topic, DownPid) - end, ets:lookup(subscribed, DownPid)), - ets:delete(subscribed, DownPid), - - %% clean subscriptions - case dict:find(DownPid, Monitors) of - {ok, {undefined, _}} -> ok; - {ok, {ClientId, _}} -> mnesia:dirty_delete(subscription, ClientId); - error -> ok - end, - {noreply, State#state{monitors = dict:erase(DownPid, Monitors)}, hibernate}; - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internal Functions -%%-------------------------------------------------------------------- - -if_subsciption(#state{env = Env}, Fun) -> - case proplists:get_value(subscription, Env, true) of - false -> ok; - _true -> Fun() - end. - -%% @private -%% @doc Add a subscription. --spec(add_subscription_(binary(), binary(), mqtt_qos()) -> ok). -add_subscription_(ClientId, Topic, Qos) -> - add_subscription_(#mqtt_subscription{subid = ClientId, topic = Topic, qos = Qos}). - --spec(add_subscription_(mqtt_subscription()) -> ok). -add_subscription_(Subscription) when is_record(Subscription, mqtt_subscription) -> - mnesia:dirty_write(subscription, Subscription). - -update_subscription_(OldSub, NewSub) -> - mnesia:delete_object(subscription, OldSub, write), - mnesia:write(subscription, NewSub, write). - -%% @private -%% @doc Delete a subscription --spec(del_subscription_(binary(), binary(), mqtt_qos()) -> ok). -del_subscription_(ClientId, Topic, Qos) -> - del_subscription_(#mqtt_subscription{subid = ClientId, topic = Topic, qos = Qos}). - -del_subscription_(Subscription) when is_record(Subscription, mqtt_subscription) -> - mnesia:dirty_delete_object(subscription, Subscription). - -%% @private -%% @doc Call pubsub to subscribe -pubsub_subscribe_(SubPid, Topic) -> - case ets:match(subscribed, {SubPid, Topic}) of - [] -> - emqttd_pubsub:async_subscribe(Topic, SubPid), - ets:insert(subscribed, {SubPid, Topic}); - [_] -> - false - end. - -%% @private -pubsub_unsubscribe_(SubPid, Topic) -> - emqttd_pubsub:async_unsubscribe(Topic, SubPid), - ets:delete_object(subscribed, {SubPid, Topic}). - -monitor_subscriber_(ClientId, SubPid, State = #state{monitors = Monitors}) -> - case dict:find(SubPid, Monitors) of - {ok, _} -> - State; - error -> - MRef = erlang:monitor(process, SubPid), - State#state{monitors = dict:store(SubPid, {ClientId, MRef}, Monitors)} - end. - -%%-------------------------------------------------------------------- -%% Trace Functions -%%-------------------------------------------------------------------- - -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; - -trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> - lager:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). - -%%-------------------------------------------------------------------- -%% Subscription Statistics -%%-------------------------------------------------------------------- - -set_subscription_stats() -> - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', - mnesia:table_info(subscription, size)). - -%%-------------------------------------------------------------------- - -ok(State) -> {reply, ok, State}. - diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 6e0586b94..6f2b3c6dd 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -297,11 +297,11 @@ handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), SubDict; {ok, OldQos} -> - emqttd_server:update_subscription(ClientId, Topic, OldQos, Qos), + emqttd_pubsub:setqos(Topic, ClientId, Qos), ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), dict:store(Topic, Qos, SubDict); error -> - emqttd:subscribe(ClientId, Topic, Qos), + emqttd:subscribe(Topic, ClientId, [{qos, Qos}]), %%TODO: the design is ugly... %% : 3.8.4 %% Where the Topic Filter is not identical to any existing Subscription’s filter, From d6c9ee66a373961222aff78d280e3dd4c271e535 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 8 Aug 2016 13:53:35 +0800 Subject: [PATCH 044/116] Version 2.0 - Publish, Route, Dispatch, Deliver --- docs/source/_static/images/publish.png | Bin 0 -> 22506 bytes docs/source/changes.rst | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 docs/source/_static/images/publish.png diff --git a/docs/source/_static/images/publish.png b/docs/source/_static/images/publish.png new file mode 100644 index 0000000000000000000000000000000000000000..0884fc22436fa223ba20b14a324b53dd154d0df6 GIT binary patch literal 22506 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 literal 0 HcmV?d00001 diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 548233125..68230b197 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -5,6 +5,18 @@ Changes ======= +.. _release_2.0: + +------------------------------- +Version 2.0 (West of West Lake) +------------------------------- + +*Release Date: 2016-08-12* + +Improve the design of PubSub and Router: + +.. images:: _static/images/publish.png + .. _release_1.1.2: ------------- From 8b6cc4d5f84460937e337a510bebfa580896ee16 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 8 Aug 2016 14:01:49 +0800 Subject: [PATCH 045/116] don't create topics --- src/emqttd_broker.erl | 6 ------ src/emqttd_metrics.erl | 2 +- src/emqttd_stats.erl | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/emqttd_broker.erl b/src/emqttd_broker.erl index 0bc27afa1..2d53a1328 100644 --- a/src/emqttd_broker.erl +++ b/src/emqttd_broker.erl @@ -115,9 +115,6 @@ stop_tick(TRef) -> init([]) -> emqttd_time:seed(), ets:new(?BROKER_TAB, [set, public, named_table]), - % Create $SYS Topics - emqttd:create(topic, <<"$SYS/brokers">>), - [ok = create_topic(Topic) || Topic <- ?SYSTOP_BROKERS], % Tick {ok, #state{started_at = os:timestamp(), heartbeat = start_tick(1000, heartbeat), @@ -160,9 +157,6 @@ code_change(_OldVsn, State, _Extra) -> %% Internal functions %%-------------------------------------------------------------------- -create_topic(Topic) -> - emqttd:create(topic, emqttd_topic:systop(Topic)). - retain(brokers) -> Payload = list_to_binary(string:join([atom_to_list(N) || N <- emqttd_mnesia:running_nodes()], ",")), diff --git a/src/emqttd_metrics.erl b/src/emqttd_metrics.erl index 79bfa95db..651e5037a 100644 --- a/src/emqttd_metrics.erl +++ b/src/emqttd_metrics.erl @@ -243,7 +243,7 @@ init([]) -> % Init metrics [create_metric(Metric) || Metric <- Metrics], % $SYS Topics for metrics - [ok = emqttd:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics], + % [ok = emqttd:create(topic, metric_topic(Topic)) || {_, Topic} <- Metrics], % Tick to publish metrics {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. diff --git a/src/emqttd_stats.erl b/src/emqttd_stats.erl index 478313fd0..9ffc08299 100644 --- a/src/emqttd_stats.erl +++ b/src/emqttd_stats.erl @@ -122,7 +122,7 @@ init([]) -> Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB ++ ?SYSTOP_RETAINED, ets:insert(?STATS_TAB, [{Topic, 0} || Topic <- Topics]), % Create $SYS Topics - [ok = emqttd:create(topic, stats_topic(Topic)) || Topic <- Topics], + % [ok = emqttd:create(topic, stats_topic(Topic)) || Topic <- Topics], % Tick to publish stats {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. From b3a48b937f287baa97aa8eef214296860e8bc605 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 8 Aug 2016 14:06:52 +0800 Subject: [PATCH 046/116] pubsub_adapter --- src/emqttd.erl | 2 +- src/emqttd_pubsub_sup.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index 6b73d9027..763928b30 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -134,7 +134,7 @@ subscribers(Topic) -> subscriptions(Subscriber) -> with_pubsub(fun(PubSub) -> PubSub:subscriptions(Subscriber) end). -with_pubsub(Fun) -> Fun(conf(pubsub_adapter)). +with_pubsub(Fun) -> {ok, PubSub} = conf(pubsub_adapter), Fun(PubSub). dump() -> with_pubsub(fun(PubSub) -> lists:append(PubSub:dump(), zenmq_router:dump()) end). diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 72d4fce68..5e26ed092 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -32,7 +32,7 @@ pubsub_pool() -> hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). init([Env]) -> - PubSub = emqttd:conf(pubsub_adapter), + {ok, PubSub} = emqttd:conf(pubsub_adapter), PubSubMFA = {PubSub, start_link, [Env]}, PoolArgs = [pubsub, hash, pool_size(Env), PubSubMFA], PubSubPoolSup = emqttd_pool_sup:spec(pubsub_pool, PoolArgs), From bc72e33056461c118576e04f1295ff50bb156092 Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 8 Aug 2016 14:14:54 +0800 Subject: [PATCH 047/116] PubSub:init_tabs --- src/emqttd_pubsub_sup.erl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 5e26ed092..414868fbf 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -32,11 +32,10 @@ pubsub_pool() -> hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). init([Env]) -> - {ok, PubSub} = emqttd:conf(pubsub_adapter), - PubSubMFA = {PubSub, start_link, [Env]}, - PoolArgs = [pubsub, hash, pool_size(Env), PubSubMFA], - PubSubPoolSup = emqttd_pool_sup:spec(pubsub_pool, PoolArgs), - {ok, { {one_for_all, 10, 3600}, [PubSubPoolSup]} }. + {ok, PubSub} = emqttd:conf(pubsub_adapter), PubSub:init_tabs(), + PoolArgs = [pubsub, hash, pool_size(Env), {PubSub, start_link, [Env]}], + PoolSup = emqttd_pool_sup:spec(pubsub_pool, PoolArgs), + {ok, { {one_for_all, 10, 3600}, [PoolSup]} }. pool_size(Env) -> Schedulers = erlang:system_info(schedulers), From 28be1e2a0056aa1a20e7ff6b9d156dc2355e9fbe Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 8 Aug 2016 14:34:11 +0800 Subject: [PATCH 048/116] Add dispatch/3 --- src/emqttd_sm.erl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index 05aed7b42..3ab36186f 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -36,6 +36,8 @@ -export([register_session/3, unregister_session/2]). +-export([dispatch/3]). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -105,6 +107,15 @@ register_session(CleanSess, ClientId, Info) -> unregister_session(CleanSess, ClientId) -> ets:delete(sesstab(CleanSess), {ClientId, self()}). +%%TODO: FIXME... +dispatch(Id, Topic, Msg) -> + case lookup_session(Id) of + #mqtt_session{sess_pid = Pid} -> + Pid ! {dispatch, Topic, Msg}; + undefined -> + ok + end. + sesstab(true) -> mqtt_transient_session; sesstab(false) -> mqtt_persistent_session. From 317ce3cfdf6cbea610b63b38a94580c506fc3f66 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 8 Aug 2016 14:58:49 +0800 Subject: [PATCH 049/116] ok --- src/emqttd_session.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 6f2b3c6dd..8c2bbf8bc 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -175,11 +175,11 @@ subscribe(SessPid, PacketId, TopicTable) -> -spec(publish(pid(), mqtt_message()) -> ok | {error, any()}). publish(_SessPid, Msg = #mqtt_message{qos = ?QOS_0}) -> %% publish qos0 directly - emqttd:publish(Msg); + emqttd:publish(Msg), ok; publish(_SessPid, Msg = #mqtt_message{qos = ?QOS_1}) -> %% publish qos1 directly, and client will puback automatically - emqttd:publish(Msg); + emqttd:publish(Msg), ok; publish(SessPid, Msg = #mqtt_message{qos = ?QOS_2}) -> %% publish qos2 by session From 4c4d9a718ca4060ac77ad200dd542c5c1292ea58 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 8 Aug 2016 17:41:48 +0800 Subject: [PATCH 050/116] emqttd_router:dump/0 --- src/emqttd.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index 763928b30..6914d7993 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -136,7 +136,7 @@ subscriptions(Subscriber) -> with_pubsub(Fun) -> {ok, PubSub} = conf(pubsub_adapter), Fun(PubSub). -dump() -> with_pubsub(fun(PubSub) -> lists:append(PubSub:dump(), zenmq_router:dump()) end). +dump() -> with_pubsub(fun(PubSub) -> lists:append(PubSub:dump(), emqttd_router:dump()) end). %%-------------------------------------------------------------------- %% Hooks API From af6c7796311bea33f627c5cd935efcdd36a3f370 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 9 Aug 2016 18:45:47 +0800 Subject: [PATCH 051/116] improve the pubsub design --- src/emqttd_pubsub.erl | 107 +++++++++--------------------------- src/emqttd_pubsub_sup.erl | 29 +++++++++- src/emqttd_submgr.erl | 111 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 82 deletions(-) create mode 100644 src/emqttd_submgr.erl diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index cdf5cba31..704dbe663 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -26,8 +26,8 @@ -include("emqttd_internal.hrl"). -%% Init And Start --export([init_tabs/0, start_link/3]). +%% Start +-export([start_link/3]). %% PubSub API. -export([subscribe/1, subscribe/2, subscribe/3, publish/2, @@ -54,40 +54,7 @@ -define(PUBSUB, ?MODULE). --define(is_local(Options), lists:member(local, Options)). - --define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). - -%%-------------------------------------------------------------------- -%% Init ETS Tables -%%-------------------------------------------------------------------- - -init_tabs() -> - %% Create ETS Tabs - lists:foreach(fun create_tab/1, [subscriber, subscription, subproperty]). - -create_tab(subscriber) -> - %% Subscriber: Topic -> Sub1, {Share, Sub2}, {Share, Sub3}, ..., SubN - %% duplicate_bag: o(1) insert - ensure_tab(subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); - -create_tab(subscription) -> - %% Subscription: Sub -> Topic1, {Share, Topic2}, {Share, Topic3}, ..., TopicN - %% bag: o(n) insert - ensure_tab(subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]); - -create_tab(subproperty) -> - %% Subproperty: {Topic, Sub} -> [local, {qos, 1}, {share, <<"share">>}] - ensure_tab(subproperty, [public, named_table, ordered_set | ?CONCURRENCY_OPTS]). - -ensure_tab(Tab, Opts) -> - case ets:info(Tab, name) of undefined -> ets:new(Tab, Opts); _ -> ok end. - -%%-------------------------------------------------------------------- -%% Start PubSub -%%-------------------------------------------------------------------- - -%% @doc Start one pubsub +%% @doc Start a pubsub server -spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). start_link(Pool, Id, Env) -> gen_server2:start_link({local, ?PROC_NAME(?PUBSUB, Id)}, ?MODULE, [Pool, Id, Env], []). @@ -110,7 +77,7 @@ subscribe(Topic, Subscriber) when is_binary(Topic) -> subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> call(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). -%% @doc Subscribe a Topic Asynchronously +%% @doc Subscribe a Topic Asynchronously -spec(async_subscribe(binary()) -> ok). async_subscribe(Topic) when is_binary(Topic) -> async_subscribe(Topic, self()). @@ -132,6 +99,7 @@ publish(Topic, Msg) when is_binary(Topic) -> route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); + %% Forward to other nodes route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> forward(Node, To, Delivery#mqtt_delivery{flows = [{route, Node, To}|Flows]}); @@ -153,9 +121,9 @@ dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> case subscribers(Topic) of [] -> dropped(Topic), {ok, Delivery}; - [Sub] -> + [Sub] -> %% optimize? dispatch(Sub, Topic, Msg), - {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1}|Flows]}}; + {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1} | Flows]}}; Subscribers -> Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), @@ -223,8 +191,8 @@ call(PubSub, Req) when is_pid(PubSub) -> cast(PubSub, Msg) when is_pid(PubSub) -> gen_server2:cast(PubSub, Msg). -pick(Topic) -> - gproc_pool:pick_worker(pubsub, Topic). +pick(Subscriber) -> + gproc_pool:pick_worker(pubsub, Subscriber). %%-------------------------------------------------------------------- %% gen_server Callbacks @@ -235,13 +203,13 @@ init([Pool, Id, Env]) -> {ok, #state{pool = Pool, id = Id, env = Env, submon = emqttd_pmon:new()}}. handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> - case do_subscribe_(Topic, Subscriber, Options, State) of + case do_subscribe(Topic, Subscriber, Options, State) of {ok, NewState} -> {reply, ok, setstats(NewState)}; {error, Error} -> {reply, {error, Error}, State} end; handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> - case do_unsubscribe_(Topic, Subscriber, State) of + case do_unsubscribe(Topic, Subscriber, State) of {ok, NewState} -> {reply, ok, setstats(NewState), hibernate}; {error, Error} -> {reply, {error, Error}, State} end; @@ -261,13 +229,13 @@ handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). handle_cast({subscribe, Topic, Subscriber, Options}, State) -> - case do_subscribe_(Topic, Subscriber, Options, State) of + case do_subscribe(Topic, Subscriber, Options, State) of {ok, NewState} -> {noreply, setstats(NewState)}; {error, _Error} -> {noreply, State} end; handle_cast({unsubscribe, Topic, Subscriber}, State) -> - case do_unsubscribe_(Topic, Subscriber, State) of + case do_unsubscribe(Topic, Subscriber, State) of {ok, NewState} -> {noreply, setstats(NewState), hibernate}; {error, _Error} -> {noreply, State} end; @@ -277,7 +245,7 @@ handle_cast(Msg, State) -> handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{submon = PM}) -> lists:foreach(fun({_, Topic}) -> - subscriber_down_(DownPid, Topic) + subscriber_down(DownPid, Topic) end, ets:lookup(subscription, DownPid)), ets:delete(subscription, DownPid), {noreply, setstats(State#state{submon = PM:erase(DownPid)}), hibernate}; @@ -295,35 +263,24 @@ code_change(_OldVsn, State, _Extra) -> %% Internal Functions %%-------------------------------------------------------------------- -do_subscribe_(Topic, Subscriber, Options, State) -> +do_subscribe(Topic, Subscriber, Options, State) -> case ets:lookup(subproperty, {Topic, Subscriber}) of [] -> - do_subscribe2_(Topic, Subscriber, Options), + add_subscription(Subscriber, Topic), + emqttd_dispatcher:async_add_subscriber(Topic, Subscriber), ets:insert(subproperty, {{Topic, Subscriber}, Options}), {ok, monitor_subpid(Subscriber, State)}; [_] -> {error, {already_subscribed, Topic}} end. -do_subscribe2_(Topic, Subscriber, _Options) -> - add_subscription_(Subscriber, Topic), - add_subscriber_(Topic, Subscriber). - -add_subscription_(Subscriber, Topic) -> +add_subscription(Subscriber, Topic) -> ets:insert(subscription, {Subscriber, Topic}). -add_subscriber_(Topic, Subscriber) -> - %%TODO: LOCK here... - case ets:member(subscriber, Topic) of - false -> emqttd_router:add_route(Topic, node()); - true -> ok - end, - ets:insert(subscriber, {Topic, Subscriber}). - -do_unsubscribe_(Topic, Subscriber, State) -> +do_unsubscribe(Topic, Subscriber, State) -> case ets:lookup(subproperty, {Topic, Subscriber}) of [_] -> - del_subscriber_(Topic, Subscriber), + emqttd_dispatcher:async_del_subscriber(Topic, Subscriber), del_subscription(Subscriber, Topic), ets:delete(subproperty, {Topic, Subscriber}), {ok, case ets:member(subscription, Subscriber) of @@ -337,18 +294,10 @@ do_unsubscribe_(Topic, Subscriber, State) -> del_subscription(Subscriber, Topic) -> ets:delete_object(subscription, {Subscriber, Topic}). -del_subscriber_(Topic, Subscriber) -> - ets:delete_object(subscriber, {Topic, Subscriber}), - %%TODO: LOCK TOPIC - case ets:member(subscriber, Topic) of - false -> emqttd_router:del_route(Topic, node()); - true -> ok - end. - -subscriber_down_(DownPid, Topic) -> +subscriber_down(DownPid, Topic) -> case ets:lookup(subproperty, {Topic, DownPid}) of - [] -> del_subscriber_(Topic, DownPid); %%TODO: warning? - [_] -> del_subscriber_(Topic, DownPid), + [] -> emqttd_dispatcher:async_del_subscriber(Topic, DownPid); %% warning??? + [_] -> emqttd_dispatcher:async_del_subscriber(Topic, DownPid), ets:delete(subproperty, {Topic, DownPid}) end. @@ -363,11 +312,7 @@ demonitor_subpid(_SubPid, State) -> State. setstats(State) when is_record(State, state) -> - setstats(subscriber), setstats(subscription), State; - -setstats(subscriber) -> - emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(subscriber, size)); - -setstats(subscription) -> - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', ets:info(subscription, size)). + emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(subscriber, size)), + emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', ets:info(subscription, size)), + State. diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 414868fbf..845aea56e 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -25,6 +25,8 @@ %% Supervisor callbacks -export([init/1]). +-define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). + start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, [emqttd_conf:pubsub()]). @@ -32,7 +34,11 @@ pubsub_pool() -> hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). init([Env]) -> - {ok, PubSub} = emqttd:conf(pubsub_adapter), PubSub:init_tabs(), + %% Create ETS Tables + [create_tab(Tab) || Tab <- [subscriber, subscription, subproperty]], + + %% PubSub Pool + {ok, PubSub} = emqttd:conf(pubsub_adapter), PoolArgs = [pubsub, hash, pool_size(Env), {PubSub, start_link, [Env]}], PoolSup = emqttd_pool_sup:spec(pubsub_pool, PoolArgs), {ok, { {one_for_all, 10, 3600}, [PoolSup]} }. @@ -41,3 +47,24 @@ pool_size(Env) -> Schedulers = erlang:system_info(schedulers), proplists:get_value(pool_size, Env, Schedulers). +%%-------------------------------------------------------------------- +%% Create PubSub Tables +%%-------------------------------------------------------------------- + +create_tab(subscriber) -> + %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN + %% duplicate_bag: o(1) insert + ensure_tab(subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); + +create_tab(subscription) -> + %% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN + %% bag: o(n) insert + ensure_tab(subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]); + +create_tab(subproperty) -> + %% Subproperty: {Topic, Sub} -> [{qos, 1}] + ensure_tab(subproperty, [public, named_table, ordered_set | ?CONCURRENCY_OPTS]). + +ensure_tab(Tab, Opts) -> + case ets:info(Tab, name) of undefined -> ets:new(Tab, Opts); _ -> ok end. + diff --git a/src/emqttd_submgr.erl b/src/emqttd_submgr.erl new file mode 100644 index 000000000..1cff03ea9 --- /dev/null +++ b/src/emqttd_submgr.erl @@ -0,0 +1,111 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_submgr). + +-author("Feng Lee "). + +-behaviour(gen_server2). + +-include("emqttd.hrl"). + +-include("emqttd_internal.hrl"). + +%% API Exports +-export([start_link/3, add_subscriber/2, async_add_subscriber/2, + del_subscriber/2, async_del_subscriber/2]). + +%% gen_server. +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {pool, id, env}). + +-spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). +start_link(Pool, Id, Env) -> + gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + +-spec(add_subscriber(binary(), emqttd:subscriber()) -> ok). +add_subscriber(Topic, Subscriber) -> + gen_server2:call(pick(Topic), {add_subscriber, Topic, Subscriber}, infinity). + +-spec(async_add_subscriber(binary(), emqttd:subscriber()) -> ok). +async_add_subscriber(Topic, Subscriber) -> + gen_server2:cast(pick(Topic), {add_subscriber, Topic, Subscriber}). + +-spec(del_subscriber(binary(), emqttd:subscriber()) -> ok). +del_subscriber(Topic, Subscriber) -> + gen_server2:call(pick(Topic), {del_subscriber, Topic, Subscriber}, infinity). + +-spec(async_del_subscriber(binary(), emqttd:subscriber()) -> ok). +async_del_subscriber(Topic, Subscriber) -> + gen_server2:cast(pick(Topic), {del_subscriber, Topic, Subscriber}). + +pick(Topic) -> gproc_pool:pick_worker(dispatcher, Topic). + +init([Pool, Id, Env]) -> + ?GPROC_POOL(join, Pool, Id), + {ok, #state{pool = Pool, id = Id, env = Env}}. + +handle_call({add_subscriber, Topic, Subscriber}, _From, State) -> + add_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_call({del_subscriber, Topic, Subscriber}, _From, State) -> + del_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_call(Req, _From, State) -> + ?UNEXPECTED_REQ(Req, State). + +handle_cast({add_subscriber, Topic, Subscriber}, State) -> + add_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_cast({del_subscriber, Topic, Subscriber}, State) -> + del_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_cast(Msg, State) -> + ?UNEXPECTED_MSG(Msg, State). + +handle_info(Info, State) -> + ?UNEXPECTED_INFO(Info, State). + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + ?GPROC_POOL(leave, Pool, Id). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internel Functions +%%-------------------------------------------------------------------- + +add_subscriber_(Topic, Subscriber) -> + case ets:member(subscriber, Topic) of + false -> emqttd_router:add_route(Topic, node()); + true -> ok + end, + ets:insert(subscriber, {Topic, Subscriber}). + +del_subscriber_(Topic, Subscriber) -> + ets:delete_object(subscriber, {Topic, Subscriber}), + case ets:member(subscriber, Topic) of + false -> emqttd_router:del_route(Topic, node()); + true -> ok + end. + From 33472a7f6e9c882876d68270c8172d32f9a0b26a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 12:11:52 +0800 Subject: [PATCH 052/116] pubsub and dispatcher --- src/emqttd.erl | 4 +- src/emqttd_pubsub.erl | 49 +++-------------- src/emqttd_pubsub_sup.erl | 23 ++++++-- src/emqttd_submgr.erl | 111 -------------------------------------- 4 files changed, 29 insertions(+), 158 deletions(-) delete mode 100644 src/emqttd_submgr.erl diff --git a/src/emqttd.erl b/src/emqttd.erl index 6914d7993..28d7441e2 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -124,11 +124,11 @@ unsubscribe(Topic, Subscriber) -> with_pubsub(fun(PubSub) -> PubSub:unsubscribe(iolist_to_binary(Topic), Subscriber) end). -spec(topics() -> [binary()]). -topics() -> with_pubsub(fun(PubSub) -> PubSub:topics() end). +topics() -> emqttd_router:topics(). -spec(subscribers(iodata()) -> list(subscriber())). subscribers(Topic) -> - with_pubsub(fun(PubSub) -> PubSub:subscribers(iolist_to_binary(Topic)) end). + emqttd_dispatcher:subscribers(Topic). -spec(subscriptions(subscriber()) -> [{binary(), suboption()}]). subscriptions(Subscriber) -> diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 704dbe663..75e2e58d5 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -38,10 +38,7 @@ async_unsubscribe/1, async_unsubscribe/2]). %% Management API. --export([setqos/3, topics/0, subscribers/1, is_subscribed/2, subscriptions/1]). - -%% Route API --export([forward/3, dispatch/2]). +-export([setqos/3, is_subscribed/2, subscriptions/1]). %% Debug API -export([dump/0]). @@ -98,7 +95,7 @@ publish(Topic, Msg) when is_binary(Topic) -> %% Dispatch on the local node route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> - dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); + emqttd_dispatch:dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); %% Forward to other nodes route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> @@ -113,32 +110,7 @@ delivery(Msg) -> #mqtt_delivery{message = Msg, flows = []}. %% @doc Forward message to another node... forward(Node, To, Delivery) -> - rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}. - -%% @doc Dispatch Message to Subscribers --spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). -dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> - case subscribers(Topic) of - [] -> - dropped(Topic), {ok, Delivery}; - [Sub] -> %% optimize? - dispatch(Sub, Topic, Msg), - {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1} | Flows]}}; - Subscribers -> - Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], - lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), - {ok, Delivery#mqtt_delivery{flows = Flows1}} - end. - -dispatch(Pid, Topic, Msg) when is_pid(Pid) -> - Pid ! {dispatch, Topic, Msg}; -dispatch(SubId, Topic, Msg) when is_binary(SubId) -> - emqttd_sm:dispatch(SubId, Topic, Msg). - -topics() -> emqttd_router:topics(). - -subscribers(Topic) -> - try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. + rpc:cast(Node, emqttd_dispatch, dispatch, [To, Delivery]), {ok, Delivery}. subscriptions(Subscriber) -> lists:map(fun({_, Topic}) -> @@ -159,13 +131,6 @@ dump() -> {subscription, ets:tab2list(subscription)}, {subproperty, ets:tab2list(subproperty)}]. -%% @private -%% @doc Ingore $SYS Messages. -dropped(<<"$SYS/", _/binary>>) -> - ok; -dropped(_Topic) -> - emqttd_metrics:inc('messages/dropped'). - %% @doc Unsubscribe -spec(unsubscribe(binary()) -> ok | emqttd:pubsub_error()). unsubscribe(Topic) when is_binary(Topic) -> @@ -267,7 +232,7 @@ do_subscribe(Topic, Subscriber, Options, State) -> case ets:lookup(subproperty, {Topic, Subscriber}) of [] -> add_subscription(Subscriber, Topic), - emqttd_dispatcher:async_add_subscriber(Topic, Subscriber), + emqttd_dispatch:async_subscribe(Topic, Subscriber), ets:insert(subproperty, {{Topic, Subscriber}, Options}), {ok, monitor_subpid(Subscriber, State)}; [_] -> @@ -280,7 +245,7 @@ add_subscription(Subscriber, Topic) -> do_unsubscribe(Topic, Subscriber, State) -> case ets:lookup(subproperty, {Topic, Subscriber}) of [_] -> - emqttd_dispatcher:async_del_subscriber(Topic, Subscriber), + emqttd_dispatch:async_subscribe(Topic, Subscriber), del_subscription(Subscriber, Topic), ets:delete(subproperty, {Topic, Subscriber}), {ok, case ets:member(subscription, Subscriber) of @@ -296,8 +261,8 @@ del_subscription(Subscriber, Topic) -> subscriber_down(DownPid, Topic) -> case ets:lookup(subproperty, {Topic, DownPid}) of - [] -> emqttd_dispatcher:async_del_subscriber(Topic, DownPid); %% warning??? - [_] -> emqttd_dispatcher:async_del_subscriber(Topic, DownPid), + [] -> emqttd_dispatch:async_subscribe(Topic, DownPid); %% warning??? + [_] -> emqttd_dispatch:async_subscribe(Topic, DownPid), ets:delete(subproperty, {Topic, DownPid}) end. diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 845aea56e..4663151ed 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -27,26 +27,43 @@ -define(CONCURRENCY_OPTS, [{read_concurrency, true}, {write_concurrency, true}]). +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, [emqttd_conf:pubsub()]). pubsub_pool() -> hd([Pid || {pubsub_pool, Pid, _, _} <- supervisor:which_children(?MODULE)]). +%%-------------------------------------------------------------------- +%% Supervisor callbacks +%%-------------------------------------------------------------------- + init([Env]) -> %% Create ETS Tables [create_tab(Tab) || Tab <- [subscriber, subscription, subproperty]], + %% Dispatcher Pool + DispatcherMFA = {emqttd_dispatcher, start_link, [Env]}, + DispatcherPool = pool_sup(dispatcher, Env, DispatcherMFA), + %% PubSub Pool {ok, PubSub} = emqttd:conf(pubsub_adapter), - PoolArgs = [pubsub, hash, pool_size(Env), {PubSub, start_link, [Env]}], - PoolSup = emqttd_pool_sup:spec(pubsub_pool, PoolArgs), - {ok, { {one_for_all, 10, 3600}, [PoolSup]} }. + PubSubMFA = {PubSub, start_link, [Env]}, + PubSubPool = pool_sup(pubsub, Env, PubSubMFA), + + {ok, { {one_for_all, 10, 3600}, [DispatcherPool, PubSubPool]} }. pool_size(Env) -> Schedulers = erlang:system_info(schedulers), proplists:get_value(pool_size, Env, Schedulers). +pool_sup(Name, Env, MFA) -> + Pool = list_to_atom(atom_to_list(Name) ++ "_pool"), + emqttd_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]). + %%-------------------------------------------------------------------- %% Create PubSub Tables %%-------------------------------------------------------------------- diff --git a/src/emqttd_submgr.erl b/src/emqttd_submgr.erl deleted file mode 100644 index 1cff03ea9..000000000 --- a/src/emqttd_submgr.erl +++ /dev/null @@ -1,111 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_submgr). - --author("Feng Lee "). - --behaviour(gen_server2). - --include("emqttd.hrl"). - --include("emqttd_internal.hrl"). - -%% API Exports --export([start_link/3, add_subscriber/2, async_add_subscriber/2, - del_subscriber/2, async_del_subscriber/2]). - -%% gen_server. --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id, env}). - --spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). -start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). - --spec(add_subscriber(binary(), emqttd:subscriber()) -> ok). -add_subscriber(Topic, Subscriber) -> - gen_server2:call(pick(Topic), {add_subscriber, Topic, Subscriber}, infinity). - --spec(async_add_subscriber(binary(), emqttd:subscriber()) -> ok). -async_add_subscriber(Topic, Subscriber) -> - gen_server2:cast(pick(Topic), {add_subscriber, Topic, Subscriber}). - --spec(del_subscriber(binary(), emqttd:subscriber()) -> ok). -del_subscriber(Topic, Subscriber) -> - gen_server2:call(pick(Topic), {del_subscriber, Topic, Subscriber}, infinity). - --spec(async_del_subscriber(binary(), emqttd:subscriber()) -> ok). -async_del_subscriber(Topic, Subscriber) -> - gen_server2:cast(pick(Topic), {del_subscriber, Topic, Subscriber}). - -pick(Topic) -> gproc_pool:pick_worker(dispatcher, Topic). - -init([Pool, Id, Env]) -> - ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env}}. - -handle_call({add_subscriber, Topic, Subscriber}, _From, State) -> - add_subscriber_(Topic, Subscriber), - {reply, ok, State}; - -handle_call({del_subscriber, Topic, Subscriber}, _From, State) -> - del_subscriber_(Topic, Subscriber), - {reply, ok, State}; - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast({add_subscriber, Topic, Subscriber}, State) -> - add_subscriber_(Topic, Subscriber), - {reply, ok, State}; - -handle_cast({del_subscriber, Topic, Subscriber}, State) -> - del_subscriber_(Topic, Subscriber), - {reply, ok, State}; - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internel Functions -%%-------------------------------------------------------------------- - -add_subscriber_(Topic, Subscriber) -> - case ets:member(subscriber, Topic) of - false -> emqttd_router:add_route(Topic, node()); - true -> ok - end, - ets:insert(subscriber, {Topic, Subscriber}). - -del_subscriber_(Topic, Subscriber) -> - ets:delete_object(subscriber, {Topic, Subscriber}), - case ets:member(subscriber, Topic) of - false -> emqttd_router:del_route(Topic, node()); - true -> ok - end. - From 078c39232bbd8be827c505a0f135de05c5f81502 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 12:12:25 +0800 Subject: [PATCH 053/116] dispatcher --- src/emqttd_dispatcher.erl | 150 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/emqttd_dispatcher.erl diff --git a/src/emqttd_dispatcher.erl b/src/emqttd_dispatcher.erl new file mode 100644 index 000000000..31bd22f46 --- /dev/null +++ b/src/emqttd_dispatcher.erl @@ -0,0 +1,150 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_dispatcher). + +-author("Feng Lee "). + +-behaviour(gen_server2). + +-include("emqttd.hrl"). + +-include("emqttd_internal.hrl"). + +%% API Exports +-export([start_link/3, subscribe/2, unsubscribe/2, dispatch/2, + async_subscribe/2, async_unsubscribe/2]). + +-export([subscribers/1]). + +%% gen_server. +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {pool, id, env}). + +-spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). +start_link(Pool, Id, Env) -> + gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + +-spec(subscribe(binary(), emqttd:subscriber()) -> ok). +subscribe(Topic, Subscriber) -> + call(pick(Topic), {subscribe, Topic, Subscriber}). + +-spec(async_subscribe(binary(), emqttd:subscriber()) -> ok). +async_subscribe(Topic, Subscriber) -> + cast(pick(Topic), {subscribe, Topic, Subscriber}). + +%% @doc Dispatch Message to Subscribers +-spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). +dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> + case subscribers(Topic) of + [] -> + dropped(Topic), {ok, Delivery}; + [Sub] -> %% optimize? + dispatch(Sub, Topic, Msg), + {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1} | Flows]}}; + Subscribers -> + Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], + lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), + {ok, Delivery#mqtt_delivery{flows = Flows1}} + end. + +dispatch(Pid, Topic, Msg) when is_pid(Pid) -> + Pid ! {dispatch, Topic, Msg}; +dispatch(SubId, Topic, Msg) when is_binary(SubId) -> + emqttd_sm:dispatch(SubId, Topic, Msg). + +subscribers(Topic) -> + try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. + +%% @private +%% @doc Ingore $SYS Messages. +dropped(<<"$SYS/", _/binary>>) -> + ok; +dropped(_Topic) -> + emqttd_metrics:inc('messages/dropped'). + +-spec(unsubscribe(binary(), emqttd:subscriber()) -> ok). +unsubscribe(Topic, Subscriber) -> + call(pick(Topic), {unsubscribe, Topic, Subscriber}). + +-spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). +async_unsubscribe(Topic, Subscriber) -> + cast(pick(Topic), {async_unsubscribe, Topic, Subscriber}). + +call(Server, Req) -> + gen_server2:call(Server, Req, infinity). + +cast(Server, Msg) -> + gen_server2:cast(Server, Msg). + +pick(Topic) -> + gproc_pool:pick_worker(dispatcher, Topic). + +init([Pool, Id, Env]) -> + ?GPROC_POOL(join, Pool, Id), + {ok, #state{pool = Pool, id = Id, env = Env}}. + +handle_call({subscribe, Topic, Subscriber}, _From, State) -> + add_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> + del_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_call(Req, _From, State) -> + ?UNEXPECTED_REQ(Req, State). + +handle_cast({subscribe, Topic, Subscriber}, State) -> + add_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_cast({unsubscribe, Topic, Subscriber}, State) -> + del_subscriber_(Topic, Subscriber), + {reply, ok, State}; + +handle_cast(Msg, State) -> + ?UNEXPECTED_MSG(Msg, State). + +handle_info(Info, State) -> + ?UNEXPECTED_INFO(Info, State). + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + ?GPROC_POOL(leave, Pool, Id). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internel Functions +%%-------------------------------------------------------------------- + +add_subscriber_(Topic, Subscriber) -> + case ets:member(subscriber, Topic) of + false -> emqttd_router:add_route(Topic, node()); + true -> ok + end, + ets:insert(subscriber, {Topic, Subscriber}). + +del_subscriber_(Topic, Subscriber) -> + ets:delete_object(subscriber, {Topic, Subscriber}), + case ets:member(subscriber, Topic) of + false -> emqttd_router:del_route(Topic, node()); + true -> ok + end. + From bba806663987ed181607a60a42cd53014c9c2d06 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 12:18:55 +0800 Subject: [PATCH 054/116] Dispatcher --- src/emqttd_pubsub.erl | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 75e2e58d5..71a9197d2 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -51,6 +51,8 @@ -define(PUBSUB, ?MODULE). +-define(Dispatcher, emqttd_dispatcher). + %% @doc Start a pubsub server -spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). start_link(Pool, Id, Env) -> @@ -95,7 +97,7 @@ publish(Topic, Msg) when is_binary(Topic) -> %% Dispatch on the local node route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> - emqttd_dispatch:dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); + ?Dispatcher:dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); %% Forward to other nodes route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> @@ -110,7 +112,7 @@ delivery(Msg) -> #mqtt_delivery{message = Msg, flows = []}. %% @doc Forward message to another node... forward(Node, To, Delivery) -> - rpc:cast(Node, emqttd_dispatch, dispatch, [To, Delivery]), {ok, Delivery}. + rpc:cast(Node, ?Dispatcher, dispatch, [To, Delivery]), {ok, Delivery}. subscriptions(Subscriber) -> lists:map(fun({_, Topic}) -> @@ -231,8 +233,8 @@ code_change(_OldVsn, State, _Extra) -> do_subscribe(Topic, Subscriber, Options, State) -> case ets:lookup(subproperty, {Topic, Subscriber}) of [] -> + ?Dispatcher:async_subscribe(Topic, Subscriber), add_subscription(Subscriber, Topic), - emqttd_dispatch:async_subscribe(Topic, Subscriber), ets:insert(subproperty, {{Topic, Subscriber}, Options}), {ok, monitor_subpid(Subscriber, State)}; [_] -> @@ -245,7 +247,7 @@ add_subscription(Subscriber, Topic) -> do_unsubscribe(Topic, Subscriber, State) -> case ets:lookup(subproperty, {Topic, Subscriber}) of [_] -> - emqttd_dispatch:async_subscribe(Topic, Subscriber), + ?Dispatcher:async_unsubscribe(Topic, Subscriber), del_subscription(Subscriber, Topic), ets:delete(subproperty, {Topic, Subscriber}), {ok, case ets:member(subscription, Subscriber) of @@ -261,9 +263,12 @@ del_subscription(Subscriber, Topic) -> subscriber_down(DownPid, Topic) -> case ets:lookup(subproperty, {Topic, DownPid}) of - [] -> emqttd_dispatch:async_subscribe(Topic, DownPid); %% warning??? - [_] -> emqttd_dispatch:async_subscribe(Topic, DownPid), - ets:delete(subproperty, {Topic, DownPid}) + [] -> + %% here? + ?Dispatcher:async_unsubscribe(Topic, DownPid); + [_] -> + ?Dispatcher:async_unsubscribe(Topic, DownPid), + ets:delete(subproperty, {Topic, DownPid}) end. monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> From bf8730d12d71741a1271fa0ac52987d66dbde3ac Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 12:26:51 +0800 Subject: [PATCH 055/116] {noreply, State} --- src/emqttd_dispatcher.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqttd_dispatcher.erl b/src/emqttd_dispatcher.erl index 31bd22f46..ddef32347 100644 --- a/src/emqttd_dispatcher.erl +++ b/src/emqttd_dispatcher.erl @@ -112,11 +112,11 @@ handle_call(Req, _From, State) -> handle_cast({subscribe, Topic, Subscriber}, State) -> add_subscriber_(Topic, Subscriber), - {reply, ok, State}; + {noreply, State}; handle_cast({unsubscribe, Topic, Subscriber}, State) -> del_subscriber_(Topic, Subscriber), - {reply, ok, State}; + {noreply, State}; handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). From b92231047cf0a191105c3e5fdbdd4ebca5a3a086 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 14:02:13 +0800 Subject: [PATCH 056/116] subscriber_down/1 --- src/emqttd_pubsub.erl | 22 +++++++++++++++++----- src/emqttd_session.erl | 2 ++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 71a9197d2..6107704c3 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -37,6 +37,8 @@ -export([async_subscribe/1, async_subscribe/2, async_subscribe/3, async_unsubscribe/1, async_unsubscribe/2]). +-export([subscriber_down/1]). + %% Management API. -export([setqos/3, is_subscribed/2, subscriptions/1]). @@ -89,6 +91,9 @@ async_subscribe(Topic, Subscriber) when is_binary(Topic) -> async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> cast(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). +subscriber_down(Subscriber) -> + cast(pick(Subscriber), {down, Subscriber}). + %% @doc Publish message to Topic. -spec(publish(binary(), any()) -> {ok, mqtt_delivery()} | ignore). publish(Topic, Msg) when is_binary(Topic) -> @@ -207,14 +212,15 @@ handle_cast({unsubscribe, Topic, Subscriber}, State) -> {error, _Error} -> {noreply, State} end; +handle_cast({down, Subscriber}, State) -> + subscriber_down_(Subscriber), + {noreply, State}; + handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{submon = PM}) -> - lists:foreach(fun({_, Topic}) -> - subscriber_down(DownPid, Topic) - end, ets:lookup(subscription, DownPid)), - ets:delete(subscription, DownPid), + subscriber_down_(DownPid), {noreply, setstats(State#state{submon = PM:erase(DownPid)}), hibernate}; handle_info(Info, State) -> @@ -261,7 +267,13 @@ do_unsubscribe(Topic, Subscriber, State) -> del_subscription(Subscriber, Topic) -> ets:delete_object(subscription, {Subscriber, Topic}). -subscriber_down(DownPid, Topic) -> +subscriber_down_(Subscriber) -> + lists:foreach(fun({_, Topic}) -> + subscriber_down_(Subscriber, Topic) + end, ets:lookup(subscription, Subscriber)), + ets:delete(subscription, Subscriber). + +subscriber_down_(DownPid, Topic) -> case ets:lookup(subproperty, {Topic, DownPid}) of [] -> %% here? diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 8c2bbf8bc..63feb4617 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -532,6 +532,8 @@ handle_info(Info, Session) -> ?UNEXPECTED_INFO(Info, Session). terminate(_Reason, #session{clean_sess = CleanSess, client_id = ClientId}) -> + %%TODO: ... + emqttd_pubsub:subscriber_down(ClientId), emqttd_sm:unregister_session(CleanSess, ClientId). code_change(_OldVsn, Session, _Extra) -> From 15be2037f3b794d652c3bb1c577c0c9f01d4119e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 14:08:46 +0800 Subject: [PATCH 057/116] async_unsubscribe -> unsubscribe --- src/emqttd_dispatcher.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_dispatcher.erl b/src/emqttd_dispatcher.erl index ddef32347..44e44219a 100644 --- a/src/emqttd_dispatcher.erl +++ b/src/emqttd_dispatcher.erl @@ -84,7 +84,7 @@ unsubscribe(Topic, Subscriber) -> -spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). async_unsubscribe(Topic, Subscriber) -> - cast(pick(Topic), {async_unsubscribe, Topic, Subscriber}). + cast(pick(Topic), {unsubscribe, Topic, Subscriber}). call(Server, Req) -> gen_server2:call(Server, Req, infinity). From 6686139bc62451bfcd88c137d57d5c56386915b4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 15:04:26 +0800 Subject: [PATCH 058/116] mqtt_trie, mqtt_trie_node --- include/emqttd_trie.hrl | 10 +++--- src/emqttd_trie.erl | 68 +++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/include/emqttd_trie.hrl b/include/emqttd_trie.hrl index d077da5fb..e701d90cd 100644 --- a/include/emqttd_trie.hrl +++ b/include/emqttd_trie.hrl @@ -14,7 +14,7 @@ %% limitations under the License. %%-------------------------------------------------------------------- --type trie_node_id() :: binary() | atom(). +-type(trie_node_id() :: binary() | atom()). -record(trie_node, { node_id :: trie_node_id(), @@ -24,12 +24,12 @@ }). -record(trie_edge, { - node_id :: trie_node_id(), - word :: binary() | atom() + node_id :: trie_node_id(), + word :: binary() | atom() }). -record(trie, { - edge :: #trie_edge{}, - node_id :: trie_node_id() + edge :: #trie_edge{}, + node_id :: trie_node_id() }). diff --git a/src/emqttd_trie.erl b/src/emqttd_trie.erl index c0ed8e064..7266ea4b6 100644 --- a/src/emqttd_trie.erl +++ b/src/emqttd_trie.erl @@ -38,21 +38,21 @@ -spec(mnesia(boot | copy) -> ok). mnesia(boot) -> %% Trie Table - ok = emqttd_mnesia:create_table(trie, [ + ok = emqttd_mnesia:create_table(mqtt_trie, [ {ram_copies, [node()]}, {record_name, trie}, {attributes, record_info(fields, trie)}]), %% Trie Node Table - ok = emqttd_mnesia:create_table(trie_node, [ + ok = emqttd_mnesia:create_table(mqtt_trie_node, [ {ram_copies, [node()]}, {record_name, trie_node}, {attributes, record_info(fields, trie_node)}]); mnesia(copy) -> %% Copy Trie Table - ok = emqttd_mnesia:copy_table(trie), + ok = emqttd_mnesia:copy_table(mqtt_trie), %% Copy Trie Node Table - ok = emqttd_mnesia:copy_table(trie_node). + ok = emqttd_mnesia:copy_table(mqtt_trie_node). %%-------------------------------------------------------------------- %% Trie API @@ -61,16 +61,16 @@ mnesia(copy) -> %% @doc Insert topic to trie -spec(insert(Topic :: binary()) -> ok). insert(Topic) when is_binary(Topic) -> - case mnesia:read(trie_node, Topic) of + case mnesia:read(mqtt_trie_node, Topic) of [#trie_node{topic=Topic}] -> ok; [TrieNode=#trie_node{topic=undefined}] -> - mnesia:write(TrieNode#trie_node{topic=Topic}); + write_trie_node(TrieNode#trie_node{topic=Topic}); [] -> - %add trie path + % Add trie path lists:foreach(fun add_path/1, emqttd_topic:triples(Topic)), - %add last node - mnesia:write(#trie_node{node_id=Topic, topic=Topic}) + % Add last node + write_trie_node(#trie_node{node_id=Topic, topic=Topic}) end. %% @doc Find trie nodes that match topic @@ -82,19 +82,19 @@ match(Topic) when is_binary(Topic) -> %% @doc Lookup a Trie Node -spec(lookup(NodeId :: binary()) -> [#trie_node{}]). lookup(NodeId) -> - mnesia:read(trie_node, NodeId). + mnesia:read(mqtt_trie_node, NodeId). %% @doc Delete topic from trie -spec(delete(Topic :: binary()) -> ok). delete(Topic) when is_binary(Topic) -> - case mnesia:read(trie_node, Topic) of - [#trie_node{edge_count=0}] -> - mnesia:delete({trie_node, Topic}), + case mnesia:read(mqtt_trie_node, Topic) of + [#trie_node{edge_count=0}] -> + mnesia:delete({mqtt_trie_node, Topic}), delete_path(lists:reverse(emqttd_topic:triples(Topic))); [TrieNode] -> - mnesia:write(TrieNode#trie_node{topic = undefined}); + write_trie_node(TrieNode#trie_node{topic = undefined}); [] -> - ok + ok end. %%-------------------------------------------------------------------- @@ -105,18 +105,18 @@ delete(Topic) when is_binary(Topic) -> %% @doc Add path to trie tree. add_path({Node, Word, Child}) -> Edge = #trie_edge{node_id=Node, word=Word}, - case mnesia:read(trie_node, Node) of + case mnesia:read(mqtt_trie_node, Node) of [TrieNode = #trie_node{edge_count=Count}] -> - case mnesia:wread({trie, Edge}) of + case mnesia:wread({mqtt_trie, Edge}) of [] -> - mnesia:write(TrieNode#trie_node{edge_count=Count+1}), - mnesia:write(#trie{edge=Edge, node_id=Child}); + write_trie_node(TrieNode#trie_node{edge_count=Count+1}), + write_trie(#trie{edge=Edge, node_id=Child}); [_] -> ok end; [] -> - mnesia:write(#trie_node{node_id=Node, edge_count=1}), - mnesia:write(#trie{edge=Edge, node_id=Child}) + write_trie_node(#trie_node{node_id=Node, edge_count=1}), + write_trie(#trie{edge=Edge, node_id=Child}) end. %% @private @@ -128,11 +128,11 @@ match_node(NodeId, Words) -> match_node(NodeId, Words, []). match_node(NodeId, [], ResAcc) -> - mnesia:read(trie_node, NodeId) ++ 'match_#'(NodeId, ResAcc); + mnesia:read(mqtt_trie_node, NodeId) ++ 'match_#'(NodeId, ResAcc); match_node(NodeId, [W|Words], ResAcc) -> lists:foldl(fun(WArg, Acc) -> - case mnesia:read(trie, #trie_edge{node_id=NodeId, word=WArg}) of + case mnesia:read(mqtt_trie, #trie_edge{node_id=NodeId, word=WArg}) of [#trie{node_id=ChildId}] -> match_node(ChildId, Words, Acc); [] -> Acc end @@ -141,9 +141,9 @@ match_node(NodeId, [W|Words], ResAcc) -> %% @private %% @doc Match node with '#'. 'match_#'(NodeId, ResAcc) -> - case mnesia:read(trie, #trie_edge{node_id=NodeId, word = '#'}) of + case mnesia:read(mqtt_trie, #trie_edge{node_id=NodeId, word = '#'}) of [#trie{node_id=ChildId}] -> - mnesia:read(trie_node, ChildId) ++ ResAcc; + mnesia:read(mqtt_trie_node, ChildId) ++ ResAcc; [] -> ResAcc end. @@ -153,16 +153,24 @@ match_node(NodeId, [W|Words], ResAcc) -> delete_path([]) -> ok; delete_path([{NodeId, Word, _} | RestPath]) -> - mnesia:delete({trie, #trie_edge{node_id=NodeId, word=Word}}), - case mnesia:read(trie_node, NodeId) of + mnesia:delete({mqtt_trie, #trie_edge{node_id=NodeId, word=Word}}), + case mnesia:read(mqtt_trie_node, NodeId) of [#trie_node{edge_count=1, topic=undefined}] -> - mnesia:delete({trie_node, NodeId}), + mnesia:delete({mqtt_trie_node, NodeId}), delete_path(RestPath); [TrieNode=#trie_node{edge_count=1, topic=_}] -> - mnesia:write(TrieNode#trie_node{edge_count=0}); + write_trie_node(TrieNode#trie_node{edge_count=0}); [TrieNode=#trie_node{edge_count=C}] -> - mnesia:write(TrieNode#trie_node{edge_count=C-1}); + write_trie_node(TrieNode#trie_node{edge_count=C-1}); [] -> throw({notfound, NodeId}) end. +%% @private +write_trie(Trie) -> + mnesia:write(mqtt_trie, Trie, write). + +%% @private +write_trie_node(TrieNode) -> + mnesia:write(mqtt_trie_node, TrieNode, write). + From 162b7ec229ff0c585b9ec2864131db50c1224605 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 15:41:10 +0800 Subject: [PATCH 059/116] mqtt_pubsub, mqtt_subscription, mqtt_subscriber --- src/emqttd_pubsub.erl | 40 +++++++++++++++++++-------------------- src/emqttd_pubsub_sup.erl | 18 +++++++++--------- src/emqttd_router.erl | 36 +++++++++++++++++------------------ 3 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 6107704c3..9c9524ee6 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -122,21 +122,19 @@ forward(Node, To, Delivery) -> subscriptions(Subscriber) -> lists:map(fun({_, Topic}) -> subscription(Topic, Subscriber) - end, ets:lookup(subscription, Subscriber)). + end, ets:lookup(mqtt_subscription, Subscriber)). subscription(Topic, Subscriber) -> - {Topic, ets:lookup_element(subproperty, {Topic, Subscriber}, 2)}. + {Topic, ets:lookup_element(mqtt_pubsub, {Topic, Subscriber}, 2)}. is_subscribed(Topic, Subscriber) when is_binary(Topic) -> - ets:member(subproperty, {Topic, Subscriber}). + ets:member(mqtt_pubsub, {Topic, Subscriber}). setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> call(pick(Subscriber), {setqos, Topic, Subscriber, Qos}). dump() -> - [{subscriber, ets:tab2list(subscriber)}, - {subscription, ets:tab2list(subscription)}, - {subproperty, ets:tab2list(subproperty)}]. + [{Tab, ets:tab2list(Tab)} || Tab <- [mqtt_pubsub, mqtt_subscription, mqtt_subscriber]]. %% @doc Unsubscribe -spec(unsubscribe(binary()) -> ok | emqttd:pubsub_error()). @@ -188,10 +186,10 @@ handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> handle_call({setqos, Topic, Subscriber, Qos}, _From, State) -> Key = {Topic, Subscriber}, - case ets:lookup(subproperty, Key) of + case ets:lookup(mqtt_pubsub, Key) of [{_, Opts}] -> Opts1 = lists:ukeymerge(1, [{qos, Qos}], Opts), - ets:insert(subproperty, {Key, Opts1}), + ets:insert(mqtt_pubsub, {Key, Opts1}), {reply, ok, State}; [] -> {reply, {error, {subscription_not_found, Topic}}, State} @@ -237,26 +235,26 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- do_subscribe(Topic, Subscriber, Options, State) -> - case ets:lookup(subproperty, {Topic, Subscriber}) of + case ets:lookup(mqtt_pubsub, {Topic, Subscriber}) of [] -> ?Dispatcher:async_subscribe(Topic, Subscriber), add_subscription(Subscriber, Topic), - ets:insert(subproperty, {{Topic, Subscriber}, Options}), + ets:insert(mqtt_pubsub, {{Topic, Subscriber}, Options}), {ok, monitor_subpid(Subscriber, State)}; [_] -> {error, {already_subscribed, Topic}} end. add_subscription(Subscriber, Topic) -> - ets:insert(subscription, {Subscriber, Topic}). + ets:insert(mqtt_subscription, {Subscriber, Topic}). do_unsubscribe(Topic, Subscriber, State) -> - case ets:lookup(subproperty, {Topic, Subscriber}) of + case ets:lookup(mqtt_pubsub, {Topic, Subscriber}) of [_] -> ?Dispatcher:async_unsubscribe(Topic, Subscriber), del_subscription(Subscriber, Topic), - ets:delete(subproperty, {Topic, Subscriber}), - {ok, case ets:member(subscription, Subscriber) of + ets:delete(mqtt_pubsub, {Topic, Subscriber}), + {ok, case ets:member(mqtt_subscription, Subscriber) of true -> State; false -> demonitor_subpid(Subscriber, State) end}; @@ -265,22 +263,22 @@ do_unsubscribe(Topic, Subscriber, State) -> end. del_subscription(Subscriber, Topic) -> - ets:delete_object(subscription, {Subscriber, Topic}). + ets:delete_object(mqtt_subscription, {Subscriber, Topic}). subscriber_down_(Subscriber) -> lists:foreach(fun({_, Topic}) -> subscriber_down_(Subscriber, Topic) - end, ets:lookup(subscription, Subscriber)), - ets:delete(subscription, Subscriber). + end, ets:lookup(mqtt_subscription, Subscriber)), + ets:delete(mqtt_subscription, Subscriber). subscriber_down_(DownPid, Topic) -> - case ets:lookup(subproperty, {Topic, DownPid}) of + case ets:lookup(mqtt_pubsub, {Topic, DownPid}) of [] -> %% here? ?Dispatcher:async_unsubscribe(Topic, DownPid); [_] -> ?Dispatcher:async_unsubscribe(Topic, DownPid), - ets:delete(subproperty, {Topic, DownPid}) + ets:delete(mqtt_pubsub, {Topic, DownPid}) end. monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> @@ -294,7 +292,7 @@ demonitor_subpid(_SubPid, State) -> State. setstats(State) when is_record(State, state) -> - emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(subscriber, size)), - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', ets:info(subscription, size)), + emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(mqtt_subscriber, size)), + emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', ets:info(mqtt_subscription, size)), State. diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 4663151ed..9b794dcfd 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -43,7 +43,7 @@ pubsub_pool() -> init([Env]) -> %% Create ETS Tables - [create_tab(Tab) || Tab <- [subscriber, subscription, subproperty]], + [create_tab(Tab) || Tab <- [mqtt_pubsub, mqtt_subscriber, mqtt_subscription]], %% Dispatcher Pool DispatcherMFA = {emqttd_dispatcher, start_link, [Env]}, @@ -68,19 +68,19 @@ pool_sup(Name, Env, MFA) -> %% Create PubSub Tables %%-------------------------------------------------------------------- -create_tab(subscriber) -> +create_tab(mqtt_pubsub) -> + %% Subproperty: {Topic, Sub} -> [{qos, 1}] + ensure_tab(mqtt_pubsub, [public, named_table, set | ?CONCURRENCY_OPTS]); + +create_tab(mqtt_subscriber) -> %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN %% duplicate_bag: o(1) insert - ensure_tab(subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); + ensure_tab(mqtt_subscriber, [public, named_table, duplicate_bag | ?CONCURRENCY_OPTS]); -create_tab(subscription) -> +create_tab(mqtt_subscription) -> %% Subscription: Sub -> Topic1, Topic2, Topic3, ..., TopicN %% bag: o(n) insert - ensure_tab(subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]); - -create_tab(subproperty) -> - %% Subproperty: {Topic, Sub} -> [{qos, 1}] - ensure_tab(subproperty, [public, named_table, ordered_set | ?CONCURRENCY_OPTS]). + ensure_tab(mqtt_subscription, [public, named_table, bag | ?CONCURRENCY_OPTS]). ensure_tab(Tab, Opts) -> case ets:info(Tab, name) of undefined -> ets:new(Tab, Opts); _ -> ok end. diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl index d946e945e..d77a86488 100644 --- a/src/emqttd_router.erl +++ b/src/emqttd_router.erl @@ -53,14 +53,14 @@ %%-------------------------------------------------------------------- mnesia(boot) -> - ok = emqttd_mnesia:create_table(route, [ + ok = emqttd_mnesia:create_table(mqtt_route, [ {type, bag}, {ram_copies, [node()]}, {record_name, mqtt_route}, {attributes, record_info(fields, mqtt_route)}]); mnesia(copy) -> - ok = emqttd_mnesia:copy_table(route, ram_copies). + ok = emqttd_mnesia:copy_table(mqtt_route, ram_copies). %%-------------------------------------------------------------------- %% Start the Router @@ -74,17 +74,17 @@ start_link() -> %%-------------------------------------------------------------------- topics() -> - mnesia:dirty_all_keys(route). + mnesia:dirty_all_keys(mqtt_route). local_topics() -> - ets:select(local_route, [{{'$1', '_'}, [], ['$1']}]). + ets:select(mqtt_local_route, [{{'$1', '_'}, [], ['$1']}]). %% @doc Match Routes. -spec(match(Topic:: binary()) -> [mqtt_route()]). match(Topic) when is_binary(Topic) -> Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [Topic]), %% Optimize: route table will be replicated to all nodes. - lists:append([ets:lookup(route, To) || To <- [Topic | Matched]]). + lists:append([ets:lookup(mqtt_route, To) || To <- [Topic | Matched]]). %% @doc Print Routes. -spec(print(Topic :: binary()) -> [ok]). @@ -114,17 +114,17 @@ add_routes(Routes) -> %% @private add_route_(Route = #mqtt_route{topic = Topic}) -> - case mnesia:wread({route, Topic}) of + case mnesia:wread({mqtt_route, Topic}) of [] -> case emqttd_topic:wildcard(Topic) of true -> emqttd_trie:insert(Topic); false -> ok end, - mnesia:write(route, Route, write); + mnesia:write(Route); Records -> case lists:member(Route, Records) of true -> ok; - false -> mnesia:write(route, Route, write) + false -> mnesia:write(Route) end end. @@ -149,27 +149,27 @@ del_routes(Routes) -> end. del_route_(Route = #mqtt_route{topic = Topic}) -> - case mnesia:wread({route, Topic}) of + case mnesia:wread({mqtt_route, Topic}) of [] -> ok; [Route] -> %% Remove route and trie - mnesia:delete_object(route, Route, write), + mnesia:delete_object(Route), case emqttd_topic:wildcard(Topic) of true -> emqttd_trie:delete(Topic); false -> ok end; _More -> %% Remove route only - mnesia:delete_object(route, Route, write) + mnesia:delete_object(Route) end. %% @doc Has Route? -spec(has_route(binary()) -> boolean()). has_route(Topic) -> Routes = case mnesia:is_transaction() of - true -> mnesia:read(route, Topic); - false -> mnesia:dirty_read(route, Topic) + true -> mnesia:read(mqtt_route, Topic); + false -> mnesia:dirty_read(mqtt_route, Topic) end, length(Routes) > 0. @@ -196,11 +196,11 @@ del_local_route(Topic) -> -spec(match_local(binary()) -> [mqtt_route()]). match_local(Name) -> [#mqtt_route{topic = {local, Filter}, node = Node} - || {Filter, Node} <- ets:tab2list(local_route), + || {Filter, Node} <- ets:tab2list(mqtt_local_route), emqttd_topic:match(Name, Filter)]. dump() -> - [{route, ets:tab2list(route)}, {local_route, ets:tab2list(local_route)}]. + [{route, ets:tab2list(mqtt_route)}, {local_route, ets:tab2list(mqtt_local_route)}]. stop() -> gen_server:call(?ROUTER, stop). @@ -209,8 +209,8 @@ stop() -> gen_server:call(?ROUTER, stop). %%-------------------------------------------------------------------- init([]) -> - ets:new(local_route, [set, named_table, protected]), mnesia:subscribe(system), + ets:new(mqtt_local_route, [set, named_table, protected]), {ok, TRef} = timer:send_interval(timer:seconds(1), stats), {ok, #state{stats_timer = TRef}}. @@ -222,11 +222,11 @@ handle_call(_Req, _From, State) -> handle_cast({add_local_route, Topic}, State) -> %% why node()...? - ets:insert(local_route, {Topic, node()}), + ets:insert(mqtt_local_route, {Topic, node()}), {noreply, State}; handle_cast({del_local_route, Topic}, State) -> - ets:delete(local_route, Topic), + ets:delete(mqtt_local_route, Topic), {noreply, State}; handle_cast(_Msg, State) -> From 766f74692d7edb38bdad5dbdade0c404bae7a5cd Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 16:36:09 +0800 Subject: [PATCH 060/116] mqtt_local_session --- src/emqttd_cm.erl | 44 ++++++++++++++----------------------- src/emqttd_sm.erl | 47 ++++++++++++++-------------------------- src/emqttd_sm_helper.erl | 2 +- src/emqttd_sm_sup.erl | 9 +------- 4 files changed, 34 insertions(+), 68 deletions(-) diff --git a/src/emqttd_cm.erl b/src/emqttd_cm.erl index f78db2ad0..da98dedd6 100644 --- a/src/emqttd_cm.erl +++ b/src/emqttd_cm.erl @@ -26,7 +26,7 @@ %% API Exports -export([start_link/3]). --export([lookup/1, lookup_proc/1, register/1, unregister/1]). +-export([lookup/1, lookup_proc/1, reg/1, unreg/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -44,23 +44,17 @@ %%-------------------------------------------------------------------- %% @doc Start Client Manager --spec(start_link(Pool, Id, StatsFun) -> {ok, pid()} | ignore | {error, any()} when - Pool :: atom(), - Id :: pos_integer(), - StatsFun :: fun()). +-spec(start_link(atom(), pos_integer(), fun()) -> {ok, pid()} | ignore | {error, any()}). start_link(Pool, Id, StatsFun) -> gen_server2:start_link(?MODULE, [Pool, Id, StatsFun], []). %% @doc Lookup Client by ClientId --spec(lookup(ClientId :: binary()) -> mqtt_client() | undefined). +-spec(lookup(binary()) -> mqtt_client() | undefined). lookup(ClientId) when is_binary(ClientId) -> - case ets:lookup(mqtt_client, ClientId) of - [Client] -> Client; - [] -> undefined - end. + case ets:lookup(mqtt_client, ClientId) of [Client] -> Client; [] -> undefined end. %% @doc Lookup client pid by clientId --spec(lookup_proc(ClientId :: binary()) -> pid() | undefined). +-spec(lookup_proc(binary()) -> pid() | undefined). lookup_proc(ClientId) when is_binary(ClientId) -> try ets:lookup_element(mqtt_client, ClientId, #mqtt_client.client_pid) catch @@ -68,14 +62,14 @@ lookup_proc(ClientId) when is_binary(ClientId) -> end. %% @doc Register ClientId with Pid. --spec(register(Client :: mqtt_client()) -> ok). -register(Client = #mqtt_client{client_id = ClientId}) -> - gen_server2:call(pick(ClientId), {register, Client}, 120000). +-spec(reg(mqtt_client()) -> ok). +reg(Client = #mqtt_client{client_id = ClientId}) -> + gen_server2:call(pick(ClientId), {reg, Client}, 120000). %% @doc Unregister clientId with pid. --spec(unregister(ClientId :: binary()) -> ok). -unregister(ClientId) when is_binary(ClientId) -> - gen_server2:cast(pick(ClientId), {unregister, ClientId, self()}). +-spec(unreg(binary()) -> ok). +unreg(ClientId) when is_binary(ClientId) -> + gen_server2:cast(pick(ClientId), {unreg, ClientId, self()}). pick(ClientId) -> gproc_pool:pick_worker(?POOL, ClientId). @@ -88,22 +82,16 @@ init([Pool, Id, StatsFun]) -> {ok, #state{pool = Pool, id = Id, statsfun = StatsFun, monitors = dict:new()}}. prioritise_call(Req, _From, _Len, _State) -> - case Req of - {register, _Client} -> 2; - _ -> 1 - end. + case Req of {reg, _Client} -> 2; _ -> 1 end. prioritise_cast(Msg, _Len, _State) -> - case Msg of - {unregister, _ClientId, _Pid} -> 9; - _ -> 1 - end. + case Msg of {unreg, _ClientId, _Pid} -> 9; _ -> 1 end. prioritise_info(_Msg, _Len, _State) -> 3. -handle_call({register, Client = #mqtt_client{client_id = ClientId, - client_pid = Pid}}, _From, State) -> +handle_call({reg, Client = #mqtt_client{client_id = ClientId, + client_pid = Pid}}, _From, State) -> case lookup_proc(ClientId) of Pid -> {reply, ok, State}; @@ -115,7 +103,7 @@ handle_call({register, Client = #mqtt_client{client_id = ClientId, handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({unregister, ClientId, Pid}, State) -> +handle_cast({unreg, ClientId, Pid}, State) -> case lookup_proc(ClientId) of Pid -> ets:delete(mqtt_client, ClientId), diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index 3ab36186f..de23d3702 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -32,9 +32,7 @@ %% API Function Exports -export([start_link/2]). --export([start_session/2, lookup_session/1]). - --export([register_session/3, unregister_session/2]). +-export([start_session/2, lookup_session/1, register_session/3, unregister_session/1]). -export([dispatch/3]). @@ -60,14 +58,14 @@ mnesia(boot) -> %% Global Session Table - ok = emqttd_mnesia:create_table(session, [ + ok = emqttd_mnesia:create_table(mqtt_session, [ {type, set}, {ram_copies, [node()]}, {record_name, mqtt_session}, {attributes, record_info(fields, mqtt_session)}]); mnesia(copy) -> - ok = emqttd_mnesia:copy_table(session). + ok = emqttd_mnesia:copy_table(mqtt_session). %%-------------------------------------------------------------------- %% API @@ -93,32 +91,22 @@ lookup_session(ClientId) -> end. %% @doc Register a session with info. --spec(register_session(CleanSess, ClientId, Info) -> ok when - CleanSess :: boolean(), - ClientId :: binary(), - Info :: [tuple()]). -register_session(CleanSess, ClientId, Info) -> - ets:insert(sesstab(CleanSess), {{ClientId, self()}, Info}). +-spec(register_session(boolean(), binary(), [tuple()]) -> true). +register_session(CleanSess, ClientId, Properties) -> + ets:insert(mqtt_local_session, {ClientId, self(), CleanSess, Properties}). %% @doc Unregister a session. --spec(unregister_session(CleanSess, ClientId) -> ok when - CleanSess :: boolean(), - ClientId :: binary()). -unregister_session(CleanSess, ClientId) -> - ets:delete(sesstab(CleanSess), {ClientId, self()}). +-spec(unregister_session(binary()) -> true). +unregister_session(ClientId) -> + ets:delete(mqtt_local_session, ClientId). -%%TODO: FIXME... -dispatch(Id, Topic, Msg) -> - case lookup_session(Id) of - #mqtt_session{sess_pid = Pid} -> - Pid ! {dispatch, Topic, Msg}; - undefined -> - ok +dispatch(ClientId, Topic, Msg) -> + try ets:lookup_element(mqtt_local_session, ClientId, 2) of + Pid -> Pid ! {dispatch, Topic, Msg} + catch + error:badarg -> ok %%TODO: How?? end. -sesstab(true) -> mqtt_transient_session; -sesstab(false) -> mqtt_persistent_session. - call(SM, Req) -> gen_server2:call(SM, Req, ?TIMEOUT). %%infinity). @@ -217,9 +205,7 @@ create_session({CleanSess, ClientId, ClientPid}, State) -> create_session(CleanSess, ClientId, ClientPid) -> case emqttd_session_sup:start_session(CleanSess, ClientId, ClientPid) of {ok, SessPid} -> - Session = #mqtt_session{client_id = ClientId, - sess_pid = SessPid, - persistent = not CleanSess}, + Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid, persistent = not CleanSess}, case insert_session(Session) of {aborted, {conflict, ConflictPid}} -> %% Conflict with othe node? @@ -244,8 +230,7 @@ insert_session(Session = #mqtt_session{client_id = ClientId}) -> end). %% Local node -resume_session(Session = #mqtt_session{client_id = ClientId, - sess_pid = SessPid}, ClientPid) +resume_session(Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid}, ClientPid) when node(SessPid) =:= node() -> case is_process_alive(SessPid) of diff --git a/src/emqttd_sm_helper.erl b/src/emqttd_sm_helper.erl index 1c90acc32..184f2be64 100644 --- a/src/emqttd_sm_helper.erl +++ b/src/emqttd_sm_helper.erl @@ -83,5 +83,5 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- setstats(State = #state{stats_fun = StatsFun}) -> - StatsFun(ets:info(mqtt_persistent_session, size)), State. + StatsFun(ets:info(mqtt_local_session, size)), State. diff --git a/src/emqttd_sm_sup.erl b/src/emqttd_sm_sup.erl index 556d9540f..1935bcfde 100644 --- a/src/emqttd_sm_sup.erl +++ b/src/emqttd_sm_sup.erl @@ -25,8 +25,6 @@ -define(HELPER, emqttd_sm_helper). --define(TABS, [mqtt_transient_session, mqtt_persistent_session]). - %% API -export([start_link/0]). @@ -38,7 +36,7 @@ start_link() -> init([]) -> %% Create session tables - create_session_tabs(), + ets:new(mqtt_local_session, [public, ordered_set, named_table, {write_concurrency, true}]), %% Helper StatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), @@ -50,9 +48,4 @@ init([]) -> PoolSup = emqttd_pool_sup:spec([?SM, hash, erlang:system_info(schedulers), MFA]), {ok, {{one_for_all, 10, 3600}, [Helper, PoolSup]}}. - -create_session_tabs() -> - Opts = [ordered_set, named_table, public, - {write_concurrency, true}], - [ets:new(Tab, Opts) || Tab <- ?TABS]. From 91e96e738d2f544f344ba9ade7bfa61b32ff642c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 16:40:36 +0800 Subject: [PATCH 061/116] emqttd_cm:reg/1, emqttd_cm:unreg/1 --- src/emqttd_protocol.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 63a6ec821..ec6de539f 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -149,7 +149,7 @@ process(Packet = ?CONNECT_PACKET(Var), State0) -> case emqttd_sm:start_session(CleanSess, clientid(State2)) of {ok, Session, SP} -> %% Register the client - emqttd_cm:register(client(State2)), + emqttd_cm:reg(client(State2)), %% Start keepalive start_keepalive(KeepAlive), %% ACCEPT @@ -277,7 +277,7 @@ shutdown(_Error, #proto_state{client_id = undefined}) -> shutdown(conflict, #proto_state{client_id = _ClientId}) -> %% let it down - %% emqttd_cm:unregister(ClientId); + %% emqttd_cm:unreg(ClientId); ignore; shutdown(Error, State = #proto_state{client_id = ClientId, will_msg = WillMsg}) -> @@ -285,7 +285,7 @@ shutdown(Error, State = #proto_state{client_id = ClientId, will_msg = WillMsg}) send_willmsg(ClientId, WillMsg), emqttd:run_hooks('client.disconnected', [Error], ClientId), %% let it down - %% emqttd_cm:unregister(ClientId). + %% emqttd_cm:unreg(ClientId). ok. willmsg(Packet) when is_record(Packet, mqtt_packet_connect) -> From 38daaa2f5ca22afe12ab400929c848580656131a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 21:29:55 +0800 Subject: [PATCH 062/116] server -> pubsub -> router --- include/emqttd.hrl | 9 +- src/emqttd.erl | 40 ++---- src/emqttd_dispatcher.erl | 150 -------------------- src/emqttd_pubsub.erl | 276 +++++++++++------------------------- src/emqttd_pubsub_sup.erl | 19 ++- src/emqttd_server.erl | 286 ++++++++++++++++++++++++++++++++++++++ src/emqttd_session.erl | 16 +-- src/emqttd_sm.erl | 4 +- 8 files changed, 397 insertions(+), 403 deletions(-) delete mode 100644 src/emqttd_dispatcher.erl create mode 100644 src/emqttd_server.erl diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 6142407d1..edd403f9b 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -70,7 +70,7 @@ %% MQTT Subscription %%-------------------------------------------------------------------- -record(mqtt_subscription, { - subid :: binary() | atom(), + subid :: binary() | atom() | pid(), topic :: binary(), qos = 0 :: 0 | 1 | 2 }). @@ -119,10 +119,11 @@ %%-------------------------------------------------------------------- %% MQTT Session %%-------------------------------------------------------------------- + -record(mqtt_session, { - client_id :: binary(), - sess_pid :: pid(), - persistent :: boolean() + client_id :: binary(), + sess_pid :: pid(), + persistent :: boolean() }). -type(mqtt_session() :: #mqtt_session{}). diff --git a/src/emqttd.erl b/src/emqttd.erl index 28d7441e2..a741adc59 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -95,24 +95,12 @@ subscribe(Topic, Subscriber) -> -spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | pubsub_error()). subscribe(Topic, Subscriber, Options) -> - with_pubsub(fun(PubSub) -> PubSub:subscribe(iolist_to_binary(Topic), Subscriber, Options) end). + emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber, Options). %% @doc Publish MQTT Message -spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Msg = #mqtt_message{from = From}) -> - trace(publish, From, Msg), - case run_hooks('message.publish', [], Msg) of - {ok, Msg1 = #mqtt_message{topic = Topic}} -> - %% Retain message first. Don't create retained topic. - Msg2 = case emqttd_retainer:retain(Msg1) of - ok -> emqttd_message:unset_flag(Msg1); - ignore -> Msg1 - end, - with_pubsub(fun(PubSub) -> PubSub:publish(Topic, Msg2) end); - {stop, Msg1} -> - lager:warning("Stop publishing: ~s", [emqttd_message:format(Msg1)]), - ignore - end. +publish(Msg = #mqtt_message{topic = Topic}) -> + emqttd_server:publish(Topic, Msg). %% @doc Unsubscribe -spec(unsubscribe(iodata()) -> ok | pubsub_error()). @@ -121,22 +109,18 @@ unsubscribe(Topic) -> -spec(unsubscribe(iodata(), subscriber()) -> ok | pubsub_error()). unsubscribe(Topic, Subscriber) -> - with_pubsub(fun(PubSub) -> PubSub:unsubscribe(iolist_to_binary(Topic), Subscriber) end). + emqttd_server:unsubscribe(iolist_to_binary(Topic), Subscriber). -spec(topics() -> [binary()]). topics() -> emqttd_router:topics(). -spec(subscribers(iodata()) -> list(subscriber())). subscribers(Topic) -> - emqttd_dispatcher:subscribers(Topic). + emqttd_pubsub:subscribers(iolist_to_binary(Topic)). -spec(subscriptions(subscriber()) -> [{binary(), suboption()}]). subscriptions(Subscriber) -> - with_pubsub(fun(PubSub) -> PubSub:subscriptions(Subscriber) end). - -with_pubsub(Fun) -> {ok, PubSub} = conf(pubsub_adapter), Fun(PubSub). - -dump() -> with_pubsub(fun(PubSub) -> lists:append(PubSub:dump(), emqttd_router:dump()) end). + emqttd_server:get_subscriptions(Subscriber). %%-------------------------------------------------------------------- %% Hooks API @@ -158,15 +142,9 @@ unhook(Hook, Function) -> run_hooks(Hook, Args, Acc) -> emqttd_hook:run(Hook, Args, Acc). + %%-------------------------------------------------------------------- -%% Trace Functions +%% Debug %%-------------------------------------------------------------------- -trace(publish, From, _Msg) when is_atom(From) -> - %% Dont' trace '$SYS' publish - ignore; - -trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> - lager:info([{client, From}, {topic, Topic}], - "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). - +dump() -> lists:append([emqttd_server:dump(), emqttd_router:dump()]). diff --git a/src/emqttd_dispatcher.erl b/src/emqttd_dispatcher.erl deleted file mode 100644 index 44e44219a..000000000 --- a/src/emqttd_dispatcher.erl +++ /dev/null @@ -1,150 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_dispatcher). - --author("Feng Lee "). - --behaviour(gen_server2). - --include("emqttd.hrl"). - --include("emqttd_internal.hrl"). - -%% API Exports --export([start_link/3, subscribe/2, unsubscribe/2, dispatch/2, - async_subscribe/2, async_unsubscribe/2]). - --export([subscribers/1]). - -%% gen_server. --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {pool, id, env}). - --spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). -start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). - --spec(subscribe(binary(), emqttd:subscriber()) -> ok). -subscribe(Topic, Subscriber) -> - call(pick(Topic), {subscribe, Topic, Subscriber}). - --spec(async_subscribe(binary(), emqttd:subscriber()) -> ok). -async_subscribe(Topic, Subscriber) -> - cast(pick(Topic), {subscribe, Topic, Subscriber}). - -%% @doc Dispatch Message to Subscribers --spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). -dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> - case subscribers(Topic) of - [] -> - dropped(Topic), {ok, Delivery}; - [Sub] -> %% optimize? - dispatch(Sub, Topic, Msg), - {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1} | Flows]}}; - Subscribers -> - Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], - lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), - {ok, Delivery#mqtt_delivery{flows = Flows1}} - end. - -dispatch(Pid, Topic, Msg) when is_pid(Pid) -> - Pid ! {dispatch, Topic, Msg}; -dispatch(SubId, Topic, Msg) when is_binary(SubId) -> - emqttd_sm:dispatch(SubId, Topic, Msg). - -subscribers(Topic) -> - try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. - -%% @private -%% @doc Ingore $SYS Messages. -dropped(<<"$SYS/", _/binary>>) -> - ok; -dropped(_Topic) -> - emqttd_metrics:inc('messages/dropped'). - --spec(unsubscribe(binary(), emqttd:subscriber()) -> ok). -unsubscribe(Topic, Subscriber) -> - call(pick(Topic), {unsubscribe, Topic, Subscriber}). - --spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). -async_unsubscribe(Topic, Subscriber) -> - cast(pick(Topic), {unsubscribe, Topic, Subscriber}). - -call(Server, Req) -> - gen_server2:call(Server, Req, infinity). - -cast(Server, Msg) -> - gen_server2:cast(Server, Msg). - -pick(Topic) -> - gproc_pool:pick_worker(dispatcher, Topic). - -init([Pool, Id, Env]) -> - ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env}}. - -handle_call({subscribe, Topic, Subscriber}, _From, State) -> - add_subscriber_(Topic, Subscriber), - {reply, ok, State}; - -handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> - del_subscriber_(Topic, Subscriber), - {reply, ok, State}; - -handle_call(Req, _From, State) -> - ?UNEXPECTED_REQ(Req, State). - -handle_cast({subscribe, Topic, Subscriber}, State) -> - add_subscriber_(Topic, Subscriber), - {noreply, State}; - -handle_cast({unsubscribe, Topic, Subscriber}, State) -> - del_subscriber_(Topic, Subscriber), - {noreply, State}; - -handle_cast(Msg, State) -> - ?UNEXPECTED_MSG(Msg, State). - -handle_info(Info, State) -> - ?UNEXPECTED_INFO(Info, State). - -terminate(_Reason, #state{pool = Pool, id = Id}) -> - ?GPROC_POOL(leave, Pool, Id). - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%-------------------------------------------------------------------- -%% Internel Functions -%%-------------------------------------------------------------------- - -add_subscriber_(Topic, Subscriber) -> - case ets:member(subscriber, Topic) of - false -> emqttd_router:add_route(Topic, node()); - true -> ok - end, - ets:insert(subscriber, {Topic, Subscriber}). - -del_subscriber_(Topic, Subscriber) -> - ets:delete_object(subscriber, {Topic, Subscriber}), - case ets:member(subscriber, Topic) of - false -> emqttd_router:del_route(Topic, node()); - true -> ok - end. - diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 9c9524ee6..cd58d2b95 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -16,93 +16,46 @@ -module(emqttd_pubsub). --author("Feng Lee "). - -behaviour(gen_server2). -include("emqttd.hrl"). --include("emqttd_protocol.hrl"). - -include("emqttd_internal.hrl"). -%% Start --export([start_link/3]). +%% API Exports +-export([start_link/3, subscribe/2, unsubscribe/2, publish/2, + async_subscribe/2, async_unsubscribe/2]). -%% PubSub API. --export([subscribe/1, subscribe/2, subscribe/3, publish/2, - unsubscribe/1, unsubscribe/2]). - -%% Async PubSub API. --export([async_subscribe/1, async_subscribe/2, async_subscribe/3, - async_unsubscribe/1, async_unsubscribe/2]). - --export([subscriber_down/1]). - -%% Management API. --export([setqos/3, is_subscribed/2, subscriptions/1]). - -%% Debug API --export([dump/0]). +-export([subscribers/1]). %% gen_server. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {pool, id, env, submon :: emqttd_pmon:pmon()}). +-record(state, {pool, id, env}). -define(PUBSUB, ?MODULE). --define(Dispatcher, emqttd_dispatcher). - -%% @doc Start a pubsub server -spec(start_link(atom(), pos_integer(), [tuple()]) -> {ok, pid()} | ignore | {error, any()}). start_link(Pool, Id, Env) -> - gen_server2:start_link({local, ?PROC_NAME(?PUBSUB, Id)}, ?MODULE, [Pool, Id, Env], []). + gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). -%%-------------------------------------------------------------------- -%% PubSub API -%%-------------------------------------------------------------------- - -%% @doc Subscribe a Topic --spec(subscribe(binary()) -> ok | emqttd:pubsub_error()). -subscribe(Topic) when is_binary(Topic) -> - subscribe(Topic, self()). - --spec(subscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). -subscribe(Topic, Subscriber) when is_binary(Topic) -> - subscribe(Topic, Subscriber, []). - --spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> - ok | emqttd:pubsub_error()). -subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> - call(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). - -%% @doc Subscribe a Topic Asynchronously --spec(async_subscribe(binary()) -> ok). -async_subscribe(Topic) when is_binary(Topic) -> - async_subscribe(Topic, self()). +-spec(subscribe(binary(), emqttd:subscriber()) -> ok). +subscribe(Topic, Subscriber) -> + call(pick(Topic), {subscribe, Topic, Subscriber}). -spec(async_subscribe(binary(), emqttd:subscriber()) -> ok). -async_subscribe(Topic, Subscriber) when is_binary(Topic) -> - async_subscribe(Topic, Subscriber, []). +async_subscribe(Topic, Subscriber) -> + cast(pick(Topic), {subscribe, Topic, Subscriber}). --spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). -async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> - cast(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). - -subscriber_down(Subscriber) -> - cast(pick(Subscriber), {down, Subscriber}). - -%% @doc Publish message to Topic. -spec(publish(binary(), any()) -> {ok, mqtt_delivery()} | ignore). -publish(Topic, Msg) when is_binary(Topic) -> +publish(Topic, Msg) -> route(emqttd_router:match(Topic), delivery(Msg)). %% Dispatch on the local node route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> - ?Dispatcher:dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); + dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]}); %% Forward to other nodes route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) -> @@ -117,110 +70,81 @@ delivery(Msg) -> #mqtt_delivery{message = Msg, flows = []}. %% @doc Forward message to another node... forward(Node, To, Delivery) -> - rpc:cast(Node, ?Dispatcher, dispatch, [To, Delivery]), {ok, Delivery}. + rpc:cast(Node, ?PUBSUB, dispatch, [To, Delivery]), {ok, Delivery}. -subscriptions(Subscriber) -> - lists:map(fun({_, Topic}) -> - subscription(Topic, Subscriber) - end, ets:lookup(mqtt_subscription, Subscriber)). +%% @doc Dispatch Message to Subscribers +-spec(dispatch(binary(), mqtt_delivery()) -> mqtt_delivery()). +dispatch(Topic, Delivery = #mqtt_delivery{message = Msg, flows = Flows}) -> + case subscribers(Topic) of + [] -> + dropped(Topic), {ok, Delivery}; + [Sub] -> %% optimize? + dispatch(Sub, Topic, Msg), + {ok, Delivery#mqtt_delivery{flows = [{dispatch, Topic, 1} | Flows]}}; + Subscribers -> + Flows1 = [{dispatch, Topic, length(Subscribers)} | Flows], + lists:foreach(fun(Sub) -> dispatch(Sub, Topic, Msg) end, Subscribers), + {ok, Delivery#mqtt_delivery{flows = Flows1}} + end. -subscription(Topic, Subscriber) -> - {Topic, ets:lookup_element(mqtt_pubsub, {Topic, Subscriber}, 2)}. +dispatch(Pid, Topic, Msg) when is_pid(Pid) -> + Pid ! {dispatch, Topic, Msg}; +dispatch(SubId, Topic, Msg) when is_binary(SubId) -> + emqttd_sm:dispatch(SubId, Topic, Msg). -is_subscribed(Topic, Subscriber) when is_binary(Topic) -> - ets:member(mqtt_pubsub, {Topic, Subscriber}). +subscribers(Topic) -> + try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. -setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> - call(pick(Subscriber), {setqos, Topic, Subscriber, Qos}). +%% @private +%% @doc Ingore $SYS Messages. +dropped(<<"$SYS/", _/binary>>) -> + ok; +dropped(_Topic) -> + emqttd_metrics:inc('messages/dropped'). -dump() -> - [{Tab, ets:tab2list(Tab)} || Tab <- [mqtt_pubsub, mqtt_subscription, mqtt_subscriber]]. - -%% @doc Unsubscribe --spec(unsubscribe(binary()) -> ok | emqttd:pubsub_error()). -unsubscribe(Topic) when is_binary(Topic) -> - unsubscribe(Topic, self()). - -%% @doc Unsubscribe --spec(unsubscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). -unsubscribe(Topic, Subscriber) when is_binary(Topic) -> - call(pick(Subscriber), {unsubscribe, Topic, Subscriber}). - -%% @doc Async Unsubscribe --spec(async_unsubscribe(binary()) -> ok). -async_unsubscribe(Topic) when is_binary(Topic) -> - async_unsubscribe(Topic, self()). +-spec(unsubscribe(binary(), emqttd:subscriber()) -> ok). +unsubscribe(Topic, Subscriber) -> + call(pick(Topic), {unsubscribe, Topic, Subscriber}). -spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). -async_unsubscribe(Topic, Subscriber) when is_binary(Topic) -> - cast(pick(Subscriber), {unsubscribe, Topic, Subscriber}). +async_unsubscribe(Topic, Subscriber) -> + cast(pick(Topic), {unsubscribe, Topic, Subscriber}). -call(PubSub, Req) when is_pid(PubSub) -> - gen_server2:call(PubSub, Req, infinity). +call(Server, Req) -> + gen_server2:call(Server, Req, infinity). -cast(PubSub, Msg) when is_pid(PubSub) -> - gen_server2:cast(PubSub, Msg). +cast(Server, Msg) -> + gen_server2:cast(Server, Msg). -pick(Subscriber) -> - gproc_pool:pick_worker(pubsub, Subscriber). - -%%-------------------------------------------------------------------- -%% gen_server Callbacks -%%-------------------------------------------------------------------- +pick(Topic) -> + gproc_pool:pick_worker(pubsub, Topic). init([Pool, Id, Env]) -> ?GPROC_POOL(join, Pool, Id), - {ok, #state{pool = Pool, id = Id, env = Env, submon = emqttd_pmon:new()}}. + {ok, #state{pool = Pool, id = Id, env = Env}}. -handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> - case do_subscribe(Topic, Subscriber, Options, State) of - {ok, NewState} -> {reply, ok, setstats(NewState)}; - {error, Error} -> {reply, {error, Error}, State} - end; +handle_call({subscribe, Topic, Subscriber}, _From, State) -> + add_subscriber_(Topic, Subscriber), + {reply, ok, setstats(State)}; handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> - case do_unsubscribe(Topic, Subscriber, State) of - {ok, NewState} -> {reply, ok, setstats(NewState), hibernate}; - {error, Error} -> {reply, {error, Error}, State} - end; - -handle_call({setqos, Topic, Subscriber, Qos}, _From, State) -> - Key = {Topic, Subscriber}, - case ets:lookup(mqtt_pubsub, Key) of - [{_, Opts}] -> - Opts1 = lists:ukeymerge(1, [{qos, Qos}], Opts), - ets:insert(mqtt_pubsub, {Key, Opts1}), - {reply, ok, State}; - [] -> - {reply, {error, {subscription_not_found, Topic}}, State} - end; + del_subscriber_(Topic, Subscriber), + {reply, ok, setstats(State)}; handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({subscribe, Topic, Subscriber, Options}, State) -> - case do_subscribe(Topic, Subscriber, Options, State) of - {ok, NewState} -> {noreply, setstats(NewState)}; - {error, _Error} -> {noreply, State} - end; +handle_cast({subscribe, Topic, Subscriber}, State) -> + add_subscriber_(Topic, Subscriber), + {noreply, setstats(State)}; handle_cast({unsubscribe, Topic, Subscriber}, State) -> - case do_unsubscribe(Topic, Subscriber, State) of - {ok, NewState} -> {noreply, setstats(NewState), hibernate}; - {error, _Error} -> {noreply, State} - end; - -handle_cast({down, Subscriber}, State) -> - subscriber_down_(Subscriber), - {noreply, State}; + del_subscriber_(Topic, Subscriber), + {noreply, setstats(State)}; handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). -handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{submon = PM}) -> - subscriber_down_(DownPid), - {noreply, setstats(State#state{submon = PM:erase(DownPid)}), hibernate}; - handle_info(Info, State) -> ?UNEXPECTED_INFO(Info, State). @@ -228,71 +152,27 @@ terminate(_Reason, #state{pool = Pool, id = Id}) -> ?GPROC_POOL(leave, Pool, Id). code_change(_OldVsn, State, _Extra) -> - {ok, State}. + {ok, State}. %%-------------------------------------------------------------------- -%% Internal Functions +%% Internel Functions %%-------------------------------------------------------------------- -do_subscribe(Topic, Subscriber, Options, State) -> - case ets:lookup(mqtt_pubsub, {Topic, Subscriber}) of - [] -> - ?Dispatcher:async_subscribe(Topic, Subscriber), - add_subscription(Subscriber, Topic), - ets:insert(mqtt_pubsub, {{Topic, Subscriber}, Options}), - {ok, monitor_subpid(Subscriber, State)}; - [_] -> - {error, {already_subscribed, Topic}} +add_subscriber_(Topic, Subscriber) -> + case ets:member(mqtt_subscriber, Topic) of + false -> emqttd_router:add_route(Topic, node()); + true -> ok + end, + ets:insert(subscriber, {Topic, Subscriber}). + +del_subscriber_(Topic, Subscriber) -> + ets:delete_object(mqtt_subscriber, {Topic, Subscriber}), + case ets:member(mqtt_subscriber, Topic) of + false -> emqttd_router:del_route(Topic, node()); + true -> ok end. -add_subscription(Subscriber, Topic) -> - ets:insert(mqtt_subscription, {Subscriber, Topic}). - -do_unsubscribe(Topic, Subscriber, State) -> - case ets:lookup(mqtt_pubsub, {Topic, Subscriber}) of - [_] -> - ?Dispatcher:async_unsubscribe(Topic, Subscriber), - del_subscription(Subscriber, Topic), - ets:delete(mqtt_pubsub, {Topic, Subscriber}), - {ok, case ets:member(mqtt_subscription, Subscriber) of - true -> State; - false -> demonitor_subpid(Subscriber, State) - end}; - [] -> - {error, {subscription_not_found, Topic}} - end. - -del_subscription(Subscriber, Topic) -> - ets:delete_object(mqtt_subscription, {Subscriber, Topic}). - -subscriber_down_(Subscriber) -> - lists:foreach(fun({_, Topic}) -> - subscriber_down_(Subscriber, Topic) - end, ets:lookup(mqtt_subscription, Subscriber)), - ets:delete(mqtt_subscription, Subscriber). - -subscriber_down_(DownPid, Topic) -> - case ets:lookup(mqtt_pubsub, {Topic, DownPid}) of - [] -> - %% here? - ?Dispatcher:async_unsubscribe(Topic, DownPid); - [_] -> - ?Dispatcher:async_unsubscribe(Topic, DownPid), - ets:delete(mqtt_pubsub, {Topic, DownPid}) - end. - -monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> - State#state{submon = PMon:monitor(SubPid)}; -monitor_subpid(_SubPid, State) -> - State. - -demonitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> - State#state{submon = PMon:demonitor(SubPid)}; -demonitor_subpid(_SubPid, State) -> - State. - setstats(State) when is_record(State, state) -> - emqttd_stats:setstats('subscribers/count', 'subscribers/max', ets:info(mqtt_subscriber, size)), - emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', ets:info(mqtt_subscription, size)), - State. + emqttd_stats:setstats('subscribers/count', 'subscribers/max', + ets:info(mqtt_subscriber, size)). diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 9b794dcfd..167ad4942 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -43,18 +43,17 @@ pubsub_pool() -> init([Env]) -> %% Create ETS Tables - [create_tab(Tab) || Tab <- [mqtt_pubsub, mqtt_subscriber, mqtt_subscription]], - - %% Dispatcher Pool - DispatcherMFA = {emqttd_dispatcher, start_link, [Env]}, - DispatcherPool = pool_sup(dispatcher, Env, DispatcherMFA), + [create_tab(Tab) || Tab <- [mqtt_subpropery, mqtt_subscriber, mqtt_subscription]], %% PubSub Pool - {ok, PubSub} = emqttd:conf(pubsub_adapter), - PubSubMFA = {PubSub, start_link, [Env]}, + PubSubMFA = {emqttd_pubsub, start_link, [Env]}, PubSubPool = pool_sup(pubsub, Env, PubSubMFA), - {ok, { {one_for_all, 10, 3600}, [DispatcherPool, PubSubPool]} }. + %% Server Pool + ServerMFA = {emqttd_server, start_link, [Env]}, + ServerPool = pool_sup(server, Env, ServerMFA), + + {ok, { {one_for_all, 10, 3600}, [PubSubPool, ServerPool]} }. pool_size(Env) -> Schedulers = erlang:system_info(schedulers), @@ -68,9 +67,9 @@ pool_sup(Name, Env, MFA) -> %% Create PubSub Tables %%-------------------------------------------------------------------- -create_tab(mqtt_pubsub) -> +create_tab(mqtt_subproperty) -> %% Subproperty: {Topic, Sub} -> [{qos, 1}] - ensure_tab(mqtt_pubsub, [public, named_table, set | ?CONCURRENCY_OPTS]); + ensure_tab(mqtt_subproperty, [public, named_table, set | ?CONCURRENCY_OPTS]); create_tab(mqtt_subscriber) -> %% Subscriber: Topic -> Sub1, Sub2, Sub3, ..., SubN diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl new file mode 100644 index 000000000..9feb3eb98 --- /dev/null +++ b/src/emqttd_server.erl @@ -0,0 +1,286 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_server). + +-author("Feng Lee "). + +-behaviour(gen_server2). + +-include("emqttd.hrl"). + +-include("emqttd_protocol.hrl"). + +-include("emqttd_internal.hrl"). + +-export([start_link/3]). + +%% PubSub API. +-export([subscribe/1, subscribe/2, subscribe/3, publish/2, + unsubscribe/1, unsubscribe/2]). + +%% Async PubSub API. +-export([async_subscribe/1, async_subscribe/2, async_subscribe/3, + async_unsubscribe/1, async_unsubscribe/2]). + +%% Management API. +-export([setqos/3, is_subscribed/2, get_subscriptions/1, subscriber_down/1]). + +%% Debug API +-export([dump/0]). + +%% gen_server. +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(state, {pool, id, env, submon :: emqttd_pmon:pmon()}). + +%% @doc Start server +-spec(start_link(atom(), pos_integer(), list()) -> {ok, pid()} | ignore | {error, any()}). +start_link(Pool, Id, Env) -> + gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id, Env], []). + +%%-------------------------------------------------------------------- +%% PubSub API +%%-------------------------------------------------------------------- + +%% @doc Subscribe a Topic +-spec(subscribe(binary()) -> ok | emqttd:pubsub_error()). +subscribe(Topic) when is_binary(Topic) -> + subscribe(Topic, self()). + +-spec(subscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). +subscribe(Topic, Subscriber) when is_binary(Topic) -> + subscribe(Topic, Subscriber, []). + +-spec(subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> + ok | emqttd:pubsub_error()). +subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> + call(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). + +%% @doc Subscribe a Topic Asynchronously +-spec(async_subscribe(binary()) -> ok). +async_subscribe(Topic) when is_binary(Topic) -> + async_subscribe(Topic, self()). + +-spec(async_subscribe(binary(), emqttd:subscriber()) -> ok). +async_subscribe(Topic, Subscriber) when is_binary(Topic) -> + async_subscribe(Topic, Subscriber, []). + +-spec(async_subscribe(binary(), emqttd:subscriber(), [emqttd:suboption()]) -> ok). +async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> + cast(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). + +%% @doc Publish message to Topic. +-spec(publish(binary(), any()) -> {ok, mqtt_delivery()} | ignore). +publish(Topic, Msg = #mqtt_message{from = From}) -> + trace(publish, From, Msg), + case emqttd_hook:run('message.publish', [], Msg) of + {ok, Msg1 = #mqtt_message{topic = Topic}} -> + %% Retain message first. Don't create retained topic. + Msg2 = case emqttd_retainer:retain(Msg1) of + ok -> emqttd_message:unset_flag(Msg1); + ignore -> Msg1 + end, + emqttd_pubsub:publish(Topic, Msg2); + {stop, Msg1} -> + lager:warning("Stop publishing: ~s", [emqttd_message:format(Msg1)]), + ignore + end. + +trace(publish, From, _Msg) when is_atom(From) -> + %% Dont' trace '$SYS' publish + ignore; + +trace(publish, From, #mqtt_message{topic = Topic, payload = Payload}) -> + lager:info([{client, From}, {topic, Topic}], + "~s PUBLISH to ~s: ~p", [From, Topic, Payload]). + +%% @doc Unsubscribe +-spec(unsubscribe(binary()) -> ok | emqttd:pubsub_error()). +unsubscribe(Topic) when is_binary(Topic) -> + unsubscribe(Topic, self()). + +%% @doc Unsubscribe +-spec(unsubscribe(binary(), emqttd:subscriber()) -> ok | emqttd:pubsub_error()). +unsubscribe(Topic, Subscriber) when is_binary(Topic) -> + call(pick(Subscriber), {unsubscribe, Topic, Subscriber}). + +%% @doc Async Unsubscribe +-spec(async_unsubscribe(binary()) -> ok). +async_unsubscribe(Topic) when is_binary(Topic) -> + async_unsubscribe(Topic, self()). + +-spec(async_unsubscribe(binary(), emqttd:subscriber()) -> ok). +async_unsubscribe(Topic, Subscriber) when is_binary(Topic) -> + cast(pick(Subscriber), {unsubscribe, Topic, Subscriber}). + +setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> + call(pick(Subscriber), {setqos, Topic, Subscriber, Qos}). + +-spec(is_subscribed(binary(), emqttd:subscriber()) -> boolean()). +is_subscribed(Topic, Subscriber) when is_binary(Topic) -> + ets:member(mqtt_subproperty, {Topic, Subscriber}). + +-spec(get_subscriptions(emqttd:subscriber()) -> [{binary(), list()}]). +get_subscriptions(Subscriber) -> + lists:map(fun({_, Topic}) -> + subscription(Topic, Subscriber) + end, ets:lookup(mqtt_subscription, Subscriber)). + +subscription(Topic, Subscriber) -> + {Topic, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)}. + +subscriber_down(Subscriber) -> + cast(pick(Subscriber), {subscriber_down, Subscriber}). + +call(Server, Req) -> + gen_server2:call(Server, Req, infinity). + +cast(Server, Msg) when is_pid(Server) -> + gen_server2:cast(Server, Msg). + +pick(Subscriber) -> + gproc_pool:pick_worker(server, Subscriber). + +dump() -> + [{Tab, ets:tab2list(Tab)} || Tab <- [mqtt_subproperty, mqtt_subscription, mqtt_subscriber]]. + +%%-------------------------------------------------------------------- +%% gen_server Callbacks +%%-------------------------------------------------------------------- + +init([Pool, Id, Env]) -> + ?GPROC_POOL(join, Pool, Id), + {ok, #state{pool = Pool, id = Id, env = Env, submon = emqttd_pmon:new()}}. + +handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> + case subscribe_(Topic, Subscriber, Options, State) of + {ok, NewState} -> {reply, ok, setstats(NewState)}; + {error, Error} -> {reply, {error, Error}, State} + end; + +handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> + case unsubscribe_(Topic, Subscriber, State) of + {ok, NewState} -> {reply, ok, setstats(NewState), hibernate}; + {error, Error} -> {reply, {error, Error}, State} + end; + +handle_call({setqos, Topic, Subscriber, Qos}, _From, State) -> + Key = {Topic, Subscriber}, + case ets:lookup(mqtt_subproperty, Key) of + [{_, Opts}] -> + Opts1 = lists:ukeymerge(1, [{qos, Qos}], Opts), + ets:insert(mqtt_subproperty, {Key, Opts1}), + {reply, ok, State}; + [] -> + {reply, {error, {subscription_not_found, Topic}}, State} + end; + +handle_call(Req, _From, State) -> + ?UNEXPECTED_REQ(Req, State). + +handle_cast({subscribe, Topic, Subscriber, Options}, State) -> + case subscribe_(Topic, Subscriber, Options, State) of + {ok, NewState} -> {noreply, setstats(NewState)}; + {error, _Error} -> {noreply, State} + end; + +handle_cast({unsubscribe, Topic, Subscriber}, State) -> + case unsubscribe_(Topic, Subscriber, State) of + {ok, NewState} -> {noreply, setstats(NewState), hibernate}; + {error, _Error} -> {noreply, State} + end; + +handle_cast({subscriber_down, Subscriber}, State) -> + subscriber_down_(Subscriber), + {noreply, setstats(State)}; + +handle_cast(Msg, State) -> + ?UNEXPECTED_MSG(Msg, State). + +handle_info({'DOWN', _MRef, process, DownPid, _Reason}, State = #state{submon = PM}) -> + subscriber_down_(DownPid), + {noreply, setstats(State#state{submon = PM:erase(DownPid)}), hibernate}; + +handle_info(Info, State) -> + ?UNEXPECTED_INFO(Info, State). + +terminate(_Reason, #state{pool = Pool, id = Id}) -> + ?GPROC_POOL(leave, Pool, Id). + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +subscribe_(Topic, Subscriber, Options, State) -> + case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of + [] -> + emqttd_pubsub:async_subscribe(Topic, Subscriber), + ets:insert(mqtt_subscription, {Subscriber, Topic}), + ets:insert(mqtt_subproperty, {{Topic, Subscriber}, Options}), + {ok, monitor_subpid(Subscriber, State)}; + [_] -> + {error, {already_subscribed, Topic}} + end. + +unsubscribe_(Topic, Subscriber, State) -> + case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of + [_] -> + emqttd_pubsub:async_unsubscribe(Topic, Subscriber), + ets:delete_object(mqtt_subscription, {Subscriber, Topic}), + ets:delete(mqtt_subproperty, {Topic, Subscriber}), + {ok, case ets:member(mqtt_subscription, Subscriber) of + true -> State; + false -> demonitor_subpid(Subscriber, State) + end}; + [] -> + {error, {subscription_not_found, Topic}} + end. + +monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> + State#state{submon = PMon:monitor(SubPid)}; +monitor_subpid(_SubPid, State) -> + State. + +demonitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> + State#state{submon = PMon:demonitor(SubPid)}; +demonitor_subpid(_SubPid, State) -> + State. + +subscriber_down_(Subscriber) -> + lists:foreach(fun({_, Topic}) -> + subscriber_down_(Subscriber, Topic) + end, ets:lookup(mqtt_subscription, Subscriber)), + ets:delete(mqtt_subscription, Subscriber). + +subscriber_down_(Subscriber, Topic) -> + case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of + [] -> + %% here? + emqttd_pubsub:async_unsubscribe(Topic, Subscriber); + [_] -> + emqttd_pubsub:async_unsubscribe(Topic, Subscriber), + ets:delete(mqtt_subproperty, {Topic, Subscriber}) + end. + +setstats(State) -> + emqttd_stats:setstats('subscriptions/count', 'subscriptions/max', + ets:info(mqtt_subscription, size)), State. + diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 63feb4617..f27ba605b 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -232,7 +232,7 @@ init([CleanSess, ClientId, ClientPid]) -> expired_after = get_value(expired_after, SessEnv) * 60, collect_interval = get_value(collect_interval, SessEnv, 0), timestamp = os:timestamp()}, - emqttd_sm:register_session(CleanSess, ClientId, sess_info(Session)), + emqttd_sm:register_session(ClientId, CleanSess, sess_info(Session)), %% Start statistics {ok, start_collector(Session), hibernate}. @@ -297,7 +297,7 @@ handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), SubDict; {ok, OldQos} -> - emqttd_pubsub:setqos(Topic, ClientId, Qos), + emqttd_server:setqos(Topic, ClientId, Qos), ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), dict:store(Topic, Qos, SubDict); error -> @@ -385,8 +385,8 @@ handle_cast({resume, ClientId, ClientPid}, Session = #session{client_id = C if CleanSess =:= true -> ?LOG(warning, "CleanSess changed to false.", [], Session), - emqttd_sm:unregister_session(CleanSess, ClientId), - emqttd_sm:register_session(false, ClientId, sess_info(Session1)); + %% emqttd_sm:unregister_session(CleanSess, ClientId), + emqttd_sm:register_session(ClientId, false, sess_info(Session1)); CleanSess =:= false -> ok end, @@ -500,7 +500,7 @@ handle_info({timeout, awaiting_comp, PktId}, Session = #session{awaiting_comp = end; handle_info(collect_info, Session = #session{clean_sess = CleanSess, client_id = ClientId}) -> - emqttd_sm:register_session(CleanSess, ClientId, sess_info(Session)), + emqttd_sm:register_session(ClientId, CleanSess, sess_info(Session)), hibernate(start_collector(Session)); handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, @@ -531,10 +531,10 @@ handle_info(expired, Session) -> handle_info(Info, Session) -> ?UNEXPECTED_INFO(Info, Session). -terminate(_Reason, #session{clean_sess = CleanSess, client_id = ClientId}) -> +terminate(_Reason, #session{client_id = ClientId}) -> %%TODO: ... - emqttd_pubsub:subscriber_down(ClientId), - emqttd_sm:unregister_session(CleanSess, ClientId). + emqttd_server:subscriber_down(ClientId), + emqttd_sm:unregister_session(ClientId). code_change(_OldVsn, Session, _Extra) -> {ok, Session}. diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index de23d3702..a7cfb1724 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -91,8 +91,8 @@ lookup_session(ClientId) -> end. %% @doc Register a session with info. --spec(register_session(boolean(), binary(), [tuple()]) -> true). -register_session(CleanSess, ClientId, Properties) -> +-spec(register_session(binary(), boolean(), [tuple()]) -> true). +register_session(ClientId, CleanSess, Properties) -> ets:insert(mqtt_local_session, {ClientId, self(), CleanSess, Properties}). %% @doc Unregister a session. From b98c147d813504da5d6f896ce37d14798b985fd5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 21:30:08 +0800 Subject: [PATCH 063/116] tables --- docs/source/tables.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/source/tables.md diff --git a/docs/source/tables.md b/docs/source/tables.md new file mode 100644 index 000000000..9268e7822 --- /dev/null +++ b/docs/source/tables.md @@ -0,0 +1,31 @@ + +.. _tables: + +====== +Tables +====== + ++--------------------+--------+----------------------------------------+ +| Table | Type | Description | ++====================+========+========================================+ +| mqtt_trie | mnesia | Trie Table | ++--------------------+--------+----------------------------------------+ +| mqtt_trie_node | mnesia | Trie Node Table | ++--------------------+--------+----------------------------------------+ +| mqtt_route | mnesia | Global Route Table | ++--------------------+--------+----------------------------------------+ +| mqtt_local_route | mnesia | Local Route Table | ++--------------------+--------+----------------------------------------+ +| mqtt_pubsub | ets | PubSub Tab | ++--------------------+--------+----------------------------------------+ +| mqtt_subscriber | ets | Subscriber Tab | ++--------------------+--------+----------------------------------------+ +| mqtt_subscription | ets | Subscription Tab | ++--------------------+--------+----------------------------------------+ +| mqtt_session | mnesia | Global Session Table | ++--------------------+--------+----------------------------------------+ +| mqtt_local_session | ets | Local Session Table | ++--------------------+--------+----------------------------------------+ +| mqtt_client | ets | Client Table | ++--------------------+--------+----------------------------------------+ + From c67a30bf7b1f162977567ee5197fde9dbe886168 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 21:38:00 +0800 Subject: [PATCH 064/116] mqtt_subproperty --- src/emqttd_pubsub_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 167ad4942..1b406e751 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -43,7 +43,7 @@ pubsub_pool() -> init([Env]) -> %% Create ETS Tables - [create_tab(Tab) || Tab <- [mqtt_subpropery, mqtt_subscriber, mqtt_subscription]], + [create_tab(Tab) || Tab <- [mqtt_subproperty, mqtt_subscriber, mqtt_subscription]], %% PubSub Pool PubSubMFA = {emqttd_pubsub, start_link, [Env]}, From d6041395ef42640a5e38604f279ddf1a57aa8fe8 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 21:45:37 +0800 Subject: [PATCH 065/116] mqtt_session and mqtt_local_session --- src/emqttd_sm.erl | 18 ++++++++++-------- src/emqttd_sm_helper.erl | 7 +++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index a7cfb1724..0be4524c7 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -85,7 +85,7 @@ start_session(CleanSess, ClientId) -> %% @doc Lookup a Session -spec(lookup_session(binary()) -> mqtt_session() | undefined). lookup_session(ClientId) -> - case mnesia:dirty_read(session, ClientId) of + case mnesia:dirty_read(mqtt_session, ClientId) of [Session] -> Session; [] -> undefined end. @@ -166,11 +166,13 @@ handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> case dict:find(MRef, State#state.monitors) of {ok, ClientId} -> mnesia:transaction(fun() -> - case mnesia:wread({session, ClientId}) of - [] -> ok; + case mnesia:wread({mqtt_session, ClientId}) of + [] -> + ok; [Sess = #mqtt_session{sess_pid = DownPid}] -> - mnesia:delete_object(session, Sess, write); - [_Sess] -> ok + mnesia:delete_object(mqtt_session, Sess, write); + [_Sess] -> + ok end end), {noreply, erase_monitor(MRef, State), hibernate}; @@ -221,9 +223,9 @@ create_session(CleanSess, ClientId, ClientPid) -> insert_session(Session = #mqtt_session{client_id = ClientId}) -> mnesia:transaction( fun() -> - case mnesia:wread({session, ClientId}) of + case mnesia:wread({mqtt_session, ClientId}) of [] -> - mnesia:write(session, Session, write); + mnesia:write(mqtt_session, Session, write); [#mqtt_session{sess_pid = SessPid}] -> mnesia:abort({conflict, SessPid}) end @@ -280,7 +282,7 @@ destroy_session(Session = #mqtt_session{client_id = ClientId, end. remove_session(Session) -> - case mnesia:transaction(fun mnesia:delete_object/3, [session, Session, write]) of + case mnesia:transaction(fun mnesia:delete_object/1, [Session]) of {atomic, ok} -> ok; {aborted, Error} -> {error, Error} end. diff --git a/src/emqttd_sm_helper.erl b/src/emqttd_sm_helper.erl index 184f2be64..aa6a7e365 100644 --- a/src/emqttd_sm_helper.erl +++ b/src/emqttd_sm_helper.erl @@ -54,10 +54,9 @@ handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> lager:error("!!!Mnesia node down: ~s", [Node]), Fun = fun() -> ClientIds = - mnesia:select(session, [{#mqtt_session{client_id = '$1', sess_pid = '$2', _ = '_'}, - [{'==', {node, '$2'}, Node}], - ['$1']}]), - lists:foreach(fun(ClientId) -> mnesia:delete({session, ClientId}) end, ClientIds) + mnesia:select(mqtt_session, [{#mqtt_session{client_id = '$1', sess_pid = '$2', _ = '_'}, + [{'==', {node, '$2'}, Node}], ['$1']}]), + lists:foreach(fun(ClientId) -> mnesia:delete({mqtt_session, ClientId}) end, ClientIds) end, mnesia:async_dirty(Fun), {noreply, State}; From 417cb3620d1e0e24b70820b63318a70439e8e75e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 21:50:28 +0800 Subject: [PATCH 066/116] mqtt_subscriber --- src/emqttd_pubsub.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index cd58d2b95..10a6466aa 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -163,7 +163,7 @@ add_subscriber_(Topic, Subscriber) -> false -> emqttd_router:add_route(Topic, node()); true -> ok end, - ets:insert(subscriber, {Topic, Subscriber}). + ets:insert(mqtt_subscriber, {Topic, Subscriber}). del_subscriber_(Topic, Subscriber) -> ets:delete_object(mqtt_subscriber, {Topic, Subscriber}), From 0d16f766c3b4d8fdc8140b8f74b469e9335377f4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 21:54:02 +0800 Subject: [PATCH 067/116] setstats for mqtt_subscriber --- src/emqttd_pubsub.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 10a6466aa..1cb68cf08 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -172,7 +172,7 @@ del_subscriber_(Topic, Subscriber) -> true -> ok end. -setstats(State) when is_record(State, state) -> +setstats(State) -> emqttd_stats:setstats('subscribers/count', 'subscribers/max', - ets:info(mqtt_subscriber, size)). + ets:info(mqtt_subscriber, size)), State. From e2dab894af27bcd6805556107f2d812f3563d030 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 22:22:12 +0800 Subject: [PATCH 068/116] emqttd_server:publish/1 --- src/emqttd.erl | 3 +-- src/emqttd_pubsub.erl | 3 +++ src/emqttd_server.erl | 6 +++--- src/emqttd_sm.erl | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index a741adc59..2518d39b3 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -99,8 +99,7 @@ subscribe(Topic, Subscriber, Options) -> %% @doc Publish MQTT Message -spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Msg = #mqtt_message{topic = Topic}) -> - emqttd_server:publish(Topic, Msg). +publish(Msg) -> emqttd_server:publish(Msg). %% @doc Unsubscribe -spec(unsubscribe(iodata()) -> ok | pubsub_error()). diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 1cb68cf08..af1c1537d 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -52,6 +52,9 @@ async_subscribe(Topic, Subscriber) -> publish(Topic, Msg) -> route(emqttd_router:match(Topic), delivery(Msg)). +route([], _Delivery) -> + ignore; + %% Dispatch on the local node route([#mqtt_route{topic = To, node = Node}], Delivery = #mqtt_delivery{flows = Flows}) when Node =:= node() -> diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl index 9feb3eb98..c56ed2309 100644 --- a/src/emqttd_server.erl +++ b/src/emqttd_server.erl @@ -29,7 +29,7 @@ -export([start_link/3]). %% PubSub API. --export([subscribe/1, subscribe/2, subscribe/3, publish/2, +-export([subscribe/1, subscribe/2, subscribe/3, publish/1, unsubscribe/1, unsubscribe/2]). %% Async PubSub API. @@ -85,8 +85,8 @@ async_subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> cast(pick(Subscriber), {subscribe, Topic, Subscriber, Options}). %% @doc Publish message to Topic. --spec(publish(binary(), any()) -> {ok, mqtt_delivery()} | ignore). -publish(Topic, Msg = #mqtt_message{from = From}) -> +-spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). +publish(Msg = #mqtt_message{from = From}) -> trace(publish, From, Msg), case emqttd_hook:run('message.publish', [], Msg) of {ok, Msg1 = #mqtt_message{topic = Topic}} -> diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index 0be4524c7..43de1e91a 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -104,7 +104,7 @@ dispatch(ClientId, Topic, Msg) -> try ets:lookup_element(mqtt_local_session, ClientId, 2) of Pid -> Pid ! {dispatch, Topic, Msg} catch - error:badarg -> ok %%TODO: How?? + error:badarg -> io:format("Session Not Found: ~p~n", [ClientId]), ok %%TODO: How?? end. call(SM, Req) -> From 8a0a7978dd56db9c84c3c73672ebf5f4dbc36e05 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 22:30:02 +0800 Subject: [PATCH 069/116] presence --- src/emqttd_mod_presence.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl index 2db444e8d..89228df78 100644 --- a/src/emqttd_mod_presence.erl +++ b/src/emqttd_mod_presence.erl @@ -50,7 +50,8 @@ on_client_disconnected(Reason, ClientId, Opts) -> {reason, reason(Reason)}, {ts, emqttd_time:now_to_secs()}]), Msg = message(qos(Opts), topic(disconnected, ClientId), Json), - emqttd:publish(emqttd_message:set_flag(sys, Msg)). + emqttd:publish(emqttd_message:set_flag(sys, Msg)), + ok. unload(_Opts) -> emqttd:unhook('client.connected', fun ?MODULE:on_client_connected/3), From c86aa94ae75c101a7438e33bf5cf4ed7e9d02097 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 10 Aug 2016 22:41:06 +0800 Subject: [PATCH 070/116] mqtt_subscriber --- src/emqttd_pubsub.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index af1c1537d..eb4722bb8 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -96,7 +96,7 @@ dispatch(SubId, Topic, Msg) when is_binary(SubId) -> emqttd_sm:dispatch(SubId, Topic, Msg). subscribers(Topic) -> - try ets:lookup_element(subscriber, Topic, 2) catch error:badarg -> [] end. + try ets:lookup_element(mqtt_subscriber, Topic, 2) catch error:badarg -> [] end. %% @private %% @doc Ingore $SYS Messages. From 813506d47a5ba126c6af1e6a30fb51ae4ba424a1 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 12 Aug 2016 11:53:20 +0800 Subject: [PATCH 071/116] Fix test cases for trie --- test/emqttd_trie_SUITE.erl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/emqttd_trie_SUITE.erl b/test/emqttd_trie_SUITE.erl index 7ee9d6616..ceda3abcb 100644 --- a/test/emqttd_trie_SUITE.erl +++ b/test/emqttd_trie_SUITE.erl @@ -22,6 +22,8 @@ -define(TRIE, emqttd_trie). +-include_lib("eunit/include/eunit.hrl"). + all() -> [t_insert, t_match, t_match2, t_match3, t_delete, t_delete2, t_delete3]. @@ -81,9 +83,9 @@ t_match3(_) -> Topics = [<<"d/#">>, <<"a/b/c">>, <<"a/b/+">>, <<"a/#">>, <<"#">>, <<"$SYS/#">>], mnesia:transaction(fun() -> [emqttd_trie:insert(Topic) || Topic <- Topics] end), Matched = mnesia:async_dirty(fun emqttd_trie:match/1, [<<"a/b/c">>]), - 4 = length(Matched), + ?assertEqual(4, length(Matched)), SysMatched = mnesia:async_dirty(fun emqttd_trie:match/1, [<<"$SYS/a/b/c">>]), - [<<"$SYS/#">>] = SysMatched. + ?assertEqual([<<"$SYS/#">>], SysMatched). t_delete(_) -> TN = #trie_node{node_id = <<"sensor/1">>, @@ -129,5 +131,5 @@ t_delete3(_) -> end). clear_tables() -> - lists:foreach(fun mnesia:clear_table/1, [trie, trie_node]). + lists:foreach(fun mnesia:clear_table/1, [mqtt_trie, mqtt_trie_node]). From 9a4c44913e3b240f6e4fbc16aa99c220a4b65e63 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 12 Aug 2016 12:59:03 +0800 Subject: [PATCH 072/116] commen tests --- Makefile | 2 +- src/emqttd.erl | 40 ++++++++++++----- src/emqttd_cli.erl | 14 ++---- src/emqttd_pubsub_sup.erl | 24 ++++++---- src/emqttd_server.erl | 18 +++++--- src/emqttd_session.erl | 9 ++-- src/emqttd_topic.erl | 49 +++++++++++++++------ test/emqttd_SUITE.erl | 82 ++++++++--------------------------- test/emqttd_backend_SUITE.erl | 45 ------------------- test/emqttd_topic_SUITE.erl | 30 ++++++++----- 10 files changed, 134 insertions(+), 179 deletions(-) delete mode 100644 test/emqttd_backend_SUITE.erl diff --git a/Makefile b/Makefile index e471d50ed..49642f180 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ TEST_ERLC_OPTS += +'{parse_transform, lager_transform}' EUNIT_OPTS = verbose # EUNIT_ERL_OPTS = -CT_SUITES = emqttd emqttd_access emqttd_backend emqttd_lib emqttd_mod emqttd_net \ +CT_SUITES = emqttd emqttd_access emqttd_lib emqttd_mod emqttd_net \ emqttd_mqueue emqttd_protocol emqttd_topic emqttd_trie CT_OPTS = -cover test/ct.cover.spec -erl_args -name emqttd_ct@127.0.0.1 diff --git a/src/emqttd.erl b/src/emqttd.erl index 2518d39b3..521251efc 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -14,9 +14,9 @@ %% limitations under the License. %%-------------------------------------------------------------------- --module(emqttd). +%% Facade Module for The EMQTT Broker --author("Feng Lee "). +-module(emqttd). -include("emqttd.hrl"). @@ -29,7 +29,8 @@ unsubscribe/1, unsubscribe/2]). %% PubSub Management API --export([topics/0, subscribers/1, subscriptions/1]). +-export([setqos/3, topics/0, subscriptions/1, subscribers/1, + is_subscribed/2, subscriber_down/1]). %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/3]). @@ -37,7 +38,7 @@ %% Debug API -export([dump/0]). --type(subscriber() :: pid() | binary() | function()). +-type(subscriber() :: pid() | binary()). -type(suboption() :: local | {qos, non_neg_integer()} | {share, {'$queue' | binary()}}). @@ -81,7 +82,7 @@ is_running(Node) -> end. %%-------------------------------------------------------------------- -%% PubSub APIs that wrap emqttd_pubsub +%% PubSub APIs %%-------------------------------------------------------------------- %% @doc Subscribe @@ -95,11 +96,12 @@ subscribe(Topic, Subscriber) -> -spec(subscribe(iodata(), subscriber(), [suboption()]) -> ok | pubsub_error()). subscribe(Topic, Subscriber, Options) -> - emqttd_server:subscribe(iolist_to_binary(Topic), Subscriber, Options). + with_pubsub(fun(PS) -> PS:subscribe(iolist_to_binary(Topic), Subscriber, Options) end). %% @doc Publish MQTT Message -spec(publish(mqtt_message()) -> {ok, mqtt_delivery()} | ignore). -publish(Msg) -> emqttd_server:publish(Msg). +publish(Msg) -> + with_pubsub(fun(PS) -> PS:publish(Msg) end). %% @doc Unsubscribe -spec(unsubscribe(iodata()) -> ok | pubsub_error()). @@ -108,18 +110,32 @@ unsubscribe(Topic) -> -spec(unsubscribe(iodata(), subscriber()) -> ok | pubsub_error()). unsubscribe(Topic, Subscriber) -> - emqttd_server:unsubscribe(iolist_to_binary(Topic), Subscriber). + with_pubsub(fun(PS) -> PS:unsubscribe(iolist_to_binary(Topic), Subscriber) end). + +-spec(setqos(binary(), subscriber(), mqtt_qos()) -> ok). +setqos(Topic, Subscriber, Qos) -> + with_pubsub(fun(PS) -> PS:setqos(iolist_to_binary(Topic), Subscriber, Qos) end). -spec(topics() -> [binary()]). topics() -> emqttd_router:topics(). -spec(subscribers(iodata()) -> list(subscriber())). subscribers(Topic) -> - emqttd_pubsub:subscribers(iolist_to_binary(Topic)). + with_pubsub(fun(PS) -> PS:subscribers(iolist_to_binary(Topic)) end). -spec(subscriptions(subscriber()) -> [{binary(), suboption()}]). subscriptions(Subscriber) -> - emqttd_server:get_subscriptions(Subscriber). + with_pubsub(fun(PS) -> PS:subscriptions(Subscriber) end). + +-spec(is_subscribed(iodata(), subscriber()) -> boolean()). +is_subscribed(Topic, Subscriber) -> + with_pubsub(fun(PS) -> PS:is_subscribed(iolist_to_binary(Topic), Subscriber) end). + +-spec(subscriber_down(subscriber()) -> ok). +subscriber_down(Subscriber) -> + with_pubsub(fun(PS) -> PS:subscriber_down(Subscriber) end). + +with_pubsub(Fun) -> Fun(env(pubsub_server, emqttd_server)). %%-------------------------------------------------------------------- %% Hooks API @@ -141,9 +157,9 @@ unhook(Hook, Function) -> run_hooks(Hook, Args, Acc) -> emqttd_hook:run(Hook, Args, Acc). - %%-------------------------------------------------------------------- %% Debug %%-------------------------------------------------------------------- -dump() -> lists:append([emqttd_server:dump(), emqttd_router:dump()]). +dump() -> with_pubsub(fun(PS) -> lists:append([PS:dump(), emqttd_router:dump()]) end). + diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index a64f086ed..6c1907f2a 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -170,7 +170,7 @@ if_client(ClientId, Fun) -> %%-------------------------------------------------------------------- %% @doc Sessions Command sessions(["list"]) -> - [sessions(["list", Type]) || Type <- ["persistent", "transient"]]; + dump(mqtt_local_session); sessions(["list", "persistent"]) -> dump(mqtt_persistent_session); @@ -179,15 +179,9 @@ sessions(["list", "transient"]) -> dump(mqtt_transient_session); sessions(["show", ClientId]) -> - MP = {{bin(ClientId), '_'}, '_'}, - case {ets:match_object(mqtt_transient_session, MP), - ets:match_object(mqtt_persistent_session, MP)} of - {[], []} -> - ?PRINT_MSG("Not Found.~n"); - {[SessInfo], _} -> - print(SessInfo); - {_, [SessInfo]} -> - print(SessInfo) + case ets:lookup(mqtt_local_session, bin(ClientId)) of + [] -> ?PRINT_MSG("Not Found.~n"); + [SessInfo] -> print(SessInfo) end; sessions(_) -> diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 1b406e751..58143cc83 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -45,24 +45,30 @@ init([Env]) -> %% Create ETS Tables [create_tab(Tab) || Tab <- [mqtt_subproperty, mqtt_subscriber, mqtt_subscription]], - %% PubSub Pool - PubSubMFA = {emqttd_pubsub, start_link, [Env]}, - PubSubPool = pool_sup(pubsub, Env, PubSubMFA), + {ok, { {one_for_all, 10, 3600}, [pool_sup(pubsub, Env), pool_sup(server, Env)]} }. - %% Server Pool - ServerMFA = {emqttd_server, start_link, [Env]}, - ServerPool = pool_sup(server, Env, ServerMFA), - - {ok, { {one_for_all, 10, 3600}, [PubSubPool, ServerPool]} }. +%%-------------------------------------------------------------------- +%% Pool +%%-------------------------------------------------------------------- pool_size(Env) -> Schedulers = erlang:system_info(schedulers), proplists:get_value(pool_size, Env, Schedulers). -pool_sup(Name, Env, MFA) -> +pool_sup(Name, Env) -> Pool = list_to_atom(atom_to_list(Name) ++ "_pool"), + MFA = {adapter(Name), start_link, [Env]}, emqttd_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]). +%%-------------------------------------------------------------------- +%% Adapter +%%-------------------------------------------------------------------- + +adapter(server) -> + emqttd:env(pubsub_server, emqttd_server); +adapter(pubsub) -> + emqttd:env(pubsub_adapter, emqttd_pubsub). + %%-------------------------------------------------------------------- %% Create PubSub Tables %%-------------------------------------------------------------------- diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl index c56ed2309..567d49388 100644 --- a/src/emqttd_server.erl +++ b/src/emqttd_server.erl @@ -37,7 +37,8 @@ async_unsubscribe/1, async_unsubscribe/2]). %% Management API. --export([setqos/3, is_subscribed/2, get_subscriptions/1, subscriber_down/1]). +-export([setqos/3, subscriptions/1, subscribers/1, is_subscribed/2, + subscriber_down/1]). %% Debug API -export([dump/0]). @@ -131,12 +132,8 @@ async_unsubscribe(Topic, Subscriber) when is_binary(Topic) -> setqos(Topic, Subscriber, Qos) when is_binary(Topic) -> call(pick(Subscriber), {setqos, Topic, Subscriber, Qos}). --spec(is_subscribed(binary(), emqttd:subscriber()) -> boolean()). -is_subscribed(Topic, Subscriber) when is_binary(Topic) -> - ets:member(mqtt_subproperty, {Topic, Subscriber}). - --spec(get_subscriptions(emqttd:subscriber()) -> [{binary(), list()}]). -get_subscriptions(Subscriber) -> +-spec(subscriptions(emqttd:subscriber()) -> [{binary(), list(emqttd:suboption())}]). +subscriptions(Subscriber) -> lists:map(fun({_, Topic}) -> subscription(Topic, Subscriber) end, ets:lookup(mqtt_subscription, Subscriber)). @@ -144,6 +141,13 @@ get_subscriptions(Subscriber) -> subscription(Topic, Subscriber) -> {Topic, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)}. +subscribers(Topic) -> emqttd_pubsub:subscribers(Topic). + +-spec(is_subscribed(binary(), emqttd:subscriber()) -> boolean()). +is_subscribed(Topic, Subscriber) when is_binary(Topic) -> + ets:member(mqtt_subproperty, {Topic, Subscriber}). + +-spec(subscriber_down(emqttd:subscriber()) -> ok). subscriber_down(Subscriber) -> cast(pick(Subscriber), {subscriber_down, Subscriber}). diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index f27ba605b..2035ce3f6 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -297,7 +297,7 @@ handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), SubDict; {ok, OldQos} -> - emqttd_server:setqos(Topic, ClientId, Qos), + emqttd:setqos(Topic, ClientId, Qos), ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), dict:store(Topic, Qos, SubDict); error -> @@ -328,8 +328,8 @@ handle_cast({unsubscribe, Topics0}, Session = #session{client_id = ClientId, Subscriptions1 = lists:foldl( fun(Topic, SubDict) -> case dict:find(Topic, SubDict) of - {ok, Qos} -> - emqttd:unsubscribe(ClientId, Topic, Qos), + {ok, _Qos} -> + emqttd:unsubscribe(ClientId, Topic), dict:erase(Topic, SubDict); error -> SubDict @@ -532,8 +532,7 @@ handle_info(Info, Session) -> ?UNEXPECTED_INFO(Info, Session). terminate(_Reason, #session{client_id = ClientId}) -> - %%TODO: ... - emqttd_server:subscriber_down(ClientId), + emqttd:subscriber_down(ClientId), emqttd_sm:unregister_session(ClientId). code_change(_OldVsn, Session, _Extra) -> diff --git a/src/emqttd_topic.erl b/src/emqttd_topic.erl index 5e91ffa26..5ece2255b 100644 --- a/src/emqttd_topic.erl +++ b/src/emqttd_topic.erl @@ -16,17 +16,20 @@ -module(emqttd_topic). +-import(lists, [reverse/1]). -export([match/2, validate/1, triples/1, words/1, wildcard/1]). --export([join/1, feed_var/3, is_queue/1, systop/1]). +-export([join/1, feed_var/3, systop/1]). --type topic() :: binary(). +-export([strip/1, strip/2]). --type word() :: '' | '+' | '#' | binary(). +-type(topic() :: binary()). --type words() :: list(word()). +-type(word() :: '' | '+' | '#' | binary()). --type triple() :: {root | binary(), word(), binary()}. +-type(words() :: list(word())). + +-type(triple() :: {root | binary(), word(), binary()}). -export_type([topic/0, word/0, triple/0]). @@ -111,7 +114,7 @@ triples(Topic) when is_binary(Topic) -> triples(words(Topic), root, []). triples([], _Parent, Acc) -> - lists:reverse(Acc); + reverse(Acc); triples([W|Words], Parent, Acc) -> Node = join(Parent, W), @@ -137,13 +140,6 @@ word(<<"+">>) -> '+'; word(<<"#">>) -> '#'; word(Bin) -> Bin. -%% @doc Queue is a special topic name that starts with "$queue/" --spec(is_queue(topic()) -> boolean()). -is_queue(<<"$queue/", _Queue/binary>>) -> - true; -is_queue(_) -> - false. - %% @doc '$SYS' Topic. systop(Name) when is_atom(Name) -> list_to_binary(lists:concat(["$SYS/brokers/", node(), "/", Name])); @@ -155,7 +151,7 @@ systop(Name) when is_binary(Name) -> feed_var(Var, Val, Topic) -> feed_var(Var, Val, words(Topic), []). feed_var(_Var, _Val, [], Acc) -> - join(lists:reverse(Acc)); + join(reverse(Acc)); feed_var(Var, Val, [Var|Words], Acc) -> feed_var(Var, Val, Words, [Val|Acc]); feed_var(Var, Val, [W|Words], Acc) -> @@ -175,3 +171,28 @@ join(Words) -> end, {true, <<>>}, [bin(W) || W <- Words]), Bin. +-spec(strip(topic()) -> {topic(), [local | {share, binary()}]}). +strip(Topic) when is_binary(Topic) -> + strip(Topic, []). + +strip(Topic = <<"$local/", Topic1/binary>>, Options) -> + case lists:member(local, Options) of + true -> error({invalid_topic, Topic}); + false -> strip(Topic1, [local | Options]) + end; + +strip(Topic = <<"$queue/", Topic1/binary>>, Options) -> + case lists:keyfind(share, 1, Options) of + {share, _} -> error({invalid_topic, Topic}); + false -> strip(Topic1, [{share, '$queue'} | Options]) + end; + +strip(Topic = <<"$share/", Topic1/binary>>, Options) -> + case lists:keyfind(share, 1, Options) of + {share, _} -> error({invalid_topic, Topic}); + false -> [Share, Topic2] = binary:split(Topic1, <<"/">>), + {Topic2, [{share, Share} | Options]} + end; + +strip(Topic, Options) -> {Topic, Options}. + diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index afd5b28d1..93b1b380b 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -32,19 +32,16 @@ all() -> {group, metrics}, {group, stats}, {group, hook}, - {group, backend}, + %%{group, backend}, {group, cli}]. groups() -> [{protocol, [sequence], [mqtt_connect]}, {pubsub, [sequence], - [create_topic, - create_subscription, - subscribe_unsubscribe, + [subscribe_unsubscribe, publish, pubsub, - 'pubsub#', 'pubsub+', - pubsub_queue]}, + 'pubsub#', 'pubsub+']}, {router, [sequence], [router_add_del, router_print, @@ -65,7 +62,7 @@ groups() -> dispatch_retained_messages, expire_retained_messages]}, {backend, [sequence], - [backend_subscription]}, + []}, {cli, [sequence], [ctl_register_cmd, cli_status, @@ -115,26 +112,13 @@ connect_broker_(Packet, RecvSize) -> %% PubSub Test %%-------------------------------------------------------------------- -create_topic(_) -> - ok = emqttd:create(topic, <<"topic/create">>), - ok = emqttd:create(topic, <<"topic/create2">>), - [#mqtt_topic{topic = <<"topic/create">>, flags = [static]}] - = emqttd:lookup(topic, <<"topic/create">>). - -create_subscription(_) -> - ok = emqttd:create(subscription, {<<"clientId">>, <<"topic/sub">>, qos2}), - [#mqtt_subscription{subid = <<"clientId">>, topic = <<"topic/sub">>, qos = 2}] - = emqttd_backend:lookup_subscriptions(<<"clientId">>), - ok = emqttd_backend:del_subscriptions(<<"clientId">>), - ?assertEqual([], emqttd_backend:lookup_subscriptions(<<"clientId">>)). - subscribe_unsubscribe(_) -> - ok = emqttd:subscribe(<<"topic/subunsub">>), - ok = emqttd:subscribe(<<"clientId">>, <<"topic/subunsub1">>, 1), - ok = emqttd:subscribe(<<"clientId">>, <<"topic/subunsub2">>, 2), - ok = emqttd:unsubscribe(<<"topic/subunsub">>), - ok = emqttd:unsubscribe(<<"clientId">>, <<"topic/subunsub1">>, 1), - ok = emqttd:unsubscribe(<<"clientId">>, <<"topic/subunsub2">>, 2). + ok = emqttd:subscribe(<<"topic">>, <<"clientId">>), + ok = emqttd:subscribe(<<"topic/1">>, <<"clientId">>, [{qos, 1}]), + ok = emqttd:subscribe(<<"topic/2">>, <<"clientId">>, [{qos, 2}]), + ok = emqttd:unsubscribe(<<"topic">>, <<"clientId">>), + ok = emqttd:unsubscribe(<<"topic/1">>, <<"clientId">>), + ok = emqttd:unsubscribe(<<"topic/2">>, <<"clientId">>). publish(_) -> Msg = emqttd_message:make(ct, <<"test/pubsub">>, <<"hello">>), @@ -145,11 +129,11 @@ publish(_) -> pubsub(_) -> Self = self(), - emqttd:subscribe({<<"clientId">>, <<"a/b/c">>, 1}), - emqttd:subscribe({<<"clientId">>, <<"a/b/c">>, 2}), + ok = emqttd:subscribe(<<"a/b/c">>, Self, [{qos, 1}]), + ?assertMatch({error, _}, emqttd:subscribe(<<"a/b/c">>, Self, [{qos, 2}])), timer:sleep(10), - [{Self, <<"a/b/c">>}] = ets:lookup(subscribed, Self), - [{<<"a/b/c">>, Self}] = ets:lookup(subscriber, <<"a/b/c">>), + [{Self, <<"a/b/c">>}] = ets:lookup(mqtt_subscription, Self), + [{<<"a/b/c">>, Self}] = ets:lookup(mqtt_subscriber, <<"a/b/c">>), emqttd:publish(emqttd_message:make(ct, <<"a/b/c">>, <<"hello">>)), ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), spawn(fun() -> @@ -175,22 +159,6 @@ pubsub(_) -> ?assert(receive {dispatch, <<"a/+/+">>, _} -> true after 1 -> false end), emqttd:unsubscribe(<<"a/+/+">>). -pubsub_queue(_) -> - Self = self(), Q = <<"$queue/abc">>, - SubFun = fun() -> - emqttd:subscribe(Q), - timer:sleep(10), - {ok, Msgs} = loop_recv(Q, 10), - Self ! {recv, self(), Msgs} - end, - Sub1 = spawn(SubFun), Sub2 = spawn(SubFun), - timer:sleep(5), - emqttd:publish(emqttd_message:make(ct, Q, <<"1", Q/binary>>)), - emqttd:publish(emqttd_message:make(ct, Q, <<"2", Q/binary>>)), - emqttd:publish(emqttd_message:make(ct, Q, <<"3", Q/binary>>)), - ?assert(receive {recv, Sub1, Msgs1} -> length(Msgs1) < 3 end), - ?assert(receive {recv, Sub2, Msgs2} -> length(Msgs2) < 3 end). - loop_recv(Topic, Timeout) -> loop_recv(Topic, Timeout, []). @@ -215,15 +183,15 @@ router_add_del(_) -> #mqtt_route{topic = <<"#">>, node = node()}, #mqtt_route{topic = <<"+/#">>, node = node()}, #mqtt_route{topic = <<"a/b/c">>, node = node()}], - Routes = lists:sort(emqttd_router:lookup(<<"a/b/c">>)), + Routes = lists:sort(emqttd_router:match(<<"a/b/c">>)), %% Batch Add emqttd_router:add_routes(Routes), - Routes = lists:sort(emqttd_router:lookup(<<"a/b/c">>)), + Routes = lists:sort(emqttd_router:match(<<"a/b/c">>)), %% Del emqttd_router:del_route(<<"a/b/c">>), - [R1, R2] = lists:sort(emqttd_router:lookup(<<"a/b/c">>)), + [R1, R2] = lists:sort(emqttd_router:match(<<"a/b/c">>)), {atomic, []} = mnesia:transaction(fun emqttd_trie:lookup/1, [<<"a/b/c">>]), %% Batch Del @@ -231,7 +199,7 @@ router_add_del(_) -> emqttd_router:add_route(R3), emqttd_router:del_routes([R1, R2]), emqttd_router:del_route(R3), - [] = lists:sort(emqttd_router:lookup(<<"a/b/c">>)). + [] = lists:sort(emqttd_router:match(<<"a/b/c">>)). router_print(_) -> Routes = [#mqtt_route{topic = <<"a/b/c">>, node = node()}, @@ -360,20 +328,6 @@ expire_retained_messages(_) -> emqttd_backend:expire_messages(emqttd_time:now_to_secs()), 0 = emqttd_backend:retained_count(). -%%-------------------------------------------------------------------- -%% Backend Test -%%-------------------------------------------------------------------- - -backend_subscription(_) -> - Sub1 = #mqtt_subscription{subid = <<"clientId">>, topic = <<"topic">>, qos = 2}, - Sub2 = #mqtt_subscription{subid = <<"clientId">>, topic = <<"#">>, qos = 2}, - emqttd_backend:add_subscription(Sub1), - emqttd_backend:add_subscription(Sub2), - [Sub1, Sub2] = emqttd_backend:lookup_subscriptions(<<"clientId">>), - emqttd_backend:del_subscription(<<"clientId">>, <<"topic">>), - [Sub2] = emqttd_backend:lookup_subscriptions(<<"clientId">>), - emqttd_backend:del_subscriptions(<<"clientId">>), - [] = emqttd_backend:lookup_subscriptions(<<"clientId">>). %%-------------------------------------------------------------------- %% CLI Group diff --git a/test/emqttd_backend_SUITE.erl b/test/emqttd_backend_SUITE.erl deleted file mode 100644 index ceb1b3d6e..000000000 --- a/test/emqttd_backend_SUITE.erl +++ /dev/null @@ -1,45 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_backend_SUITE). - --include("emqttd.hrl"). - --compile(export_all). - -all() -> [{group, subscription}]. - -groups() -> [{subscription, [], [add_del_subscription]}]. - -init_per_suite(Config) -> - ok = emqttd_mnesia:ensure_started(), - emqttd_backend:mnesia(boot), - emqttd_backend:mnesia(copy), - Config. - -end_per_suite(_Config) -> - emqttd_mnesia:ensure_stopped(). - -add_del_subscription(_) -> - Sub1 = #mqtt_subscription{subid = <<"clientId">>, topic = <<"topic">>, qos = 2}, - Sub2 = #mqtt_subscription{subid = <<"clientId">>, topic = <<"topic">>, qos = 1}, - ok = emqttd_backend:add_subscription(Sub1), - {error, already_existed} = emqttd_backend:add_subscription(Sub1), - ok = emqttd_backend:add_subscription(Sub2), - [Sub2] = emqttd_backend:lookup_subscriptions(<<"clientId">>), - emqttd_backend:del_subscription(<<"clientId">>, <<"topic">>), - [] = emqttd_backend:lookup_subscriptions(<<"clientId">>). - diff --git a/test/emqttd_topic_SUITE.erl b/test/emqttd_topic_SUITE.erl index abcf50cf9..5e9608e00 100644 --- a/test/emqttd_topic_SUITE.erl +++ b/test/emqttd_topic_SUITE.erl @@ -16,18 +16,20 @@ -module(emqttd_topic_SUITE). +-include_lib("eunit/include/eunit.hrl"). + %% CT -compile(export_all). -import(emqttd_topic, [wildcard/1, match/2, validate/1, triples/1, join/1, - words/1, systop/1, is_queue/1, feed_var/3]). + words/1, systop/1, feed_var/3, strip/1, strip/2]). -define(N, 10000). all() -> [t_wildcard, t_match, t_match2, t_validate, t_triples, t_join, - t_words, t_systop, t_is_queue, t_feed_var, t_sys_match, 't_#_match', + t_words, t_systop, t_feed_var, t_sys_match, 't_#_match', t_sigle_level_validate, t_sigle_level_match, t_match_perf, - t_triples_perf]. + t_triples_perf, t_strip]. t_wildcard(_) -> true = wildcard(<<"a/b/#">>), @@ -155,21 +157,25 @@ t_join(_) -> <<"/ab/cd/ef/">> = join(words(<<"/ab/cd/ef/">>)), <<"ab/+/#">> = join(words(<<"ab/+/#">>)). -t_is_queue(_) -> - true = is_queue(<<"$queue/queue">>), - false = is_queue(<<"xyz/queue">>). - t_systop(_) -> SysTop1 = iolist_to_binary(["$SYS/brokers/", atom_to_list(node()), "/xyz"]), - SysTop1 = systop('xyz'), + ?assertEqual(SysTop1, systop('xyz')), SysTop2 = iolist_to_binary(["$SYS/brokers/", atom_to_list(node()), "/abc"]), - SysTop2 = systop(<<"abc">>). + ?assertEqual(SysTop2,systop(<<"abc">>)). t_feed_var(_) -> - <<"$queue/client/clientId">> = feed_var(<<"$c">>, <<"clientId">>, <<"$queue/client/$c">>), - <<"username/test/client/x">> = feed_var(<<"%u">>, <<"test">>, <<"username/%u/client/x">>), - <<"username/test/client/clientId">> = feed_var(<<"%c">>, <<"clientId">>, <<"username/test/client/%c">>). + ?assertEqual(<<"$queue/client/clientId">>, feed_var(<<"$c">>, <<"clientId">>, <<"$queue/client/$c">>)), + ?assertEqual(<<"username/test/client/x">>, feed_var(<<"%u">>, <<"test">>, <<"username/%u/client/x">>)), + ?assertEqual(<<"username/test/client/clientId">>, feed_var(<<"%c">>, <<"clientId">>, <<"username/test/client/%c">>)). long_topic() -> iolist_to_binary([[integer_to_list(I), "/"] || I <- lists:seq(0, 10000)]). +t_strip(_) -> + ?assertEqual({<<"a/b/+/#">>, []}, strip(<<"a/b/+/#">>)), + ?assertEqual({<<"topic">>, [{share, '$queue'}]}, strip(<<"$queue/topic">>)), + ?assertEqual({<<"topic">>, [{share, <<"group">>}]}, strip(<<"$share/group/topic">>)), + ?assertEqual({<<"topic">>, [local]}, strip(<<"$local/topic">>)), + ?assertEqual({<<"topic">>, [{share, '$queue'}, local]}, strip(<<"$local/$queue/topic">>)), + ?assertEqual({<<"/a/b/c">>, [{share, <<"group">>}, local]}, strip(<<"$local/$share/group//a/b/c">>)). + From dd9217bf731e084e8a518abc93fe798764be1be9 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 12 Aug 2016 16:26:36 +0800 Subject: [PATCH 073/116] fix cli --- src/emqttd_cli.erl | 33 +++++++++++++++++---------------- src/emqttd_server.erl | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 6c1907f2a..94c063c54 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -172,11 +172,13 @@ if_client(ClientId, Fun) -> sessions(["list"]) -> dump(mqtt_local_session); +%% performance issue? sessions(["list", "persistent"]) -> - dump(mqtt_persistent_session); + lists:foreach(fun print/1, ets:match_object(mqtt_local_session, {'_', false, '_', '_'})); +%% performance issue? sessions(["list", "transient"]) -> - dump(mqtt_transient_session); + lists:foreach(fun print/1, ets:match_object(mqtt_local_session, {'_', true, '_', '_'})); sessions(["show", ClientId]) -> case ets:lookup(mqtt_local_session, bin(ClientId)) of @@ -193,10 +195,10 @@ sessions(_) -> %%-------------------------------------------------------------------- %% @doc Routes Command routes(["list"]) -> - if_could_print(route, fun print/1); + if_could_print(mqtt_route, fun print/1); routes(["show", Topic]) -> - print(mnesia:dirty_read(route, bin(Topic))); + print(mnesia:dirty_read(mqtt_route, bin(Topic))); routes(_) -> ?USAGE([{"routes list", "List all routes"}, @@ -205,23 +207,24 @@ routes(_) -> %%-------------------------------------------------------------------- %% @doc Topics Command topics(["list"]) -> - if_could_print(topic, fun print/1); + lists:foreach(fun(Topic) -> ?PRINT("~s~n", [Topic]) end, emqttd:topics()); topics(["show", Topic]) -> - print(mnesia:dirty_read(topic, bin(Topic))); + print(mnesia:dirty_read(mqtt_route, bin(Topic))); topics(_) -> ?USAGE([{"topics list", "List all topics"}, {"topics show ", "Show a topic"}]). subscriptions(["list"]) -> - if_could_print(subscription, fun print/1); - -subscriptions(["list", "static"]) -> - if_could_print(backend_subscription, fun print/1); + lists:foreach(fun({Sub, Topic, Opts}) when is_pid(Sub) -> + ?PRINT("~p -> ~s: ~p~n", [Sub, Topic, Opts]); + ({Sub, Topic, Opts}) -> + ?PRINT("~s -> ~s: ~p~n", [Sub, Topic, Opts]) + end, emqttd:subscriptions()); subscriptions(["show", ClientId]) -> - case mnesia:dirty_read(subscription, bin(ClientId)) of + case mnesia:dirty_read(mqtt_subscription, bin(ClientId)) of [] -> ?PRINT_MSG("Not Found.~n"); Records -> print(Records) end; @@ -252,7 +255,6 @@ subscriptions(["del", ClientId, Topic]) -> subscriptions(_) -> ?USAGE([{"subscriptions list", "List all subscriptions"}, - {"subscriptions list static", "List all static subscriptions"}, {"subscriptions show ", "Show subscriptions of a client"}, {"subscriptions add ", "Add a static subscription manually"}, {"subscriptions del ", "Delete static subscriptions manually"}, @@ -509,9 +511,8 @@ print(#mqtt_topic{topic = Topic, flags = Flags}) -> print(#mqtt_route{topic = Topic, node = Node}) -> ?PRINT("~s -> ~s~n", [Topic, Node]); -print({{ClientId, _ClientPid}, SessInfo}) -> - InfoKeys = [clean_sess, - max_inflight, +print({ClientId, _ClientPid, CleanSess, SessInfo}) -> + InfoKeys = [max_inflight, inflight_queue, message_queue, message_dropped, @@ -523,7 +524,7 @@ print({{ClientId, _ClientPid}, SessInfo}) -> "message_queue=~w, message_dropped=~w, " "awaiting_rel=~w, awaiting_ack=~w, awaiting_comp=~w, " "created_at=~w)~n", - [ClientId | [format(Key, get_value(Key, SessInfo)) || Key <- InfoKeys]]). + [ClientId, CleanSess | [format(Key, get_value(Key, SessInfo)) || Key <- InfoKeys]]). format(created_at, Val) -> emqttd_time:now_to_secs(Val); diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl index 567d49388..345b181f5 100644 --- a/src/emqttd_server.erl +++ b/src/emqttd_server.erl @@ -139,7 +139,7 @@ subscriptions(Subscriber) -> end, ets:lookup(mqtt_subscription, Subscriber)). subscription(Topic, Subscriber) -> - {Topic, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)}. + {Topic, Subscriber, ets:lookup_element(mqtt_subproperty, {Topic, Subscriber}, 2)}. subscribers(Topic) -> emqttd_pubsub:subscribers(Topic). From 6e64686f77f00fa8e8de2aa2e993c28a836471f0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 12 Aug 2016 16:49:50 +0800 Subject: [PATCH 074/116] pass test cases first --- src/emqttd_cli.erl | 65 ++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 94c063c54..e338e09a6 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -217,41 +217,45 @@ topics(_) -> {"topics show ", "Show a topic"}]). subscriptions(["list"]) -> - lists:foreach(fun({Sub, Topic, Opts}) when is_pid(Sub) -> - ?PRINT("~p -> ~s: ~p~n", [Sub, Topic, Opts]); - ({Sub, Topic, Opts}) -> - ?PRINT("~s -> ~s: ~p~n", [Sub, Topic, Opts]) - end, emqttd:subscriptions()); + lists:foreach(fun(Subscription) -> + print(subscription, Subscription) + end, []); %%emqttd:subscriptions()); subscriptions(["show", ClientId]) -> - case mnesia:dirty_read(mqtt_subscription, bin(ClientId)) of + case ets:lookup(mqtt_subscription, bin(ClientId)) of [] -> ?PRINT_MSG("Not Found.~n"); - Records -> print(Records) + Records -> [print(subscription, Subscription) || Subscription <- Records] end; -subscriptions(["add", ClientId, Topic, QoS]) -> - Add = fun(IntQos) -> - Subscription = #mqtt_subscription{subid = bin(ClientId), - topic = bin(Topic), - qos = IntQos}, - case emqttd_backend:add_subscription(Subscription) of - ok -> - ?PRINT_MSG("ok~n"); - {error, already_existed} -> - ?PRINT_MSG("Error: already existed~n"); - {error, Reason} -> - ?PRINT("Error: ~p~n", [Reason]) - end - end, - if_valid_qos(QoS, Add); +%% +%% subscriptions(["add", ClientId, Topic, QoS]) -> +%% Add = fun(IntQos) -> +%% Subscription = #mqtt_subscription{subid = bin(ClientId), +%% topic = bin(Topic), +%% qos = IntQos}, +%% case emqttd_backend:add_subscription(Subscription) of +%% ok -> +%% ?PRINT_MSG("ok~n"); +%% {error, already_existed} -> +%% ?PRINT_MSG("Error: already existed~n"); +%% {error, Reason} -> +%% ?PRINT("Error: ~p~n", [Reason]) +%% end +%% end, +%% if_valid_qos(QoS, Add); +%% -subscriptions(["del", ClientId]) -> - Ok = emqttd_backend:del_subscriptions(bin(ClientId)), - ?PRINT("~p~n", [Ok]); +%% +%% subscriptions(["del", ClientId]) -> +%% Ok = emqttd_backend:del_subscriptions(bin(ClientId)), +%% ?PRINT("~p~n", [Ok]); +%% -subscriptions(["del", ClientId, Topic]) -> - Ok = emqttd_backend:del_subscription(bin(ClientId), bin(Topic)), - ?PRINT("~p~n", [Ok]); +%% +%% subscriptions(["del", ClientId, Topic]) -> +%% Ok = emqttd_backend:del_subscription(bin(ClientId), bin(Topic)), +%% ?PRINT("~p~n", [Ok]); +%% subscriptions(_) -> ?USAGE([{"subscriptions list", "List all subscriptions"}, @@ -526,6 +530,11 @@ print({ClientId, _ClientPid, CleanSess, SessInfo}) -> "created_at=~w)~n", [ClientId, CleanSess | [format(Key, get_value(Key, SessInfo)) || Key <- InfoKeys]]). +print(subscription, {Sub, Topic, Opts}) when is_pid(Sub) -> + ?PRINT("~p -> ~s: ~p~n", [Sub, Topic, Opts]); +print(subscription, {Sub, Topic, Opts}) -> + ?PRINT("~s -> ~s: ~p~n", [Sub, Topic, Opts]). + format(created_at, Val) -> emqttd_time:now_to_secs(Val); From 0a967df15ae2672407deb6b79d0ac253bef29322 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 12 Aug 2016 22:23:29 +0800 Subject: [PATCH 075/116] do_subscribe_/4, do_unsubscribe_/3 --- src/emqttd_pubsub.erl | 12 ++++-------- src/emqttd_server.erl | 22 +++++++++++----------- src/emqttd_session.erl | 24 +++++++++++++++--------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index eb4722bb8..036b345aa 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -162,18 +162,14 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- add_subscriber_(Topic, Subscriber) -> - case ets:member(mqtt_subscriber, Topic) of - false -> emqttd_router:add_route(Topic, node()); - true -> ok - end, + (not ets:member(mqtt_subscriber, Topic)) + andalso emqttd_router:add_route(Topic), ets:insert(mqtt_subscriber, {Topic, Subscriber}). del_subscriber_(Topic, Subscriber) -> ets:delete_object(mqtt_subscriber, {Topic, Subscriber}), - case ets:member(mqtt_subscriber, Topic) of - false -> emqttd_router:del_route(Topic, node()); - true -> ok - end. + (not ets:member(mqtt_subscriber, Topic)) + andalso emqttd_router:del_route(Topic). setstats(State) -> emqttd_stats:setstats('subscribers/count', 'subscribers/max', diff --git a/src/emqttd_server.erl b/src/emqttd_server.erl index 345b181f5..e9b2d0eb5 100644 --- a/src/emqttd_server.erl +++ b/src/emqttd_server.erl @@ -172,13 +172,13 @@ init([Pool, Id, Env]) -> {ok, #state{pool = Pool, id = Id, env = Env, submon = emqttd_pmon:new()}}. handle_call({subscribe, Topic, Subscriber, Options}, _From, State) -> - case subscribe_(Topic, Subscriber, Options, State) of + case do_subscribe_(Topic, Subscriber, Options, State) of {ok, NewState} -> {reply, ok, setstats(NewState)}; {error, Error} -> {reply, {error, Error}, State} end; handle_call({unsubscribe, Topic, Subscriber}, _From, State) -> - case unsubscribe_(Topic, Subscriber, State) of + case do_unsubscribe_(Topic, Subscriber, State) of {ok, NewState} -> {reply, ok, setstats(NewState), hibernate}; {error, Error} -> {reply, {error, Error}, State} end; @@ -198,13 +198,13 @@ handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). handle_cast({subscribe, Topic, Subscriber, Options}, State) -> - case subscribe_(Topic, Subscriber, Options, State) of + case do_subscribe_(Topic, Subscriber, Options, State) of {ok, NewState} -> {noreply, setstats(NewState)}; {error, _Error} -> {noreply, State} end; handle_cast({unsubscribe, Topic, Subscriber}, State) -> - case unsubscribe_(Topic, Subscriber, State) of + case do_unsubscribe_(Topic, Subscriber, State) of {ok, NewState} -> {noreply, setstats(NewState), hibernate}; {error, _Error} -> {noreply, State} end; @@ -233,7 +233,7 @@ code_change(_OldVsn, State, _Extra) -> %% Internal Functions %%-------------------------------------------------------------------- -subscribe_(Topic, Subscriber, Options, State) -> +do_subscribe_(Topic, Subscriber, Options, State) -> case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of [] -> emqttd_pubsub:async_subscribe(Topic, Subscriber), @@ -244,7 +244,12 @@ subscribe_(Topic, Subscriber, Options, State) -> {error, {already_subscribed, Topic}} end. -unsubscribe_(Topic, Subscriber, State) -> +monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> + State#state{submon = PMon:monitor(SubPid)}; +monitor_subpid(_SubPid, State) -> + State. + +do_unsubscribe_(Topic, Subscriber, State) -> case ets:lookup(mqtt_subproperty, {Topic, Subscriber}) of [_] -> emqttd_pubsub:async_unsubscribe(Topic, Subscriber), @@ -258,11 +263,6 @@ unsubscribe_(Topic, Subscriber, State) -> {error, {subscription_not_found, Topic}} end. -monitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> - State#state{submon = PMon:monitor(SubPid)}; -monitor_subpid(_SubPid, State) -> - State. - demonitor_subpid(SubPid, State = #state{submon = PMon}) when is_pid(SubPid) -> State#state{submon = PMon:demonitor(SubPid)}; demonitor_subpid(_SubPid, State) -> diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 2035ce3f6..9345071dc 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -284,14 +284,18 @@ handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PktId}}, handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ClientId, - subscriptions = Subscriptions}) -> - +handle_cast({subscribe, RawTopicTable, AckFun}, Session = #session{client_id = ClientId, + subscriptions = Subscriptions}) -> + %% TODO: Ugly... + TopicTable0 = lists:map(fun({T, Q}) -> + {T1, Opts} = emqttd_topic:strip(T), + {T1, [{qos, Q} | Opts]} + end, RawTopicTable), case emqttd:run_hooks('client.subscribe', [ClientId], TopicTable0) of {ok, TopicTable} -> ?LOG(info, "Subscribe ~p", [TopicTable], Session), Subscriptions1 = lists:foldl( - fun({Topic, Qos}, SubDict) -> + fun({Topic, Opts = [{qos, Qos}|_]}, SubDict) -> case dict:find(Topic, SubDict) of {ok, Qos} -> ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), @@ -301,7 +305,7 @@ handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), dict:store(Topic, Qos, SubDict); error -> - emqttd:subscribe(Topic, ClientId, [{qos, Qos}]), + emqttd:subscribe(Topic, ClientId, Opts), %%TODO: the design is ugly... %% : 3.8.4 %% Where the Topic Filter is not identical to any existing Subscription’s filter, @@ -319,9 +323,11 @@ handle_cast({subscribe, TopicTable0, AckFun}, Session = #session{client_id = hibernate(Session) end; -handle_cast({unsubscribe, Topics0}, Session = #session{client_id = ClientId, - subscriptions = Subscriptions}) -> - +handle_cast({unsubscribe, RawTopics}, Session = #session{client_id = ClientId, + subscriptions = Subscriptions}) -> + Topics0 = lists:map(fun(Topic) -> + {T, _Opts} = emqttd_topic:strip(Topic), T + end, RawTopics), case emqttd:run_hooks('client.unsubscribe', [ClientId], Topics0) of {ok, Topics} -> ?LOG(info, "unsubscribe ~p", [Topics], Session), @@ -329,7 +335,7 @@ handle_cast({unsubscribe, Topics0}, Session = #session{client_id = ClientId, fun(Topic, SubDict) -> case dict:find(Topic, SubDict) of {ok, _Qos} -> - emqttd:unsubscribe(ClientId, Topic), + emqttd:unsubscribe(Topic, ClientId), dict:erase(Topic, SubDict); error -> SubDict From 1266b4e86011ac42f78bb44d69dae15f5acdac03 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 12 Aug 2016 22:39:13 +0800 Subject: [PATCH 076/116] ack qos --- src/emqttd_session.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 9345071dc..55eb08b11 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -315,7 +315,7 @@ handle_cast({subscribe, RawTopicTable, AckFun}, Session = #session{client_id dict:store(Topic, Qos, SubDict) end end, Subscriptions, TopicTable), - AckFun([Qos || {_, Qos} <- TopicTable]), + AckFun([Qos || {_, Qos} <- RawTopicTable]), emqttd:run_hooks('client.subscribe.after', [ClientId], TopicTable), hibernate(Session#session{subscriptions = Subscriptions1}); {stop, TopicTable} -> From bc5cfb4b36479c11f77fa3af9b2068b48908cc4a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 13 Aug 2016 11:02:39 +0800 Subject: [PATCH 077/116] bridge sup --- src/emqttd.erl | 11 +++++++++ src/emqttd_app.erl | 2 +- src/emqttd_bridge_sup.erl | 47 +++++++-------------------------------- src/emqttd_pubsub_sup.erl | 10 +-------- 4 files changed, 21 insertions(+), 49 deletions(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index 521251efc..252d77d78 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -35,6 +35,9 @@ %% Hooks API -export([hook/4, hook/3, unhook/2, run_hooks/3]). +%% Adapter +-export([adapter/1]). + %% Debug API -export([dump/0]). @@ -157,6 +160,14 @@ unhook(Hook, Function) -> run_hooks(Hook, Args, Acc) -> emqttd_hook:run(Hook, Args, Acc). +%%-------------------------------------------------------------------- +%% Adapter +%%-------------------------------------------------------------------- + +adapter(server) -> env(pubsub_server, emqttd_server); +adapter(pubsub) -> env(pubsub_adapter, emqttd_pubsub); +adapter(bridge) -> env(bridge_adapter, emqttd_bridge). + %%-------------------------------------------------------------------- %% Debug %%-------------------------------------------------------------------- diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 81640c884..011bd8fe5 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -91,7 +91,7 @@ start_servers(Sup) -> {"emqttd broker", emqttd_broker}, {"emqttd alarm", emqttd_alarm}, {"emqttd mod supervisor", emqttd_mod_sup}, - {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup}}, + {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup_sup}}, {"emqttd access control", emqttd_access_control}, {"emqttd system monitor", {supervisor, emqttd_sysmon_sup}}], [start_server(Sup, Server) || Server <- Servers]. diff --git a/src/emqttd_bridge_sup.erl b/src/emqttd_bridge_sup.erl index dca66a8b6..e808ee72a 100644 --- a/src/emqttd_bridge_sup.erl +++ b/src/emqttd_bridge_sup.erl @@ -18,56 +18,25 @@ -behavior(supervisor). --export([start_link/0, bridges/0, start_bridge/2, start_bridge/3, stop_bridge/2]). +-export([start_link/3]). -export([init/1]). --define(BRIDGE_ID(Node, Topic), {bridge, Node, Topic}). - %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- %% @doc Start bridge supervisor -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% @doc List all bridges --spec(bridges() -> [{tuple(), pid()}]). -bridges() -> - [{{Node, Topic}, Pid} || {?BRIDGE_ID(Node, Topic), Pid, worker, _} - <- supervisor:which_children(?MODULE)]. - -%% @doc Start a bridge --spec(start_bridge(atom(), binary()) -> {ok, pid()} | {error, any()}). -start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> - start_bridge(Node, Topic, []). - --spec(start_bridge(atom(), binary(), [emqttd_bridge:option()]) -> {ok, pid()} | {error, any()}). -start_bridge(Node, _Topic, _Options) when Node =:= node() -> - {error, bridge_to_self}; -start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> - Options1 = emqttd_opts:merge(emqttd_conf:bridge(), Options), - supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). - -%% @doc Stop a bridge --spec(stop_bridge(atom(), binary()) -> {ok, pid()} | ok). -stop_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> - ChildId = ?BRIDGE_ID(Node, Topic), - case supervisor:terminate_child(?MODULE, ChildId) of - ok -> supervisor:delete_child(?MODULE, ChildId); - Error -> Error - end. +-spec(start_link(atom(), binary(), [emqttd_bridge:option()]) -> {ok, pid()} | {error, any()}). +start_link(Node, Topic, Options) -> + supervisor:start_link(?MODULE, [Node, Topic, Options]). %%-------------------------------------------------------------------- %% Supervisor callbacks %%-------------------------------------------------------------------- -init([]) -> - {ok, {{one_for_one, 10, 100}, []}}. - -bridge_spec(Node, Topic, Options) -> - ChildId = ?BRIDGE_ID(Node, Topic), - {ChildId, {emqttd_bridge, start_link, [Node, Topic, Options]}, - transient, 10000, worker, [emqttd_bridge]}. +init([Node, Topic, Options]) -> + {ok, {{one_for_all, 10, 100}, + [{bridge, {emqttd_bridge, start_link, [Node, Topic, Options]}, + transient, 10000, worker, [emqttd_bridge]}]}}. diff --git a/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl index 58143cc83..f87f87d48 100644 --- a/src/emqttd_pubsub_sup.erl +++ b/src/emqttd_pubsub_sup.erl @@ -57,17 +57,9 @@ pool_size(Env) -> pool_sup(Name, Env) -> Pool = list_to_atom(atom_to_list(Name) ++ "_pool"), - MFA = {adapter(Name), start_link, [Env]}, + MFA = {emqttd:adapter(Name), start_link, [Env]}, emqttd_pool_sup:spec(Pool, [Name, hash, pool_size(Env), MFA]). -%%-------------------------------------------------------------------- -%% Adapter -%%-------------------------------------------------------------------- - -adapter(server) -> - emqttd:env(pubsub_server, emqttd_server); -adapter(pubsub) -> - emqttd:env(pubsub_adapter, emqttd_pubsub). %%-------------------------------------------------------------------- %% Create PubSub Tables From 8ca3430e390eff0aae6c939c057dac50fe189fff Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 13 Aug 2016 11:03:14 +0800 Subject: [PATCH 078/116] improve the design of bridge --- src/emqttd_bridge_sup_sup.erl | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/emqttd_bridge_sup_sup.erl diff --git a/src/emqttd_bridge_sup_sup.erl b/src/emqttd_bridge_sup_sup.erl new file mode 100644 index 000000000..475149afe --- /dev/null +++ b/src/emqttd_bridge_sup_sup.erl @@ -0,0 +1,76 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2012-2016 Feng Lee . +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqttd_bridge_sup_sup). + +-behavior(supervisor). + +-export([start_link/0, bridges/0, start_bridge/2, start_bridge/3, stop_bridge/2]). + +-export([init/1]). + +-define(CHILD_ID(Node, Topic), {bridge_sup, Node, Topic}). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +%% @doc List all bridges +-spec(bridges() -> [{node(), binary(), pid()}]). +bridges() -> + [{Node, Topic, Pid} || {?CHILD_ID(Node, Topic), Pid, supervisor, _} + <- supervisor:which_children(?MODULE)]. + +%% @doc Start a bridge +-spec(start_bridge(atom(), binary()) -> {ok, pid()} | {error, any()}). +start_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> + start_bridge(Node, Topic, []). + +-spec(start_bridge(atom(), binary(), [emqttd_bridge:option()]) -> {ok, pid()} | {error, any()}). +start_bridge(Node, _Topic, _Options) when Node =:= node() -> + {error, bridge_to_self}; +start_bridge(Node, Topic, Options) when is_atom(Node) andalso is_binary(Topic) -> + Options1 = emqttd_opts:merge(emqttd_conf:bridge(), Options), + supervisor:start_child(?MODULE, bridge_spec(Node, Topic, Options1)). + +%% @doc Stop a bridge +-spec(stop_bridge(atom(), binary()) -> {ok, pid()} | ok). +stop_bridge(Node, Topic) when is_atom(Node) andalso is_binary(Topic) -> + ChildId = ?CHILD_ID(Node, Topic), + case supervisor:terminate_child(?MODULE, ChildId) of + ok -> supervisor:delete_child(?MODULE, ChildId); + Error -> Error + end. + +%%-------------------------------------------------------------------- +%% Supervisor callbacks +%%-------------------------------------------------------------------- + +init([]) -> + {ok, {{one_for_one, 10, 100}, []}}. + +bridge_spec(Node, Topic, Options) -> + SupMod = sup_mod(emqttd:adapter(bridge)), + {?CHILD_ID(Node, Topic), + {SupMod, start_link, [Node, Topic, Options]}, + permanent, infinity, supervisor, [SupMod]}. + +sup_mod(Adaper) -> + list_to_atom(atom_to_list(Adaper) ++ "_sup"). + From d8aa7d9cfa798d18f80585e4f130be004c2cd975 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 13 Aug 2016 11:24:06 +0800 Subject: [PATCH 079/116] emqttd_bridge_sup_sup --- src/emqttd_cli.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index e338e09a6..6c77ddcd5 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -310,7 +310,7 @@ plugins(_) -> bridges(["list"]) -> foreach(fun({{Node, Topic}, _Pid}) -> ?PRINT("bridge: ~s--~s-->~s~n", [node(), Topic, Node]) - end, emqttd_bridge_sup:bridges()); + end, emqttd_bridge_sup_sup:bridges()); bridges(["options"]) -> ?PRINT_MSG("Options:~n"), @@ -322,20 +322,20 @@ bridges(["options"]) -> ?PRINT_MSG(" qos=2,prefix=abc/,suffix=/yxz,queue=1000~n"); bridges(["start", SNode, Topic]) -> - case emqttd_bridge_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic)) of + case emqttd_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic)) of {ok, _} -> ?PRINT_MSG("bridge is started.~n"); {error, Error} -> ?PRINT("error: ~p~n", [Error]) end; bridges(["start", SNode, Topic, OptStr]) -> Opts = parse_opts(bridge, OptStr), - case emqttd_bridge_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic), Opts) of + case emqttd_bridge_sup_sup:start_bridge(list_to_atom(SNode), list_to_binary(Topic), Opts) of {ok, _} -> ?PRINT_MSG("bridge is started.~n"); {error, Error} -> ?PRINT("error: ~p~n", [Error]) end; bridges(["stop", SNode, Topic]) -> - case emqttd_bridge_sup:stop_bridge(list_to_atom(SNode), list_to_binary(Topic)) of + case emqttd_bridge_sup_sup:stop_bridge(list_to_atom(SNode), list_to_binary(Topic)) of ok -> ?PRINT_MSG("bridge is stopped.~n"); {error, Error} -> ?PRINT("error: ~p~n", [Error]) end; From f166143cd07e11ed1a7c597f313f0b8772e56e7d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 13 Aug 2016 12:21:18 +0800 Subject: [PATCH 080/116] Don't register name for pool_sup --- src/emqttd_pool_sup.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/emqttd_pool_sup.erl b/src/emqttd_pool_sup.erl index 0f16df67c..b47199cb0 100644 --- a/src/emqttd_pool_sup.erl +++ b/src/emqttd_pool_sup.erl @@ -41,10 +41,10 @@ start_link(Pool, Type, MFA) -> -spec(start_link(atom(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, any()}). start_link(Pool, Type, Size, MFA) -> - supervisor:start_link({local, sup_name(Pool)}, ?MODULE, [Pool, Type, Size, MFA]). + supervisor:start_link(?MODULE, [Pool, Type, Size, MFA]). -sup_name(Pool) when is_atom(Pool) -> - list_to_atom(atom_to_list(Pool) ++ "_pool_sup"). +%% sup_name(Pool) when is_atom(Pool) -> +%% list_to_atom(atom_to_list(Pool) ++ "_pool_sup"). init([Pool, Type, Size, {M, F, Args}]) -> ensure_pool(Pool, Type, [{size, Size}]), From 78b305ff85b508ed20c54637c9dc75c4f6842d5f Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 14 Aug 2016 20:26:52 +0800 Subject: [PATCH 081/116] mnesia -> ets --- src/emqttd_cli.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 94c063c54..b4f05be27 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -224,7 +224,7 @@ subscriptions(["list"]) -> end, emqttd:subscriptions()); subscriptions(["show", ClientId]) -> - case mnesia:dirty_read(mqtt_subscription, bin(ClientId)) of + case ets:dirty_read(mqtt_subscription, bin(ClientId)) of [] -> ?PRINT_MSG("Not Found.~n"); Records -> print(Records) end; From 8c1af879d352369a5fb3657055411ff86e158f40 Mon Sep 17 00:00:00 2001 From: huangdan Date: Mon, 15 Aug 2016 11:29:12 +0800 Subject: [PATCH 082/116] emqttd_app eunit --- src/emqttd_app.erl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 011bd8fe5..899258c7b 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -159,8 +159,7 @@ load_mod({module, Name, Opts}) -> %% @doc Is module enabled? -spec(is_mod_enabled(Name :: atom()) -> boolean()). -is_mod_enabled(Name) -> - lists:keyfind(Name, 2, gen_conf:list(emqttd, module)). +is_mod_enabled(Name) -> lists:keyfind(Name, 2, gen_conf:list(emqttd, module)). %%-------------------------------------------------------------------- %% Start Listeners @@ -206,3 +205,18 @@ stop_listeners() -> lists:foreach(fun stop_listener/1, gen_conf:list(listener)). %% @private stop_listener({listener, Protocol, ListenOn, _Opts}) -> esockd:close(Protocol, ListenOn). +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +merge_sockopts_test_() -> + Opts = [{acceptors, 16}, {max_clients, 512}], + ?_assert(merge_sockopts(Opts) == [{sockopts, [binary, {packet, raw}, {reuseaddr, true}, + {backlog, 512}, {nodelay, true}]}, {acceptors, 16}, {max_clients, 512}]). + +load_all_mods_test_() -> + ?_assert(load_all_mods() == ok). + +is_mod_enabled_test_() -> + ?_assert(is_mod_enabled(presence) == {module, presence, [{qos, 0}]}), + ?_assert(is_mod_enabled(test) == false). + +-endif. From c5e88ab1a4993c5098324209702f8f237ad1d945 Mon Sep 17 00:00:00 2001 From: huangdan Date: Mon, 15 Aug 2016 15:36:52 +0800 Subject: [PATCH 083/116] emqttd_ctl eunit --- src/emqttd_ctl.erl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/emqttd_ctl.erl b/src/emqttd_ctl.erl index 2f406bef0..1cd5d3055 100644 --- a/src/emqttd_ctl.erl +++ b/src/emqttd_ctl.erl @@ -133,3 +133,20 @@ noreply(State) -> next_seq(State = #state{seq = Seq}) -> State#state{seq = Seq + 1}. +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +register_cmd_test_() -> + {setup, + fun() -> + {ok, InitState} = emqttd_ctl:init([]), + InitState + end, + fun(State) -> + ok = emqttd_ctl:terminate(shutdown, State) + end, + fun(State = #state{seq = Seq}) -> + emqttd_ctl:handle_cast({register_cmd, test0, {?MODULE, test0}, []}, State), + [?_assertMatch([{{0,test0},{?MODULE, test0}, []}], ets:lookup(?CMD_TAB, {Seq,test0}))] + end + }. +-endif. From 123f9b444cf6cec4d7ca6c216222670e94b02142 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 15 Aug 2016 17:30:27 +0800 Subject: [PATCH 084/116] mqtt_message, mqtt_delivery --- include/emqttd.hrl | 55 ++++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index edd403f9b..ccf4ef47d 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -14,8 +14,6 @@ %% limitations under the License. %%-------------------------------------------------------------------- -%% MQTT Broker Header - %%-------------------------------------------------------------------- %% Banner %%-------------------------------------------------------------------- @@ -77,30 +75,10 @@ -type(mqtt_subscription() :: #mqtt_subscription{}). -%% {<<"a/b/c">>, '$queue', <<"client1">>} -%% {<<"a/b/c">>, undefined, <0.31.0>} -%% {<<"a/b/c">>, <<"group1">>, <<"client2">>} -%% -record(mqtt_subscription, {topic, share, destination :: pid() | binary()}). - -%%-------------------------------------------------------------------- -%% MQTT Credential -%%-------------------------------------------------------------------- --record(mqtt_credential, { - clientid :: binary() | undefined, %% ClientId - username :: binary() | undefined, %% Username - token :: binary() | undefined, - cookie :: binary() | undefined -}). - --type(mqtt_credential() :: #mqtt_credential{}). - %%-------------------------------------------------------------------- %% MQTT Client %%-------------------------------------------------------------------- --type ws_header_key() :: atom() | binary() | string(). --type ws_header_val() :: atom() | binary() | string() | integer(). - -record(mqtt_client, { client_id :: binary() | undefined, client_pid :: pid(), @@ -110,7 +88,9 @@ proto_ver :: 3 | 4, keepalive = 0, will_topic :: undefined | binary(), - ws_initial_headers :: list({ws_header_key(), ws_header_val()}), + token :: binary() | undefined, %% auth token + cookie :: binary() | undefined, %% auth cookie + %%ws_initial_headers :: list({ws_header_key(), ws_header_val()}), connected_at :: erlang:timestamp() }). @@ -135,20 +115,17 @@ -type(mqtt_pktid() :: 1..16#ffff | undefined). -record(mqtt_message, { - msgid :: mqtt_msgid(), %% Global unique message ID - pktid :: mqtt_pktid(), %% PacketId - topic :: binary(), %% Topic that the message is published to - sender :: pid(), %% Pid of the sender/publisher - from, - credential :: mqtt_credential(), %% Credential of the sender/publisher - qos = 0 :: 0 | 1 | 2, %% Message QoS - flags = [] :: [retain | dup | sys], %% Message Flags - retain = false :: boolean(), %% Retain flag - dup = false :: boolean(), %% Dup flag - sys = false :: boolean(), %% $SYS flag - payload :: binary(), %% Payload - timestamp :: erlang:timestamp(), %% os:timestamp - extra = [] :: list() + msgid :: mqtt_msgid(), %% Global unique message ID + pktid :: mqtt_pktid(), %% PacketId + topic :: binary(), %% Topic that the message is published to + qos = 0 :: 0 | 1 | 2, %% Message QoS + flags = [] :: [retain | dup | sys], %% Message Flags + retain = false :: boolean(), %% Retain flag + dup = false :: boolean(), %% Dup flag + sys = false :: boolean(), %% $SYS flag + headers = [] :: list(), + payload :: binary(), %% Payload + timestamp :: erlang:timestamp() %% os:timestamp }). -type(mqtt_message() :: #mqtt_message{}). @@ -157,7 +134,9 @@ %% MQTT Delivery %%-------------------------------------------------------------------- -record(mqtt_delivery, { - message :: mqtt_message(), %% Message + sender :: pid(), %% Pid of the sender/publisher + from :: binary(), + message :: mqtt_message(), %% Message flows :: list() }). From c72a07dbb80b5a7b7c7535dfbd00c5152ac00d6a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 13:41:53 +0800 Subject: [PATCH 085/116] mqtt_delivery, mqtt_message --- include/emqttd.hrl | 54 ++++++++++++------------------------- include/emqttd_protocol.hrl | 17 ++++++------ 2 files changed, 26 insertions(+), 45 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index ccf4ef47d..4fd6749f0 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -40,45 +40,17 @@ %% PubSub %%-------------------------------------------------------------------- --type pubsub() :: publish | subscribe. +-type(pubsub() :: publish | subscribe). -define(PUBSUB(PS), (PS =:= publish orelse PS =:= subscribe)). -%%-------------------------------------------------------------------- -%% MQTT Topic -%%-------------------------------------------------------------------- --record(mqtt_topic, { - topic :: binary(), - flags :: [retained | static] -}). - --type(mqtt_topic() :: #mqtt_topic{}). - -%%-------------------------------------------------------------------- -%% MQTT Route -%%-------------------------------------------------------------------- --record(mqtt_route, { - topic :: binary(), - node :: node() -}). - --type(mqtt_route() :: #mqtt_route{}). - -%%-------------------------------------------------------------------- -%% MQTT Subscription -%%-------------------------------------------------------------------- --record(mqtt_subscription, { - subid :: binary() | atom() | pid(), - topic :: binary(), - qos = 0 :: 0 | 1 | 2 -}). - --type(mqtt_subscription() :: #mqtt_subscription{}). - %%-------------------------------------------------------------------- %% MQTT Client %%-------------------------------------------------------------------- +-type(ws_header_key() :: atom() | binary() | string()). +-type(ws_header_val() :: atom() | binary() | string() | integer()). + -record(mqtt_client, { client_id :: binary() | undefined, client_pid :: pid(), @@ -88,9 +60,7 @@ proto_ver :: 3 | 4, keepalive = 0, will_topic :: undefined | binary(), - token :: binary() | undefined, %% auth token - cookie :: binary() | undefined, %% auth cookie - %%ws_initial_headers :: list({ws_header_key(), ws_header_val()}), + ws_initial_headers :: list({ws_header_key(), ws_header_val()}), connected_at :: erlang:timestamp() }). @@ -117,6 +87,7 @@ -record(mqtt_message, { msgid :: mqtt_msgid(), %% Global unique message ID pktid :: mqtt_pktid(), %% PacketId + from :: {binary(), undefined | binary()}, %% ClientId and Username topic :: binary(), %% Topic that the message is published to qos = 0 :: 0 | 1 | 2, %% Message QoS flags = [] :: [retain | dup | sys], %% Message Flags @@ -135,13 +106,22 @@ %%-------------------------------------------------------------------- -record(mqtt_delivery, { sender :: pid(), %% Pid of the sender/publisher - from :: binary(), - message :: mqtt_message(), %% Message + message :: mqtt_message(), %% Message flows :: list() }). -type(mqtt_delivery() :: #mqtt_delivery{}). +%%-------------------------------------------------------------------- +%% MQTT Route +%%-------------------------------------------------------------------- +-record(mqtt_route, { + topic :: binary(), + node :: node() +}). + +-type(mqtt_route() :: #mqtt_route{}). + %%-------------------------------------------------------------------- %% MQTT Alarm %%-------------------------------------------------------------------- diff --git a/include/emqttd_protocol.hrl b/include/emqttd_protocol.hrl index 61b82f02d..8a5e5d0ca 100644 --- a/include/emqttd_protocol.hrl +++ b/include/emqttd_protocol.hrl @@ -26,7 +26,7 @@ {?MQTT_PROTO_V31, <<"MQIsdp">>}, {?MQTT_PROTO_V311, <<"MQTT">>}]). --type mqtt_vsn() :: ?MQTT_PROTO_V31 | ?MQTT_PROTO_V311. +-type(mqtt_vsn() :: ?MQTT_PROTO_V31 | ?MQTT_PROTO_V311). %%-------------------------------------------------------------------- %% MQTT QoS @@ -41,11 +41,11 @@ -define(IS_QOS(I), (I >= ?QOS0 andalso I =< ?QOS2)). --type mqtt_qos() :: ?QOS0 | ?QOS1 | ?QOS2. +-type(mqtt_qos() :: ?QOS0 | ?QOS1 | ?QOS2). --type mqtt_qos_name() :: qos0 | at_most_once | +-type(mqtt_qos_name() :: qos0 | at_most_once | qos1 | at_least_once | - qos2 | exactly_once. + qos2 | exactly_once). -define(QOS_I(Name), begin @@ -102,7 +102,7 @@ 'PINGRESP', 'DISCONNECT']). --type mqtt_packet_type() :: ?RESERVED..?DISCONNECT. +-type(mqtt_packet_type() :: ?RESERVED..?DISCONNECT). %%-------------------------------------------------------------------- %% MQTT Connect Return Codes @@ -114,7 +114,7 @@ -define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed -define(CONNACK_AUTH, 5). %% Client is not authorized to connect --type mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH. +-type(mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH). %%-------------------------------------------------------------------- %% MQTT Parser and Serializer @@ -135,8 +135,9 @@ %%-------------------------------------------------------------------- %% MQTT Packets %%-------------------------------------------------------------------- --type mqtt_client_id() :: binary(). --type mqtt_packet_id() :: 1..16#ffff | undefined. +-type(mqtt_client_id() :: binary()). +-type(mqtt_username() :: binary() | undefined). +-type(mqtt_packet_id() :: 1..16#ffff | undefined). -record(mqtt_packet_connect, { client_id = <<>> :: mqtt_client_id(), From d0be556f337ed49d4a8b234cca01f96fd040714b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 14:29:39 +0800 Subject: [PATCH 086/116] 2.0 - improve design of session and hook --- src/emqttd_cli.erl | 16 ++--- src/emqttd_message.erl | 11 ++-- src/emqttd_mod_presence.erl | 2 +- src/emqttd_protocol.erl | 15 +++-- src/emqttd_session.erl | 127 ++++++++++++++++++------------------ src/emqttd_session_sup.erl | 6 +- src/emqttd_sm.erl | 33 +++++----- src/emqttd_topic.erl | 1 + 8 files changed, 108 insertions(+), 103 deletions(-) diff --git a/src/emqttd_cli.erl b/src/emqttd_cli.erl index 6c77ddcd5..f346be653 100644 --- a/src/emqttd_cli.erl +++ b/src/emqttd_cli.erl @@ -491,13 +491,13 @@ print(Routes = [#mqtt_route{topic = Topic} | _]) -> Nodes = [atom_to_list(Node) || #mqtt_route{node = Node} <- Routes], ?PRINT("~s -> ~s~n", [Topic, string:join(Nodes, ",")]); -print(Subscriptions = [#mqtt_subscription{subid = ClientId} | _]) -> - TopicTable = [io_lib:format("~s:~w", [Topic, Qos]) - || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions], - ?PRINT("~s -> ~s~n", [ClientId, string:join(TopicTable, ",")]); +%% print(Subscriptions = [#mqtt_subscription{subid = ClientId} | _]) -> +%% TopicTable = [io_lib:format("~s:~w", [Topic, Qos]) +%% || #mqtt_subscription{topic = Topic, qos = Qos} <- Subscriptions], +%% ?PRINT("~s -> ~s~n", [ClientId, string:join(TopicTable, ",")]); -print(Topics = [#mqtt_topic{}|_]) -> - foreach(fun print/1, Topics); +%% print(Topics = [#mqtt_topic{}|_]) -> +%% foreach(fun print/1, Topics); print(#mqtt_plugin{name = Name, version = Ver, descr = Descr, active = Active}) -> ?PRINT("Plugin(~s, version=~s, description=~s, active=~s)~n", @@ -509,8 +509,8 @@ print(#mqtt_client{client_id = ClientId, clean_sess = CleanSess, username = User [ClientId, CleanSess, Username, emqttd_net:format(Peername), emqttd_time:now_to_secs(ConnectedAt)]); -print(#mqtt_topic{topic = Topic, flags = Flags}) -> - ?PRINT("~s: ~s~n", [Topic, string:join([atom_to_list(F) || F <- Flags], ",")]); +%% print(#mqtt_topic{topic = Topic, flags = Flags}) -> +%% ?PRINT("~s: ~s~n", [Topic, string:join([atom_to_list(F) || F <- Flags], ",")]); print(#mqtt_route{topic = Topic, node = Node}) -> ?PRINT("~s -> ~s~n", [Topic, Node]); diff --git a/src/emqttd_message.erl b/src/emqttd_message.erl index 7a3632d0e..c0b5ffb3b 100644 --- a/src/emqttd_message.erl +++ b/src/emqttd_message.erl @@ -81,8 +81,7 @@ from_packet(#mqtt_packet_connect{client_id = ClientId, will_msg = Msg}) -> #mqtt_message{msgid = msgid(Qos), topic = Topic, - from = ClientId, - sender = Username, + from = {ClientId, Username}, retain = Retain, qos = Qos, dup = false, @@ -95,7 +94,7 @@ from_packet(ClientId, Packet) -> from_packet(Username, ClientId, Packet) -> Msg = from_packet(Packet), - Msg#mqtt_message{from = ClientId, sender = Username}. + Msg#mqtt_message{from = {ClientId, Username}}. msgid(?QOS_0) -> undefined; @@ -150,10 +149,10 @@ unset_flag(retain, Msg = #mqtt_message{retain = true}) -> unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. %% @doc Format MQTT Message -format(#mqtt_message{msgid = MsgId, pktid = PktId, from = From, sender = Sender, +format(#mqtt_message{msgid = MsgId, pktid = PktId, from = {ClientId, Username}, qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> - io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Sender=~s, Topic=~s)", - [i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Sender, Topic]). + io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)", + [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]). i(true) -> 1; i(false) -> 0; diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl index 89228df78..7815e88be 100644 --- a/src/emqttd_mod_presence.erl +++ b/src/emqttd_mod_presence.erl @@ -45,7 +45,7 @@ on_client_connected(ConnAck, Client = #mqtt_client{client_id = ClientId, emqttd:publish(emqttd_message:set_flag(sys, Msg)), {ok, Client}. -on_client_disconnected(Reason, ClientId, Opts) -> +on_client_disconnected(Reason, #mqtt_client{client_id = ClientId}, Opts) -> Json = mochijson2:encode([{clientid, ClientId}, {reason, reason(Reason)}, {ts, emqttd_time:now_to_secs()}]), diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index ec6de539f..96ce19dea 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -146,7 +146,7 @@ process(Packet = ?CONNECT_PACKET(Var), State0) -> State2 = maybe_set_clientid(State1), %% Start session - case emqttd_sm:start_session(CleanSess, clientid(State2)) of + case emqttd_sm:start_session(CleanSess, {clientid(State2), Username}) of {ok, Session, SP} -> %% Register the client emqttd_cm:reg(client(State2)), @@ -280,10 +280,11 @@ shutdown(conflict, #proto_state{client_id = _ClientId}) -> %% emqttd_cm:unreg(ClientId); ignore; -shutdown(Error, State = #proto_state{client_id = ClientId, will_msg = WillMsg}) -> +shutdown(Error, State = #proto_state{will_msg = WillMsg}) -> ?LOG(info, "Shutdown for ~p", [Error], State), - send_willmsg(ClientId, WillMsg), - emqttd:run_hooks('client.disconnected', [Error], ClientId), + Client = client(State), + send_willmsg(Client, WillMsg), + emqttd:run_hooks('client.disconnected', [Error], Client), %% let it down %% emqttd_cm:unreg(ClientId). ok. @@ -301,10 +302,10 @@ maybe_set_clientid(State = #proto_state{client_id = NullId}) maybe_set_clientid(State) -> State. -send_willmsg(_ClientId, undefined) -> +send_willmsg(_Client, undefined) -> ignore; -send_willmsg(ClientId, WillMsg) -> - emqttd:publish(WillMsg#mqtt_message{from = ClientId}). +send_willmsg(#mqtt_client{client_id = ClientId, username = Username}, WillMsg) -> + emqttd:publish(WillMsg#mqtt_message{from = {ClientId, Username}}). start_keepalive(0) -> ignore; diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 55eb08b11..eca673691 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -77,6 +77,9 @@ %% Old Client Pid that has been kickout old_client_pid :: pid(), + %% Username + username :: binary() | undefined, + %% Last packet id of the session packet_id = 1, @@ -136,9 +139,9 @@ "Session(~s): " ++ Format, [State#session.client_id | Args])). %% @doc Start a session. --spec(start_link(boolean(), mqtt_client_id(), pid()) -> {ok, pid()} | {error, any()}). -start_link(CleanSess, ClientId, ClientPid) -> - gen_server2:start_link(?MODULE, [CleanSess, ClientId, ClientPid], []). +-spec(start_link(boolean(), {mqtt_client_id(), mqtt_username()}, pid()) -> {ok, pid()} | {error, any()}). +start_link(CleanSess, {ClientId, Username}, ClientPid) -> + gen_server2:start_link(?MODULE, [CleanSess, {ClientId, Username}, ClientPid], []). %% @doc Resume a session. -spec(resume(pid(), mqtt_client_id(), pid()) -> ok). @@ -208,10 +211,10 @@ unsubscribe(SessPid, Topics) -> gen_server2:cast(SessPid, {unsubscribe, Topics}). %%-------------------------------------------------------------------- -%% gen_server callbacks +%% gen_server Callbacks %%-------------------------------------------------------------------- -init([CleanSess, ClientId, ClientPid]) -> +init([CleanSess, {ClientId, Username}, ClientPid]) -> process_flag(trap_exit, true), true = link(ClientPid), SessEnv = emqttd_conf:session(), @@ -219,6 +222,7 @@ init([CleanSess, ClientId, ClientPid]) -> clean_sess = CleanSess, client_id = ClientId, client_pid = ClientPid, + username = Username, subscriptions = dict:new(), inflight_queue = [], max_inflight = get_value(max_inflight, SessEnv, 0), @@ -284,68 +288,67 @@ handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, pktid = PktId}}, handle_call(Req, _From, State) -> ?UNEXPECTED_REQ(Req, State). -handle_cast({subscribe, RawTopicTable, AckFun}, Session = #session{client_id = ClientId, - subscriptions = Subscriptions}) -> - %% TODO: Ugly... - TopicTable0 = lists:map(fun({T, Q}) -> - {T1, Opts} = emqttd_topic:strip(T), - {T1, [{qos, Q} | Opts]} - end, RawTopicTable), - case emqttd:run_hooks('client.subscribe', [ClientId], TopicTable0) of - {ok, TopicTable} -> - ?LOG(info, "Subscribe ~p", [TopicTable], Session), - Subscriptions1 = lists:foldl( - fun({Topic, Opts = [{qos, Qos}|_]}, SubDict) -> - case dict:find(Topic, SubDict) of - {ok, Qos} -> - ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, Qos], Session), - SubDict; - {ok, OldQos} -> - emqttd:setqos(Topic, ClientId, Qos), - ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, Qos], Session), - dict:store(Topic, Qos, SubDict); - error -> - emqttd:subscribe(Topic, ClientId, Opts), - %%TODO: the design is ugly... - %% : 3.8.4 - %% Where the Topic Filter is not identical to any existing Subscription’s filter, - %% a new Subscription is created and all matching retained messages are sent. - emqttd_retainer:dispatch(Topic, self()), +%%TODO: 2.0 FIX - dict:store(Topic, Qos, SubDict) - end - end, Subscriptions, TopicTable), - AckFun([Qos || {_, Qos} <- RawTopicTable]), - emqttd:run_hooks('client.subscribe.after', [ClientId], TopicTable), - hibernate(Session#session{subscriptions = Subscriptions1}); - {stop, TopicTable} -> - ?LOG(error, "Cannot subscribe: ~p", [TopicTable], Session), - hibernate(Session) - end; +handle_cast({subscribe, TopicTable, AckFun}, Session = #session{client_id = ClientId, + username = Username, + subscriptions = Subscriptions}) -> + ?LOG(info, "Subscribe ~p", [TopicTable], Session), + {GrantedQos, Subscriptions1} = + lists:foldl(fun({RawTopic, Qos}, {QosAcc, SubDict}) -> + {Topic, Opts} = emqttd_topic:strip(RawTopic), + case emqttd:run_hooks('client.subscribe', [{ClientId, Username}], {Topic, Opts}) of + {ok, {Topic1, Opts1}} -> + NewQos = proplists:get_value(qos, Opts1, Qos), + {[NewQos | QosAcc], case dict:find(Topic, SubDict) of + {ok, NewQos} -> + ?LOG(warning, "duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], Session), + SubDict; + {ok, OldQos} -> + emqttd:setqos(Topic, ClientId, NewQos), + ?LOG(warning, "duplicated subscribe ~s, old_qos=~w, new_qos=~w", [Topic, OldQos, NewQos], Session), + dict:store(Topic, NewQos, SubDict); + error -> + emqttd:subscribe(Topic1, ClientId, Opts1), + %%TODO: the design is ugly... + %% : 3.8.4 + %% Where the Topic Filter is not identical to any existing Subscription’s filter, + %% a new Subscription is created and all matching retained messages are sent. + emqttd_retainer:dispatch(Topic1, self()), + dict:store(Topic1, NewQos, SubDict) + end}; + {stop, _} -> + ?LOG(error, "Cannot subscribe: ~p", [Topic], Session), + {[128 | QosAcc], SubDict} + end + end, {[], Subscriptions}, TopicTable), + AckFun(lists:reverse(GrantedQos)), + %%emqttd:run_hooks('client.subscribe.after', [ClientId], TopicTable), + hibernate(Session#session{subscriptions = Subscriptions1}); -handle_cast({unsubscribe, RawTopics}, Session = #session{client_id = ClientId, - subscriptions = Subscriptions}) -> - Topics0 = lists:map(fun(Topic) -> - {T, _Opts} = emqttd_topic:strip(Topic), T - end, RawTopics), - case emqttd:run_hooks('client.unsubscribe', [ClientId], Topics0) of - {ok, Topics} -> - ?LOG(info, "unsubscribe ~p", [Topics], Session), - Subscriptions1 = lists:foldl( - fun(Topic, SubDict) -> - case dict:find(Topic, SubDict) of - {ok, _Qos} -> - emqttd:unsubscribe(Topic, ClientId), - dict:erase(Topic, SubDict); - error -> +%%TODO: 2.0 FIX + +handle_cast({unsubscribe, Topics}, Session = #session{client_id = ClientId, + username = Username, + subscriptions = Subscriptions}) -> + ?LOG(info, "unsubscribe ~p", [Topics], Session), + Subscriptions1 = + lists:foldl(fun(RawTopic, SubDict) -> + {Topic0, _Opts} = emqttd_topic:strip(RawTopic), + case emqttd:run_hooks('client.unsubscribe', [ClientId, Username], Topic0) of + {ok, Topic1} -> + case dict:find(Topic1, SubDict) of + {ok, _Qos} -> + emqttd:unsubscribe(Topic1, ClientId), + dict:erase(Topic1, SubDict); + error -> + SubDict + end; + {stop, _} -> SubDict end end, Subscriptions, Topics), - hibernate(Session#session{subscriptions = Subscriptions1}); - {stop, Topics} -> - ?LOG(info, "Cannot unsubscribe: ~p", [Topics], Session), - hibernate(Session) - end; + hibernate(Session#session{subscriptions = Subscriptions1}); handle_cast({destroy, ClientId}, Session = #session{client_id = ClientId}) -> ?LOG(warning, "destroyed", [], Session), diff --git a/src/emqttd_session_sup.erl b/src/emqttd_session_sup.erl index 2b9ee9496..394cb84d0 100644 --- a/src/emqttd_session_sup.erl +++ b/src/emqttd_session_sup.erl @@ -29,9 +29,9 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @doc Start a session --spec(start_session(boolean(), binary(), pid()) -> {ok, pid()}). -start_session(CleanSess, ClientId, ClientPid) -> - supervisor:start_child(?MODULE, [CleanSess, ClientId, ClientPid]). +-spec(start_session(boolean(), {binary(), binary() | undefined} , pid()) -> {ok, pid()}). +start_session(CleanSess, {ClientId, Username}, ClientPid) -> + supervisor:start_child(?MODULE, [CleanSess, {ClientId, Username}, ClientPid]). %%-------------------------------------------------------------------- %% Supervisor callbacks diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index 43de1e91a..4b82c3434 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -32,7 +32,7 @@ %% API Function Exports -export([start_link/2]). --export([start_session/2, lookup_session/1, register_session/3, unregister_session/1]). +-export([start_session/2, lookup_session/1, reg_session/3, unreg_session/1]). -export([dispatch/3]). @@ -77,10 +77,10 @@ start_link(Pool, Id) -> gen_server2:start_link({local, ?PROC_NAME(?MODULE, Id)}, ?MODULE, [Pool, Id], []). %% @doc Start a session --spec(start_session(boolean(), binary()) -> {ok, pid(), boolean()} | {error, any()}). -start_session(CleanSess, ClientId) -> +-spec(start_session(boolean(), {binary(), binary() | undefined}) -> {ok, pid(), boolean()} | {error, any()}). +start_session(CleanSess, {ClientId, Username}) -> SM = gproc_pool:pick_worker(?POOL, ClientId), - call(SM, {start_session, {CleanSess, ClientId, self()}}). + call(SM, {start_session, CleanSess, {ClientId, Username}, self()}). %% @doc Lookup a Session -spec(lookup_session(binary()) -> mqtt_session() | undefined). @@ -91,18 +91,18 @@ lookup_session(ClientId) -> end. %% @doc Register a session with info. --spec(register_session(binary(), boolean(), [tuple()]) -> true). -register_session(ClientId, CleanSess, Properties) -> +-spec(reg_session(binary(), boolean(), [tuple()]) -> true). +reg_session(ClientId, CleanSess, Properties) -> ets:insert(mqtt_local_session, {ClientId, self(), CleanSess, Properties}). %% @doc Unregister a session. --spec(unregister_session(binary()) -> true). -unregister_session(ClientId) -> +-spec(unreg_session(binary()) -> true). +unreg_session(ClientId) -> ets:delete(mqtt_local_session, ClientId). dispatch(ClientId, Topic, Msg) -> try ets:lookup_element(mqtt_local_session, ClientId, 2) of - Pid -> Pid ! {dispatch, Topic, Msg} + Pid -> Pid ! {deliver, Topic, Msg} catch error:badarg -> io:format("Session Not Found: ~p~n", [ClientId]), ok %%TODO: How?? end. @@ -128,11 +128,11 @@ prioritise_info(_Msg, _Len, _State) -> 2. %% Persistent Session -handle_call({start_session, Client = {false, ClientId, ClientPid}}, _From, State) -> +handle_call({start_session, false, {ClientId, Username}, ClientPid}, _From, State) -> case lookup_session(ClientId) of undefined -> %% Create session locally - create_session(Client, State); + create_session({false, {ClientId, Username}, ClientPid}, State); Session -> case resume_session(Session, ClientPid) of {ok, SessPid} -> @@ -143,7 +143,8 @@ handle_call({start_session, Client = {false, ClientId, ClientPid}}, _From, State end; %% Transient Session -handle_call({start_session, Client = {true, ClientId, _ClientPid}}, _From, State) -> +handle_call({start_session, true, {ClientId, Username}, ClientPid}, _From, State) -> + Client = {true, {ClientId, Username}, ClientPid}, case lookup_session(ClientId) of undefined -> create_session(Client, State); @@ -195,8 +196,8 @@ code_change(_OldVsn, State, _Extra) -> %%-------------------------------------------------------------------- %% Create Session Locally -create_session({CleanSess, ClientId, ClientPid}, State) -> - case create_session(CleanSess, ClientId, ClientPid) of +create_session({CleanSess, {ClientId, Username}, ClientPid}, State) -> + case create_session(CleanSess, {ClientId, Username}, ClientPid) of {ok, SessPid} -> {reply, {ok, SessPid, false}, monitor_session(ClientId, SessPid, State)}; @@ -204,8 +205,8 @@ create_session({CleanSess, ClientId, ClientPid}, State) -> {reply, {error, Error}, State} end. -create_session(CleanSess, ClientId, ClientPid) -> - case emqttd_session_sup:start_session(CleanSess, ClientId, ClientPid) of +create_session(CleanSess, {ClientId, Username}, ClientPid) -> + case emqttd_session_sup:start_session(CleanSess, {ClientId, Username}, ClientPid) of {ok, SessPid} -> Session = #mqtt_session{client_id = ClientId, sess_pid = SessPid, persistent = not CleanSess}, case insert_session(Session) of diff --git a/src/emqttd_topic.erl b/src/emqttd_topic.erl index 5ece2255b..ebd16714d 100644 --- a/src/emqttd_topic.erl +++ b/src/emqttd_topic.erl @@ -17,6 +17,7 @@ -module(emqttd_topic). -import(lists, [reverse/1]). + -export([match/2, validate/1, triples/1, words/1, wildcard/1]). -export([join/1, feed_var/3, systop/1]). From e2246c7406e1a360b9d16ba293afcb60e6b32292 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 14:43:16 +0800 Subject: [PATCH 087/116] reg_session --- src/emqttd_session.erl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index eca673691..facfaf2aa 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -236,7 +236,7 @@ init([CleanSess, {ClientId, Username}, ClientPid]) -> expired_after = get_value(expired_after, SessEnv) * 60, collect_interval = get_value(collect_interval, SessEnv, 0), timestamp = os:timestamp()}, - emqttd_sm:register_session(ClientId, CleanSess, sess_info(Session)), + emqttd_sm:reg_session(ClientId, CleanSess, sess_info(Session)), %% Start statistics {ok, start_collector(Session), hibernate}. @@ -394,8 +394,7 @@ handle_cast({resume, ClientId, ClientPid}, Session = #session{client_id = C if CleanSess =:= true -> ?LOG(warning, "CleanSess changed to false.", [], Session), - %% emqttd_sm:unregister_session(CleanSess, ClientId), - emqttd_sm:register_session(ClientId, false, sess_info(Session1)); + emqttd_sm:reg_session(ClientId, false, sess_info(Session1)); CleanSess =:= false -> ok end, @@ -509,7 +508,7 @@ handle_info({timeout, awaiting_comp, PktId}, Session = #session{awaiting_comp = end; handle_info(collect_info, Session = #session{clean_sess = CleanSess, client_id = ClientId}) -> - emqttd_sm:register_session(ClientId, CleanSess, sess_info(Session)), + emqttd_sm:reg_session(ClientId, CleanSess, sess_info(Session)), hibernate(start_collector(Session)); handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, @@ -542,7 +541,7 @@ handle_info(Info, Session) -> terminate(_Reason, #session{client_id = ClientId}) -> emqttd:subscriber_down(ClientId), - emqttd_sm:unregister_session(ClientId). + emqttd_sm:unreg_session(ClientId). code_change(_OldVsn, Session, _Extra) -> {ok, Session}. From ef8006bc89fa44a0f18f53dc98900b65e686c93f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 15:23:17 +0800 Subject: [PATCH 088/116] fix rewrite --- src/emqttd_mod_rewrite.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/emqttd_mod_rewrite.erl b/src/emqttd_mod_rewrite.erl index d23654bdc..edd6ac41a 100644 --- a/src/emqttd_mod_rewrite.erl +++ b/src/emqttd_mod_rewrite.erl @@ -40,13 +40,13 @@ load(Opts) -> emqttd:hook('message.publish', fun ?MODULE:rewrite_publish/2, [Sections]) end. -rewrite_subscribe(_ClientId, TopicTable, Sections) -> - lager:info("Rewrite subscribe: ~p", [TopicTable]), - {ok, [{match_topic(Topic, Sections), Qos} || {Topic, Qos} <- TopicTable]}. +rewrite_subscribe({_ClientId, _Username}, {Topic, Opts}, Sections) -> + lager:info("Rewrite subscribe: ~p", [{Topic, Opts}]), + {ok, {match_topic(Topic, Sections), Opts}}. -rewrite_unsubscribe(_ClientId, Topics, Sections) -> - lager:info("Rewrite unsubscribe: ~p", [Topics]), - {ok, [match_topic(Topic, Sections) || Topic <- Topics]}. +rewrite_unsubscribe({_ClientId, _Username}, {Topic, Opts}, Sections) -> + lager:info("Rewrite unsubscribe: ~p", [{Topic, Opts}]), + {ok, {match_topic(Topic, Sections), Opts}}. rewrite_publish(Message=#mqtt_message{topic = Topic}, Sections) -> %%TODO: this will not work if the client is always online. From 595c0f2d5ad2faddee3693cf0f3f914dbe7706b6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 15:38:38 +0800 Subject: [PATCH 089/116] dispatch --- src/emqttd_sm.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index 4b82c3434..3a978b3fd 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -102,7 +102,7 @@ unreg_session(ClientId) -> dispatch(ClientId, Topic, Msg) -> try ets:lookup_element(mqtt_local_session, ClientId, 2) of - Pid -> Pid ! {deliver, Topic, Msg} + Pid -> Pid ! {dispatch, Topic, Msg} catch error:badarg -> io:format("Session Not Found: ~p~n", [ClientId]), ok %%TODO: How?? end. From 01cf8b85623c8b88d4369daacc62c5c07055e84e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 15:49:00 +0800 Subject: [PATCH 090/116] Username --- src/emqttd_protocol.erl | 4 ++-- src/emqttd_session.erl | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index 96ce19dea..bfaee267f 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -247,9 +247,9 @@ with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), end. -spec(send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}). -send(Msg, State = #proto_state{client_id = ClientId}) +send(Msg, State = #proto_state{client_id = ClientId, username = Username}) when is_record(Msg, mqtt_message) -> - emqttd:run_hooks('message.delivered', [ClientId], Msg), + emqttd:run_hooks('message.delivered', [{ClientId, Username}], Msg), send(emqttd_message:to_packet(Msg), State); send(Packet, State = #proto_state{sendfun = SendFun}) diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index facfaf2aa..0e2df6b2e 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -658,11 +658,12 @@ await(#mqtt_message{pktid = PktId}, Session = #session{awaiting_ack = Awaiting Session#session{awaiting_ack = Awaiting1}. acked(PktId, Session = #session{client_id = ClientId, + username = Username, inflight_queue = InflightQ, awaiting_ack = Awaiting}) -> case lists:keyfind(PktId, 1, InflightQ) of {_, Msg} -> - emqttd:run_hooks('message.acked', [ClientId], Msg); + emqttd:run_hooks('message.acked', [{ClientId, Username}], Msg); false -> ?LOG(error, "Cannot find acked pktid: ~p", [PktId], Session) end, From c18c5e17b138be913423670722ccb0776a69d762 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Aug 2016 16:21:54 +0800 Subject: [PATCH 091/116] mqtt-sn, coap --- docs/source/coap.rst | 9 +++++++-- docs/source/mqtt-sn.rst | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 docs/source/mqtt-sn.rst diff --git a/docs/source/coap.rst b/docs/source/coap.rst index 32d482e17..41d73e618 100644 --- a/docs/source/coap.rst +++ b/docs/source/coap.rst @@ -1,3 +1,8 @@ -============== + +.. _coap: + +============= CoAP Protocol -============== +============= + + diff --git a/docs/source/mqtt-sn.rst b/docs/source/mqtt-sn.rst new file mode 100644 index 000000000..22bcabf6f --- /dev/null +++ b/docs/source/mqtt-sn.rst @@ -0,0 +1,10 @@ + +.. _mqtt_sn: + +TODO:... + +================ +MQTT-SN Protocol +================ + + From f25420309c7c488a39c0a8eed991e42725949339 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 16 Aug 2016 23:08:59 +0800 Subject: [PATCH 092/116] -type(). --- include/emqttd.hrl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index ccf4ef47d..0fc868355 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -40,7 +40,7 @@ %% PubSub %%-------------------------------------------------------------------- --type pubsub() :: publish | subscribe. +-type(pubsub() :: publish | subscribe). -define(PUBSUB(PS), (PS =:= publish orelse PS =:= subscribe)). From b59a1d7547d78aefa8d526e3107387deac77d3be Mon Sep 17 00:00:00 2001 From: Feng Date: Wed, 17 Aug 2016 08:49:45 +0800 Subject: [PATCH 093/116] sender --- src/emqttd_pubsub.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl index 036b345aa..1af6274bf 100644 --- a/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -69,7 +69,7 @@ route(Routes, Delivery) -> {ok, DelAcc1} = route([Route], DelAcc), DelAcc1 end, Delivery, Routes)}. -delivery(Msg) -> #mqtt_delivery{message = Msg, flows = []}. +delivery(Msg) -> #mqtt_delivery{sender = self(), message = Msg, flows = []}. %% @doc Forward message to another node... forward(Node, To, Delivery) -> From d72c7ad07c7effd140a67b615336489bb65ffac5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Aug 2016 12:35:26 +0800 Subject: [PATCH 094/116] fix format --- src/emqttd_message.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/emqttd_message.erl b/src/emqttd_message.erl index c0b5ffb3b..477ac3aac 100644 --- a/src/emqttd_message.erl +++ b/src/emqttd_message.erl @@ -152,7 +152,13 @@ unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. format(#mqtt_message{msgid = MsgId, pktid = PktId, from = {ClientId, Username}, qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)", - [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]). + [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]); + +%% TODO:... +format(#mqtt_message{msgid = MsgId, pktid = PktId, from = From, + qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> + io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Topic=~s)", + [i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Topic]). i(true) -> 1; i(false) -> 0; From 2b45dbf09fbf91726420bdc3e7020336fd777780 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Aug 2016 13:46:32 +0800 Subject: [PATCH 095/116] fix 'undefined' msgid --- src/emqttd_message.erl | 47 ++++++++++++++++-------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/emqttd_message.erl b/src/emqttd_message.erl index 477ac3aac..e798f0fd8 100644 --- a/src/emqttd_message.erl +++ b/src/emqttd_message.erl @@ -28,27 +28,19 @@ -export([format/1]). -%% @doc Make a message --spec(make(From, Topic, Payload) -> mqtt_message() when - From :: atom() | binary(), - Topic :: binary(), - Payload :: binary()). -make(From, Topic, Payload) -> - #mqtt_message{topic = Topic, - from = From, - payload = Payload, - timestamp = os:timestamp()}. +-type(msg_from() :: atom() | {binary(), undefined | binary()}). --spec(make(From, Qos, Topic, Payload) -> mqtt_message() when - From :: atom() | binary(), - Qos :: mqtt_qos() | mqtt_qos_name(), - Topic :: binary(), - Payload :: binary()). +%% @doc Make a message +-spec(make(msg_from(), binary(), binary()) -> mqtt_message()). +make(From, Topic, Payload) -> + make(From, ?QOS_0, Topic, Payload). + +-spec(make(msg_from(), mqtt_qos(), binary(), binary()) -> mqtt_message()). make(From, Qos, Topic, Payload) -> - #mqtt_message{msgid = msgid(?QOS_I(Qos)), - topic = Topic, + #mqtt_message{msgid = msgid(), from = From, qos = ?QOS_I(Qos), + topic = Topic, payload = Payload, timestamp = os:timestamp()}. @@ -61,13 +53,13 @@ from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId}, payload = Payload}) -> - #mqtt_message{msgid = msgid(Qos), - pktid = PacketId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload, + #mqtt_message{msgid = msgid(), + pktid = PacketId, + qos = Qos, + retain = Retain, + dup = Dup, + topic = Topic, + payload = Payload, timestamp = os:timestamp()}; from_packet(#mqtt_packet_connect{will_flag = false}) -> @@ -79,7 +71,7 @@ from_packet(#mqtt_packet_connect{client_id = ClientId, will_qos = Qos, will_topic = Topic, will_msg = Msg}) -> - #mqtt_message{msgid = msgid(Qos), + #mqtt_message{msgid = msgid(), topic = Topic, from = {ClientId, Username}, retain = Retain, @@ -96,10 +88,7 @@ from_packet(Username, ClientId, Packet) -> Msg = from_packet(Packet), Msg#mqtt_message{from = {ClientId, Username}}. -msgid(?QOS_0) -> - undefined; -msgid(Qos) when Qos =:= ?QOS_1 orelse Qos =:= ?QOS_2 -> - emqttd_guid:gen(). +msgid() -> emqttd_guid:gen(). %% @doc Message to packet -spec(to_packet(mqtt_message()) -> mqtt_packet()). From f2f5f251d77e0d4a6b75c98d493d453940f5a855 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 18 Aug 2016 08:09:43 +0800 Subject: [PATCH 096/116] improve the design of retainer --- src/emqttd_backend.erl | 95 ----------------------------------------- src/emqttd_retainer.erl | 86 +++++++++++++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 103 deletions(-) delete mode 100644 src/emqttd_backend.erl diff --git a/src/emqttd_backend.erl b/src/emqttd_backend.erl deleted file mode 100644 index 5515b2ac8..000000000 --- a/src/emqttd_backend.erl +++ /dev/null @@ -1,95 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2012-2016 Feng Lee . -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(emqttd_backend). - --include("emqttd.hrl"). - --include_lib("stdlib/include/ms_transform.hrl"). - -%% Mnesia Callbacks --export([mnesia/1]). - --boot_mnesia({mnesia, [boot]}). --copy_mnesia({mnesia, [copy]}). - -%% Retained Message API --export([retain_message/1, read_messages/1, match_messages/1, delete_message/1, - expire_messages/1, retained_count/0]). - --record(retained_message, {topic, msg}). - -%%-------------------------------------------------------------------- -%% Mnesia callbacks -%%-------------------------------------------------------------------- - -mnesia(boot) -> - ok = emqttd_mnesia:create_table(retained_message, [ - {type, ordered_set}, - {disc_copies, [node()]}, - {record_name, retained_message}, - {attributes, record_info(fields, retained_message)}, - {storage_properties, [{ets, [compressed]}, - {dets, [{auto_save, 1000}]}]}]); - -mnesia(copy) -> - ok = emqttd_mnesia:copy_table(retained_message). - -%%-------------------------------------------------------------------- -%% Retained Message -%%-------------------------------------------------------------------- - --spec(retain_message(mqtt_message()) -> ok). -retain_message(Msg = #mqtt_message{topic = Topic}) -> - mnesia:dirty_write(#retained_message{topic = Topic, msg = Msg}). - --spec(read_messages(binary()) -> [mqtt_message()]). -read_messages(Topic) -> - [Msg || #retained_message{msg = Msg} <- mnesia:dirty_read(retained_message, Topic)]. - --spec(match_messages(binary()) -> [mqtt_message()]). -match_messages(Filter) -> - %% TODO: optimize later... - Fun = fun(#retained_message{topic = Name, msg = Msg}, Acc) -> - case emqttd_topic:match(Name, Filter) of - true -> [Msg|Acc]; - false -> Acc - end - end, - mnesia:async_dirty(fun mnesia:foldl/3, [Fun, [], retained_message]). - --spec(delete_message(binary()) -> ok). -delete_message(Topic) -> - mnesia:dirty_delete(retained_message, Topic). - --spec(expire_messages(pos_integer()) -> any()). -expire_messages(Time) when is_integer(Time) -> - mnesia:transaction( - fun() -> - Match = ets:fun2ms( - fun(#retained_message{topic = Topic, msg = #mqtt_message{timestamp = {MegaSecs, Secs, _}}}) - when Time > (MegaSecs * 1000000 + Secs) -> Topic - end), - Topics = mnesia:select(retained_message, Match, write), - lists:foreach(fun(<<"$SYS/", _/binary>>) -> ok; %% ignore $SYS/# messages - (Topic) -> mnesia:delete({retained_message, Topic}) - end, Topics) - end). - --spec(retained_count() -> non_neg_integer()). -retained_count() -> - mnesia:table_info(retained_message, size). - diff --git a/src/emqttd_retainer.erl b/src/emqttd_retainer.erl index 0b239bec5..005489e58 100644 --- a/src/emqttd_retainer.erl +++ b/src/emqttd_retainer.erl @@ -23,6 +23,14 @@ -include("emqttd_internal.hrl"). +-include_lib("stdlib/include/ms_transform.hrl"). + +%% Mnesia Callbacks +-export([mnesia/1]). + +-boot_mnesia({mnesia, [boot]}). +-copy_mnesia({mnesia, [copy]}). + %% API Function Exports -export([retain/1, dispatch/2]). @@ -33,8 +41,26 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-record(retained_message, {topic, msg}). + -record(state, {stats_fun, expired_after, stats_timer, expire_timer}). +%%-------------------------------------------------------------------- +%% Mnesia callbacks +%%-------------------------------------------------------------------- + +mnesia(boot) -> + ok = emqttd_mnesia:create_table(retained_message, [ + {type, ordered_set}, + {disc_copies, [node()]}, + {record_name, retained_message}, + {attributes, record_info(fields, retained_message)}, + {storage_properties, [{ets, [compressed]}, + {dets, [{auto_save, 1000}]}]}]); + +mnesia(copy) -> + ok = emqttd_mnesia:copy_table(retained_message). + %%-------------------------------------------------------------------- %% API %%-------------------------------------------------------------------- @@ -50,14 +76,14 @@ retain(#mqtt_message{retain = false}) -> ignore; %% RETAIN flag set to 1 and payload containing zero bytes retain(#mqtt_message{retain = true, topic = Topic, payload = <<>>}) -> - emqttd_backend:delete_message(Topic); + delete_message(Topic); retain(Msg = #mqtt_message{topic = Topic, retain = true, payload = Payload}) -> - TabSize = emqttd_backend:retained_count(), + TabSize = retained_count(), case {TabSize < limit(table), size(Payload) < limit(payload)} of {true, true} -> - emqttd_backend:retain_message(Msg), - emqttd_metrics:set('messages/retained', emqttd_backend:retained_count()); + retain_message(Msg), + emqttd_metrics:set('messages/retained', retained_count()); {false, _}-> lager:error("Cannot retain message(topic=~s) for table is full!", [Topic]); {_, false}-> @@ -82,8 +108,8 @@ env(Key) -> -spec(dispatch(Topic :: binary(), CPid :: pid()) -> any()). dispatch(Topic, CPid) when is_binary(Topic) -> Msgs = case emqttd_topic:wildcard(Topic) of - false -> emqttd_backend:read_messages(Topic); - true -> emqttd_backend:match_messages(Topic) + false -> read_messages(Topic); + true -> match_messages(Topic) end, lists:foreach(fun(Msg) -> CPid ! {dispatch, Topic, Msg} end, lists:reverse(Msgs)). @@ -113,7 +139,7 @@ handle_cast(Msg, State) -> ?UNEXPECTED_MSG(Msg, State). handle_info(stats, State = #state{stats_fun = StatsFun}) -> - StatsFun(emqttd_backend:retained_count()), + StatsFun(retained_count()), {noreply, State, hibernate}; handle_info(expire, State = #state{expired_after = Never}) @@ -121,7 +147,7 @@ handle_info(expire, State = #state{expired_after = Never}) {noreply, State, hibernate}; handle_info(expire, State = #state{expired_after = ExpiredAfter}) -> - emqttd_backend:expire_messages(emqttd_time:now_to_secs() - ExpiredAfter), + expire_messages(emqttd_time:now_to_secs() - ExpiredAfter), {noreply, State, hibernate}; handle_info(Info, State) -> @@ -134,3 +160,47 @@ terminate(_Reason, _State = #state{stats_timer = TRef1, expire_timer = TRef2}) - code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%-------------------------------------------------------------------- +%% Internal Functions +%%-------------------------------------------------------------------- + +-spec(retain_message(mqtt_message()) -> ok). +retain_message(Msg = #mqtt_message{topic = Topic}) -> + mnesia:dirty_write(#retained_message{topic = Topic, msg = Msg}). + +-spec(read_messages(binary()) -> [mqtt_message()]). +read_messages(Topic) -> + [Msg || #retained_message{msg = Msg} <- mnesia:dirty_read(retained_message, Topic)]. + +-spec(match_messages(binary()) -> [mqtt_message()]). +match_messages(Filter) -> + %% TODO: optimize later... + Fun = fun(#retained_message{topic = Name, msg = Msg}, Acc) -> + case emqttd_topic:match(Name, Filter) of + true -> [Msg|Acc]; + false -> Acc + end + end, + mnesia:async_dirty(fun mnesia:foldl/3, [Fun, [], retained_message]). + +-spec(delete_message(binary()) -> ok). +delete_message(Topic) -> + mnesia:dirty_delete(retained_message, Topic). + +-spec(expire_messages(pos_integer()) -> any()). +expire_messages(Time) when is_integer(Time) -> + mnesia:transaction( + fun() -> + Match = ets:fun2ms( + fun(#retained_message{topic = Topic, msg = #mqtt_message{timestamp = {MegaSecs, Secs, _}}}) + when Time > (MegaSecs * 1000000 + Secs) -> Topic + end), + Topics = mnesia:select(retained_message, Match, write), + lists:foreach(fun(<<"$SYS/", _/binary>>) -> ok; %% ignore $SYS/# messages + (Topic) -> mnesia:delete({retained_message, Topic}) + end, Topics) + end). + +-spec(retained_count() -> non_neg_integer()). +retained_count() -> mnesia:table_info(retained_message, size). + From 6beb6bbbe44a7bfa6071ede42be5ea43f005b318 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 18 Aug 2016 10:25:20 +0800 Subject: [PATCH 097/116] os:timestamp --- src/emqttd_message.erl | 6 +++--- src/emqttd_retainer.erl | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emqttd_message.erl b/src/emqttd_message.erl index e798f0fd8..4784e6331 100644 --- a/src/emqttd_message.erl +++ b/src/emqttd_message.erl @@ -42,7 +42,7 @@ make(From, Qos, Topic, Payload) -> qos = ?QOS_I(Qos), topic = Topic, payload = Payload, - timestamp = os:timestamp()}. + timestamp = emqttd_time:now_to_secs()}. %% @doc Message from Packet -spec(from_packet(mqtt_packet()) -> mqtt_message()). @@ -60,7 +60,7 @@ from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, dup = Dup, topic = Topic, payload = Payload, - timestamp = os:timestamp()}; + timestamp = emqttd_time:now_to_secs()}; from_packet(#mqtt_packet_connect{will_flag = false}) -> undefined; @@ -78,7 +78,7 @@ from_packet(#mqtt_packet_connect{client_id = ClientId, qos = Qos, dup = false, payload = Msg, - timestamp = os:timestamp()}. + timestamp = emqttd_time:now_to_secs()}. from_packet(ClientId, Packet) -> Msg = from_packet(Packet), diff --git a/src/emqttd_retainer.erl b/src/emqttd_retainer.erl index 005489e58..bce688e56 100644 --- a/src/emqttd_retainer.erl +++ b/src/emqttd_retainer.erl @@ -192,8 +192,8 @@ expire_messages(Time) when is_integer(Time) -> mnesia:transaction( fun() -> Match = ets:fun2ms( - fun(#retained_message{topic = Topic, msg = #mqtt_message{timestamp = {MegaSecs, Secs, _}}}) - when Time > (MegaSecs * 1000000 + Secs) -> Topic + fun(#retained_message{topic = Topic, msg = #mqtt_message{timestamp = Ts}}) + when Time > Ts -> Topic end), Topics = mnesia:select(retained_message, Match, write), lists:foreach(fun(<<"$SYS/", _/binary>>) -> ok; %% ignore $SYS/# messages From 354ab5ad21590434d0207907dab9edd0ec23129b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 18 Aug 2016 10:57:00 +0800 Subject: [PATCH 098/116] msgid -> id --- include/emqttd.hrl | 4 ++-- src/emqttd_message.erl | 10 +++++----- test/emqttd_protocol_SUITE.erl | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 4fd6749f0..364c9a28b 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -85,7 +85,7 @@ -type(mqtt_pktid() :: 1..16#ffff | undefined). -record(mqtt_message, { - msgid :: mqtt_msgid(), %% Global unique message ID + id :: mqtt_msgid(), %% Global unique message ID pktid :: mqtt_pktid(), %% PacketId from :: {binary(), undefined | binary()}, %% ClientId and Username topic :: binary(), %% Topic that the message is published to @@ -96,7 +96,7 @@ sys = false :: boolean(), %% $SYS flag headers = [] :: list(), payload :: binary(), %% Payload - timestamp :: erlang:timestamp() %% os:timestamp + timestamp :: pos_integer() %% os:timestamp to seconds }). -type(mqtt_message() :: #mqtt_message{}). diff --git a/src/emqttd_message.erl b/src/emqttd_message.erl index 4784e6331..7dfc4fdc6 100644 --- a/src/emqttd_message.erl +++ b/src/emqttd_message.erl @@ -37,7 +37,7 @@ make(From, Topic, Payload) -> -spec(make(msg_from(), mqtt_qos(), binary(), binary()) -> mqtt_message()). make(From, Qos, Topic, Payload) -> - #mqtt_message{msgid = msgid(), + #mqtt_message{id = msgid(), from = From, qos = ?QOS_I(Qos), topic = Topic, @@ -53,7 +53,7 @@ from_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, variable = #mqtt_packet_publish{topic_name = Topic, packet_id = PacketId}, payload = Payload}) -> - #mqtt_message{msgid = msgid(), + #mqtt_message{id = msgid(), pktid = PacketId, qos = Qos, retain = Retain, @@ -71,7 +71,7 @@ from_packet(#mqtt_packet_connect{client_id = ClientId, will_qos = Qos, will_topic = Topic, will_msg = Msg}) -> - #mqtt_message{msgid = msgid(), + #mqtt_message{id = msgid(), topic = Topic, from = {ClientId, Username}, retain = Retain, @@ -138,13 +138,13 @@ unset_flag(retain, Msg = #mqtt_message{retain = true}) -> unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. %% @doc Format MQTT Message -format(#mqtt_message{msgid = MsgId, pktid = PktId, from = {ClientId, Username}, +format(#mqtt_message{id = MsgId, pktid = PktId, from = {ClientId, Username}, qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s/~s, Topic=~s)", [i(Qos), i(Retain), i(Dup), MsgId, PktId, Username, ClientId, Topic]); %% TODO:... -format(#mqtt_message{msgid = MsgId, pktid = PktId, from = From, +format(#mqtt_message{id = MsgId, pktid = PktId, from = From, qos = Qos, retain = Retain, dup = Dup, topic =Topic}) -> io_lib:format("Message(Q~p, R~p, D~p, MsgId=~p, PktId=~p, From=~s, Topic=~s)", [i(Qos), i(Retain), i(Dup), MsgId, PktId, From, Topic]). diff --git a/test/emqttd_protocol_SUITE.erl b/test/emqttd_protocol_SUITE.erl index d9344786d..2d8490a57 100644 --- a/test/emqttd_protocol_SUITE.erl +++ b/test/emqttd_protocol_SUITE.erl @@ -337,9 +337,9 @@ packet_format(_) -> message_make(_) -> Msg = emqttd_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), 0 = Msg#mqtt_message.qos, - undefined = Msg#mqtt_message.msgid, + undefined = Msg#mqtt_message.id, Msg1 = emqttd_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), - true = is_binary(Msg1#mqtt_message.msgid), + true = is_binary(Msg1#mqtt_message.id), 2 = Msg1#mqtt_message.qos. message_from_packet(_) -> From 8f54572f9313348451345763351367d63190fa86 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 18 Aug 2016 15:54:02 +0800 Subject: [PATCH 099/116] run_hooks client.subscribe.after --- src/emqttd_session.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 0e2df6b2e..bf475e4f6 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -315,6 +315,8 @@ handle_cast({subscribe, TopicTable, AckFun}, Session = #session{client_id = %% Where the Topic Filter is not identical to any existing Subscription’s filter, %% a new Subscription is created and all matching retained messages are sent. emqttd_retainer:dispatch(Topic1, self()), + emqttd:run_hooks('client.subscribe.after', [{ClientId, Username}], {Topic1, Opts1}), + dict:store(Topic1, NewQos, SubDict) end}; {stop, _} -> @@ -323,7 +325,6 @@ handle_cast({subscribe, TopicTable, AckFun}, Session = #session{client_id = end end, {[], Subscriptions}, TopicTable), AckFun(lists:reverse(GrantedQos)), - %%emqttd:run_hooks('client.subscribe.after', [ClientId], TopicTable), hibernate(Session#session{subscriptions = Subscriptions1}); %%TODO: 2.0 FIX From 314aaf536fd49c9dcb1a728a8ec018fba88bfb7c Mon Sep 17 00:00:00 2001 From: huangdan Date: Sun, 21 Aug 2016 13:13:17 +0800 Subject: [PATCH 100/116] add export --- src/emqttd_retainer.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emqttd_retainer.erl b/src/emqttd_retainer.erl index bce688e56..435bd7abc 100644 --- a/src/emqttd_retainer.erl +++ b/src/emqttd_retainer.erl @@ -32,7 +32,7 @@ -copy_mnesia({mnesia, [copy]}). %% API Function Exports --export([retain/1, dispatch/2]). +-export([retain/1, read_messages/1, dispatch/2]). %% API Function Exports -export([start_link/0]). From 2f777ebe4cd8eba41f9392bdd7eb17afb2f8879e Mon Sep 17 00:00:00 2001 From: huangdan Date: Sun, 21 Aug 2016 13:14:00 +0800 Subject: [PATCH 101/116] test ct --- test/emqttd_SUITE.erl | 24 ++---------------------- test/emqttd_mock_client.erl | 2 +- test/emqttd_protocol_SUITE.erl | 4 +--- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index 93b1b380b..aa2a4ea34 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -58,9 +58,7 @@ groups() -> [add_delete_hook, run_hooks]}, {retainer, [sequence], - [retain_messages, - dispatch_retained_messages, - expire_retained_messages]}, + [dispatch_retained_messages]}, {backend, [sequence], []}, {cli, [sequence], @@ -302,14 +300,6 @@ hook_fun5(arg1, arg2, Acc, init) -> {stop, [r3 | Acc]}. %% Retainer Test %%-------------------------------------------------------------------- -retain_messages(_) -> - Msg = emqttd_message:make(<<"clientId">>, <<"topic">>, <<"payload">>), - emqttd_backend:retain_message(Msg), - [Msg] = emqttd_backend:read_messages(<<"topic">>), - [Msg] = emqttd_backend:match_messages(<<"topic/#">>), - emqttd_backend:delete_message(<<"topic">>), - 0 = emqttd_backend:retained_count(). - dispatch_retained_messages(_) -> Msg = #mqtt_message{retain = true, topic = <<"a/b/c">>, payload = <<"payload">>}, @@ -317,19 +307,9 @@ dispatch_retained_messages(_) -> emqttd_retainer:dispatch(<<"a/b/+">>, self()), ?assert(receive {dispatch, <<"a/b/+">>, Msg} -> true after 10 -> false end), emqttd_retainer:retain(#mqtt_message{retain = true, topic = <<"a/b/c">>, payload = <<>>}), - [] = emqttd_backend:read_messages(<<"a/b/c">>). - -expire_retained_messages(_) -> - Msg1 = emqttd_message:make(<<"clientId1">>, qos1, <<"topic/1">>, <<"payload1">>), - Msg2 = emqttd_message:make(<<"clientId2">>, qos2, <<"topic/2">>, <<"payload2">>), - emqttd_backend:retain_message(Msg1), - emqttd_backend:retain_message(Msg2), - timer:sleep(2000), - emqttd_backend:expire_messages(emqttd_time:now_to_secs()), - 0 = emqttd_backend:retained_count(). + [] = emqttd_retainer:read_messages(<<"a/b/c">>). -%%-------------------------------------------------------------------- %% CLI Group %%-------------------------------------------------------------------- diff --git a/test/emqttd_mock_client.erl b/test/emqttd_mock_client.erl index 7a9f90010..f4d26fa30 100644 --- a/test/emqttd_mock_client.erl +++ b/test/emqttd_mock_client.erl @@ -39,7 +39,7 @@ init([ClientId]) -> {ok, #state{clientid = ClientId}}. handle_call(start_session, _From, State = #state{clientid = ClientId}) -> - {ok, SessPid, _} = emqttd_sm:start_session(true, ClientId), + {ok, SessPid, _} = emqttd_sm:start_session(true, {ClientId, undefined}), {reply, {ok, SessPid}, State#state{session = SessPid}}; handle_call(stop, _From, State) -> diff --git a/test/emqttd_protocol_SUITE.erl b/test/emqttd_protocol_SUITE.erl index 2d8490a57..96b969278 100644 --- a/test/emqttd_protocol_SUITE.erl +++ b/test/emqttd_protocol_SUITE.erl @@ -337,7 +337,6 @@ packet_format(_) -> message_make(_) -> Msg = emqttd_message:make(<<"clientid">>, <<"topic">>, <<"payload">>), 0 = Msg#mqtt_message.qos, - undefined = Msg#mqtt_message.id, Msg1 = emqttd_message:make(<<"clientid">>, qos2, <<"topic">>, <<"payload">>), true = is_binary(Msg1#mqtt_message.id), 2 = Msg1#mqtt_message.qos. @@ -356,8 +355,7 @@ message_from_packet(_) -> Msg2 = emqttd_message:from_packet(<<"username">>, <<"clientid">>, ?PUBLISH_PACKET(1, <<"topic">>, 20, <<"payload">>)), - <<"clientid">> = Msg2#mqtt_message.from, - <<"username">> = Msg2#mqtt_message.sender, + {<<"clientid">>, <<"username">>} = Msg2#mqtt_message.from, io:format("~s", [emqttd_message:format(Msg2)]). message_flag(_) -> From 1eaa5bdfa64e366bbd62e5fc889ae319092fb044 Mon Sep 17 00:00:00 2001 From: huangdan Date: Sun, 21 Aug 2016 13:46:11 +0800 Subject: [PATCH 102/116] tab topic --- src/emqttd_router.erl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl index d77a86488..0056d9eb5 100644 --- a/src/emqttd_router.erl +++ b/src/emqttd_router.erl @@ -57,10 +57,18 @@ mnesia(boot) -> {type, bag}, {ram_copies, [node()]}, {record_name, mqtt_route}, - {attributes, record_info(fields, mqtt_route)}]); + {attributes, record_info(fields, mqtt_route)}]), + + ok = emqttd_mnesia:create_table(topic, [ + {ram_copies, [node()]}, + {record_name, mqtt_topic}, + {attributes, record_info(fields, mqtt_topic)}]); + +m mnesia(copy) -> - ok = emqttd_mnesia:copy_table(mqtt_route, ram_copies). + ok = emqttd_mnesia:copy_table(mqtt_route, ram_copies), + ok = emqttd_mnesia:copy_table(topic). %%-------------------------------------------------------------------- %% Start the Router From 7206e1862115e12ae545d141fd00f50ecea17773 Mon Sep 17 00:00:00 2001 From: huangdan Date: Sun, 21 Aug 2016 13:52:53 +0800 Subject: [PATCH 103/116] mqtt_topic --- include/emqttd.hrl | 10 ++++++++++ src/emqttd_router.erl | 2 -- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 364c9a28b..57fc55cb6 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -36,6 +36,16 @@ -define(SHARE, <<"$share/">>). %% Shared Topic +%%-------------------------------------------------------------------- +%% MQTT Topic +%%-------------------------------------------------------------------- +-record(mqtt_topic, { + topic :: binary(), + flags :: [retained | static] + }). + +-type mqtt_topic() :: #mqtt_topic{}. + %%-------------------------------------------------------------------- %% PubSub %%-------------------------------------------------------------------- diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl index 0056d9eb5..d3948a1b6 100644 --- a/src/emqttd_router.erl +++ b/src/emqttd_router.erl @@ -64,8 +64,6 @@ mnesia(boot) -> {record_name, mqtt_topic}, {attributes, record_info(fields, mqtt_topic)}]); -m - mnesia(copy) -> ok = emqttd_mnesia:copy_table(mqtt_route, ram_copies), ok = emqttd_mnesia:copy_table(topic). From 0816ec996698b4b3249bbc6153b31484df0e7cd0 Mon Sep 17 00:00:00 2001 From: huangdan Date: Sun, 21 Aug 2016 14:09:00 +0800 Subject: [PATCH 104/116] emqttd_app eunit --- src/emqttd_app.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index 899258c7b..2db44c351 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -209,8 +209,7 @@ stop_listener({listener, Protocol, ListenOn, _Opts}) -> esockd:close(Protocol, L -include_lib("eunit/include/eunit.hrl"). merge_sockopts_test_() -> Opts = [{acceptors, 16}, {max_clients, 512}], - ?_assert(merge_sockopts(Opts) == [{sockopts, [binary, {packet, raw}, {reuseaddr, true}, - {backlog, 512}, {nodelay, true}]}, {acceptors, 16}, {max_clients, 512}]). + ?_assert(merge_sockopts(Opts) == [{sockopts, ?MQTT_SOCKOPTS} | Opts]). load_all_mods_test_() -> ?_assert(load_all_mods() == ok). From 74efc0c095a50abca8c5f7a23568a28a2876d96a Mon Sep 17 00:00:00 2001 From: huangdan Date: Sun, 21 Aug 2016 16:12:08 +0800 Subject: [PATCH 105/116] http request test --- test/emqttd_SUITE.erl | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index aa2a4ea34..e8ecd2aea 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -32,6 +32,7 @@ all() -> {group, metrics}, {group, stats}, {group, hook}, + {group, http}, %%{group, backend}, {group, cli}]. @@ -61,6 +62,7 @@ groups() -> [dispatch_retained_messages]}, {backend, [sequence], []}, + {http, [sequence], [request_status]}, {cli, [sequence], [ctl_register_cmd, cli_status, @@ -310,7 +312,26 @@ dispatch_retained_messages(_) -> [] = emqttd_retainer:read_messages(<<"a/b/c">>). -%% CLI Group +%%-------------------------------------------------------------------- +%%http request Test +%%-------------------------------------------------------------------- + +request_status(_) -> + {InternalStatus, _ProvidedStatus} = init:get_status(), + AppStatus = + case lists:keysearch(emqttd, 1, application:which_applications()) of + false -> not_running; + {value, _Val} -> running + end, + Status = iolist_to_binary(io_lib:format("Node ~s is ~s~nemqttd is ~s", + [node(), InternalStatus, AppStatus])), + Url = "http://127.0.0.1:8083/status", + {ok, {{"HTTP/1.1", 200, "OK"}, _, Return}} = + httpc:request(get, {Url, []}, [], []), + ?assertEqual(binary_to_list(Status), Return). + + +%% cli group %%-------------------------------------------------------------------- ctl_register_cmd(_) -> From a1ed02499c873b8afb012343a95a1459f8c772e0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 21 Aug 2016 16:21:45 +0800 Subject: [PATCH 106/116] mqtt_topic --- include/emqttd.hrl | 21 +++++++++++---------- src/emqttd_router.erl | 21 +++++++++++---------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 57fc55cb6..0059d11a3 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -36,16 +36,6 @@ -define(SHARE, <<"$share/">>). %% Shared Topic -%%-------------------------------------------------------------------- -%% MQTT Topic -%%-------------------------------------------------------------------- --record(mqtt_topic, { - topic :: binary(), - flags :: [retained | static] - }). - --type mqtt_topic() :: #mqtt_topic{}. - %%-------------------------------------------------------------------- %% PubSub %%-------------------------------------------------------------------- @@ -54,6 +44,17 @@ -define(PUBSUB(PS), (PS =:= publish orelse PS =:= subscribe)). +%%-------------------------------------------------------------------- +%% MQTT Topic +%%-------------------------------------------------------------------- + +-record(mqtt_topic, { + topic :: binary(), + flags = [] :: [retained | static] +}). + +-type(mqtt_topic() :: #mqtt_topic{}). + %%-------------------------------------------------------------------- %% MQTT Client %%-------------------------------------------------------------------- diff --git a/src/emqttd_router.erl b/src/emqttd_router.erl index d3948a1b6..a58e599e0 100644 --- a/src/emqttd_router.erl +++ b/src/emqttd_router.erl @@ -53,20 +53,19 @@ %%-------------------------------------------------------------------- mnesia(boot) -> + ok = emqttd_mnesia:create_table(mqtt_topic, [ + {ram_copies, [node()]}, + {record_name, mqtt_topic}, + {attributes, record_info(fields, mqtt_topic)}]), ok = emqttd_mnesia:create_table(mqtt_route, [ {type, bag}, {ram_copies, [node()]}, {record_name, mqtt_route}, - {attributes, record_info(fields, mqtt_route)}]), - - ok = emqttd_mnesia:create_table(topic, [ - {ram_copies, [node()]}, - {record_name, mqtt_topic}, - {attributes, record_info(fields, mqtt_topic)}]); + {attributes, record_info(fields, mqtt_route)}]); mnesia(copy) -> - ok = emqttd_mnesia:copy_table(mqtt_route, ram_copies), - ok = emqttd_mnesia:copy_table(topic). + ok = emqttd_mnesia:copy_table(topic), + ok = emqttd_mnesia:copy_table(mqtt_route, ram_copies). %%-------------------------------------------------------------------- %% Start the Router @@ -126,7 +125,8 @@ add_route_(Route = #mqtt_route{topic = Topic}) -> true -> emqttd_trie:insert(Topic); false -> ok end, - mnesia:write(Route); + mnesia:write(Route), + mnesia:write(#mqtt_topic{topic = Topic}); Records -> case lists:member(Route, Records) of true -> ok; @@ -164,7 +164,8 @@ del_route_(Route = #mqtt_route{topic = Topic}) -> case emqttd_topic:wildcard(Topic) of true -> emqttd_trie:delete(Topic); false -> ok - end; + end, + mnesia:delete({mqtt_topic, Topic}); _More -> %% Remove route only mnesia:delete_object(Route) From a318244be03f33b15a5bc94e8a0d3e4b005cbd75 Mon Sep 17 00:00:00 2001 From: huangdan Date: Mon, 22 Aug 2016 15:22:57 +0800 Subject: [PATCH 107/116] http publish --- test/emqttd_SUITE.erl | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index e8ecd2aea..69047c11c 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -22,6 +22,8 @@ -include_lib("eunit/include/eunit.hrl"). +-define(CONTENT_TYPE, "application/x-www-form-urlencoded"). + all() -> [{group, protocol}, {group, pubsub}, @@ -62,7 +64,10 @@ groups() -> [dispatch_retained_messages]}, {backend, [sequence], []}, - {http, [sequence], [request_status]}, + {http, [sequence], + [request_status, + request_publish + ]}, {cli, [sequence], [ctl_register_cmd, cli_status, @@ -330,6 +335,32 @@ request_status(_) -> httpc:request(get, {Url, []}, [], []), ?assertEqual(binary_to_list(Status), Return). +request_publish(_) -> + ok = emqttd:subscribe(<<"a/b/c">>, self(), [{qos, 1}]), + Params = "qos=1&retain=0&topic=a/b/c&message=hello", + ?assert(connect_emqttd_publish_(post, "mqtt/publish", Params, auth_header_("", ""))), + ?assert(receive {dispatch, <<"a/b/c">>, _} -> true after 2 -> false end), + emqttd:unsubscribe(<<"a/b/c">>). + +connect_emqttd_publish_(Method, Api, Params, Auth) -> + Url = "http://127.0.0.1:8083/" ++ Api, + case httpc:request(Method, {Url, [Auth], ?CONTENT_TYPE, Params}, [], []) of + {error, socket_closed_remotely} -> + false; + {ok, {{"HTTP/1.1", 200, "OK"}, _, _Return} } -> + true; + {ok, {{"HTTP/1.1", 400, _}, _, []}} -> + false; + {ok, {{"HTTP/1.1", 404, _}, _, []}} -> + false + end. + +auth_header_(User, Pass) -> + Encoded = base64:encode_to_string(lists:append([User,":",Pass])), + {"Authorization","Basic " ++ Encoded}. + + + %% cli group %%-------------------------------------------------------------------- From b0f082ebe073dfffcafa512be970c8c4984461b7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Aug 2016 10:13:13 +0800 Subject: [PATCH 108/116] to_hexstr/1, from_hexstr/1 --- src/emqttd_guid.erl | 8 +++++++- test/emqttd_lib_SUITE.erl | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/emqttd_guid.erl b/src/emqttd_guid.erl index d9593d3a0..6dab23afe 100644 --- a/src/emqttd_guid.erl +++ b/src/emqttd_guid.erl @@ -29,7 +29,7 @@ %% @end -module(emqttd_guid). --export([gen/0, new/0, timestamp/1]). +-export([gen/0, new/0, timestamp/1, to_hexstr/1, from_hexstr/1]). -define(MAX_SEQ, 16#FFFF). @@ -120,3 +120,9 @@ npid() -> PidByte3:8, PidByte4:8>>, NPid. +to_hexstr(<>) -> + list_to_binary(integer_to_list(I, 16)). + +from_hexstr(S) -> + I = list_to_integer(binary_to_list(S), 16), <>. + diff --git a/test/emqttd_lib_SUITE.erl b/test/emqttd_lib_SUITE.erl index 1a3b1aef6..ec68294c6 100644 --- a/test/emqttd_lib_SUITE.erl +++ b/test/emqttd_lib_SUITE.erl @@ -16,6 +16,8 @@ -module(emqttd_lib_SUITE). +-include_lib("eunit/include/eunit.hrl"). + -compile(export_all). -define(SOCKOPTS, [ @@ -35,7 +37,7 @@ all() -> [{group, guid}, {group, opts}, {group, node}, {group, base62}]. groups() -> - [{guid, [], [guid_gen]}, + [{guid, [], [guid_gen, guid_hexstr]}, {opts, [], [opts_merge]}, {?PQ, [], [priority_queue_plen, priority_queue_out2]}, @@ -56,6 +58,10 @@ guid_gen(_) -> Ts2 = emqttd_guid:timestamp(emqttd_guid:gen()), true = Ts2 > Ts1. +guid_hexstr(_) -> + Guid = emqttd_guid:gen(), + ?assertEqual(Guid, emqttd_guid:from_hexstr(emqttd_guid:to_hexstr(Guid))). + %%-------------------------------------------------------------------- %% emqttd_opts %%-------------------------------------------------------------------- From dbed5b914a111bf878d2af9aa8ba27327089ac4b Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 27 Aug 2016 15:44:22 +0800 Subject: [PATCH 109/116] base62 --- src/emqttd_guid.erl | 14 +++++++++++++- test/emqttd_lib_SUITE.erl | 12 +++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/emqttd_guid.erl b/src/emqttd_guid.erl index d9593d3a0..03a4e6904 100644 --- a/src/emqttd_guid.erl +++ b/src/emqttd_guid.erl @@ -29,7 +29,7 @@ %% @end -module(emqttd_guid). --export([gen/0, new/0, timestamp/1]). +-export([gen/0, new/0, timestamp/1, to_hexstr/1, from_hexstr/1, to_base62/1, from_base62/1]). -define(MAX_SEQ, 16#FFFF). @@ -120,3 +120,15 @@ npid() -> PidByte3:8, PidByte4:8>>, NPid. +to_hexstr(<>) -> + list_to_binary(integer_to_list(I, 16)). + +from_hexstr(S) -> + I = list_to_integer(binary_to_list(S), 16), <>. + +to_base62(<>) -> + emqttd_base62:encode(I). + +from_base62(S) -> + I = emqttd_base62:decode(S), <>. + diff --git a/test/emqttd_lib_SUITE.erl b/test/emqttd_lib_SUITE.erl index 1a3b1aef6..51dd499ba 100644 --- a/test/emqttd_lib_SUITE.erl +++ b/test/emqttd_lib_SUITE.erl @@ -16,6 +16,8 @@ -module(emqttd_lib_SUITE). +-include_lib("eunit/include/eunit.hrl"). + -compile(export_all). -define(SOCKOPTS, [ @@ -35,7 +37,7 @@ all() -> [{group, guid}, {group, opts}, {group, node}, {group, base62}]. groups() -> - [{guid, [], [guid_gen]}, + [{guid, [], [guid_gen, guid_hexstr, guid_base62]}, {opts, [], [opts_merge]}, {?PQ, [], [priority_queue_plen, priority_queue_out2]}, @@ -56,6 +58,14 @@ guid_gen(_) -> Ts2 = emqttd_guid:timestamp(emqttd_guid:gen()), true = Ts2 > Ts1. +guid_hexstr(_) -> + Guid = emqttd_guid:gen(), + ?assertEqual(Guid, emqttd_guid:from_hexstr(emqttd_guid:to_hexstr(Guid))). + +guid_base62(_) -> + Guid = emqttd_guid:gen(), + ?assertEqual(Guid, emqttd_guid:from_base62(emqttd_guid:to_base62(Guid))). + %%-------------------------------------------------------------------- %% emqttd_opts %%-------------------------------------------------------------------- From 19fd92b41baf799aabe9a110cb38c22a24abdd1a Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Aug 2016 13:57:12 +0800 Subject: [PATCH 110/116] Send Packet... --- src/emqttd_client.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqttd_client.erl b/src/emqttd_client.erl index 2a8859d0c..7b7dedcf9 100644 --- a/src/emqttd_client.erl +++ b/src/emqttd_client.erl @@ -88,10 +88,9 @@ init([OriginConn, MqttEnv]) -> ConnName = esockd_net:format(PeerName), Self = self(), - %%TODO: Send packet... + %% Send Packet... SendFun = fun(Packet) -> Data = emqttd_serializer:serialize(Packet), - %%TODO: How to Log??? ?LOG(debug, "SEND ~p", [Data], #client_state{connname = ConnName}), emqttd_metrics:inc('bytes/sent', size(Data)), try Connection:async_send(Data) of From ff8d074576a391563158b908d0a3a813447de1fe Mon Sep 17 00:00:00 2001 From: Feng Date: Mon, 29 Aug 2016 14:15:45 +0800 Subject: [PATCH 111/116] HTTP Request Test --- test/emqttd_SUITE.erl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/emqttd_SUITE.erl b/test/emqttd_SUITE.erl index 69047c11c..06923ee8e 100644 --- a/test/emqttd_SUITE.erl +++ b/test/emqttd_SUITE.erl @@ -318,7 +318,7 @@ dispatch_retained_messages(_) -> %%-------------------------------------------------------------------- -%%http request Test +%% HTTP Request Test %%-------------------------------------------------------------------- request_status(_) -> @@ -359,10 +359,8 @@ auth_header_(User, Pass) -> Encoded = base64:encode_to_string(lists:append([User,":",Pass])), {"Authorization","Basic " ++ Encoded}. - - - -%% cli group +%%-------------------------------------------------------------------- +%% Cli group %%-------------------------------------------------------------------- ctl_register_cmd(_) -> From e2415a598b40c47acd2b23523994b9e59934f503 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 30 Aug 2016 13:32:17 +0800 Subject: [PATCH 112/116] EMQ 2.0 Documentation --- docs/source/changes.rst | 114 ++++- docs/source/cluster.rst | 2 +- docs/source/commands.rst | 22 +- docs/source/conf.py | 6 +- docs/source/config.rst | 945 ++++++++++++++++++------------------- docs/source/design.rst | 40 ++ docs/source/getstarted.rst | 79 ++-- docs/source/index.rst | 2 - docs/source/install.rst | 62 +-- docs/source/plugins.rst | 619 ++++++++++++------------ 10 files changed, 1008 insertions(+), 883 deletions(-) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 9d4436c57..ce5bd6191 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -11,11 +11,119 @@ Changes Version 2.0 (West of West Lake) ------------------------------- -*Release Date: 2016-08-29* +*Release Date: 2016-08-30* -Improve the design of PubSub and Router: +*Release Name: West of West Lake* -.. images:: _static/images/publish.png +.. NOTE:: Dont' upgrade 1.x production deployment to 2.0-beta1 release. + +EMQ - Shortened Project Name +---------------------------- + +Adopt a shortened projectname: EMQ(Erlang/Enterprise/Elastic MQTT Broker),E means Erlang/OTP, Enterprise and Elastic. + +Improve the Release Management +------------------------------ + +In order to iterate the project fast, we will adopt a new release management strategy since 2.0. There will be two or three 'Preview Release' named beta1, beta2 or beta3, and then one or two 'Release Candidate' named rc1, rc2 before a Major version is production ready. + +Seperate Rel from Application +----------------------------- + +We split the emqttd 1.x project into two projects since 2.0-beta1 release to resolve the plugins' dependency issue. + +A new project named `emqttd-relx`_ is created and responsible for buiding the emqttd application and the plugins:: + + git clone https://github.com/emqtt/emqttd-relx.git + + cd emqttd-relx && make + + cd _rel/emqttd && ./bin/emqttd console + +erlang.mk and relx +------------------ + +The rebar which is used in 1.x release is replaced by `erlang.mk`_ and `relx`_ tools since 2.0-beta1 release. + +You can check the 'Makefile' and 'relx.config' in the release project of the borker: `emqttd-relx`_ . + +Improve Git Branches Management +------------------------------- + ++------------+-------------------------------------------+ +| stable | 1.x Stable Branch | ++------------+-------------------------------------------+ +| master | 2.x Master Branch | ++------------+-------------------------------------------+ +| emq10 | 1.x Developement Branch | ++------------+-------------------------------------------+ +| emq20 | 2.x Development Branch | ++------------+-------------------------------------------+ +| emq30 | 3.x Development Branch | ++------------+-------------------------------------------+ +| issue#{id} | BugFix Branch | ++------------+-------------------------------------------+ + +New Config Syntax +----------------- + +Since 2.0-beta1 release the configuration file of the broker and plugins adopt a new syntax like rebar.config or relx.config: + +etc/emqttd.conf for example:: + + %% Max ClientId Length Allowed. + {mqtt_max_clientid_len, 512}. + + %% Max Packet Size Allowed, 64K by default. + {mqtt_max_packet_size, 65536}. + + %% Client Idle Timeout. + {mqtt_client_idle_timeout, 30}. % Second + +MQTT-SN Protocol Plugin +----------------------- + +The MQTT-SN Protocol Plugin `emqttd_sn`_ has been ready in 2.0-beta1 release. The default UDP port of MQTT-SN is 1884. + +Load the plugin:: + + ./bin/emqttd_ctl plugins load emqttd_sn + +Improve Design of PubSub and Router +----------------------------------- + +.. image:: _static/images/publish.png + +Improve Plugin Management +------------------------- + +The plugin of EMQ 2.0 broker is a normal erlang application which depends on and extends 'emqttd'. User can create a standalone plugin application project, and add it to `emqttd-relx`_ Makefiel as a dependency. + +All the plugins' config files will be copied to emqttd/etc/plugins/ folder when making emqttd brinary packages in `emqttd-relx`_ project:: + + ▾ emqttd/ + ▾ etc/ + ▸ modules/ + ▾ plugins/ + emqtt_coap.conf + emqttd.conf + emqttd_auth_http.conf + emqttd_auth_mongo.conf + emqttd_auth_mysql.conf + emqttd_auth_pgsql.conf + emqttd_auth_redis.conf + emqttd_coap.conf + emqttd_dashboard.conf + emqttd_plugin_template.conf + emqttd_recon.conf + emqttd_reloader.conf + emqttd_sn.conf + emqttd_stomp.conf + +EMQ 2.0 Documentation +--------------------- + +http://emqtt.io/docs/v2/index.html or http://docs.emqtt.com/emq20/ .. _release_1.1.3: diff --git a/docs/source/cluster.rst b/docs/source/cluster.rst index 8ebd992ba..96ef37070 100644 --- a/docs/source/cluster.rst +++ b/docs/source/cluster.rst @@ -245,7 +245,7 @@ The Firewall If there is a firewall between clustered nodes, the cluster requires to open 4369 port used by epmd daemon, and a port segment for nodes' communication. -Configure the port segment in etc/emqttd.config, for example: +Configure the port segment in releases/2.0/sys.config, for example: .. code-block:: erlang diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 7b4243c96..c4e8645c5 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -20,7 +20,7 @@ Show running status of the broker:: $ ./bin/emqttd_ctl status Node 'emqttd@127.0.0.1' is started - emqttd 1.1 is running + emqttd 2.0 is running .. _command_broker:: @@ -382,10 +382,6 @@ Query the subscription table of the broker: +--------------------------------------------+--------------------------------------+ | subscriptions show | Show a subscription | +--------------------------------------------+--------------------------------------+ -| subscriptions add | Add a static subscription manually | -+--------------------------------------------+--------------------------------------+ -| subscriptions del | Remove a static subscription manually| -+--------------------------------------------+--------------------------------------+ subscriptions list ------------------ @@ -415,22 +411,6 @@ Show the subscriptions of a MQTT client:: clientid: [{<<"x">>,1},{<<"topic2">>,1},{<<"topic3">>,1}] -subscriptions add ------------------------------------------- - -Add a static subscription manually:: - - $ ./bin/emqttd_ctl subscriptions add clientid new_topic 1 - ok - -subscriptions del ------------------------------------- - -Remove a static subscription manually:: - - $ ./bin/emqttd_ctl subscriptions del clientid new_topic - ok - .. _command_plugins:: ------- diff --git a/docs/source/conf.py b/docs/source/conf.py index f96ed0579..3fafdfd06 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -48,7 +48,7 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'Erlang MQTT Broker' +project = u'EMQ 2.0 - Erlang MQTT Broker' copyright = u'2016, Feng Lee' # The version info for the project you're documenting, acts as replacement for @@ -56,9 +56,9 @@ copyright = u'2016, Feng Lee' # built documents. # # The short X.Y version. -version = '1.0' +version = '2.0' # The full version, including alpha/beta/rc tags. -release = '1.0' +release = '2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/config.rst b/docs/source/config.rst index 7ddab18d9..f109a246b 100644 --- a/docs/source/config.rst +++ b/docs/source/config.rst @@ -5,24 +5,66 @@ Configuration ============= -Configuration files of the broker are under 'etc/' folder, including: +The two main configuration files of the broker are under 'etc/' folder: +-------------------+-----------------------------------+ | File | Description | +-------------------+-----------------------------------+ | etc/vm.args | Erlang VM Arguments | +-------------------+-----------------------------------+ -| etc/emqttd.config | emqttd broker Config | -+-------------------+-----------------------------------+ -| etc/acl.config | ACL Config | -+-------------------+-----------------------------------+ -| etc/clients.config| ClientId Authentication | -+-------------------+-----------------------------------+ -| etc/rewrite.config| Rewrite Rules | -+-------------------+-----------------------------------+ -| etc/ssl/* | SSL certificate and key files | +| etc/emqttd.conf | emqttd broker Config | +-------------------+-----------------------------------+ +---------------------------- +Plugins' Configuration Files +---------------------------- + ++----------------------------------------+-----------------------------------+ +| File | Description | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_auth_http.conf | HTTP Auth/ACL Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_auth_mongo.conf | MongoDB Auth/ACL Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_auth_mysql.conf | MySQL Auth/ACL Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_auth_pgsql.conf | Postgre Auth/ACL Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_auth_redis.conf | Redis Auth/ACL Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_coap.conf | CoAP Protocol Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_dashboard.conf | Dashboard Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_plugin_template.conf| Template Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_recon.conf | Recon Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_reloader.conf | Reloader Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_sn.conf | MQTT-SN Protocal Plugin Config | ++----------------------------------------+-----------------------------------+ +| etc/plugins/emqttd_stomp.conf | Stomp Protocl Plugin Config | ++----------------------------------------+-----------------------------------+ + +---------------------------- +Modules' Configuration Files +---------------------------- + +The modules' configuration files are in etc/modules/ folder, and referrenced by etc/emqttd.conf: + ++----------------------------+-----------------------------------+ +| File | Description | ++----------------------------+-----------------------------------+ +| etc/modules/acl.config | Internal ACL Rules | ++----------------------------+-----------------------------------+ +| etc/modules/client.config | Config for ClientId Auth Module | ++----------------------------+-----------------------------------+ +| etc/modules/rewrite.config | Config for Rewrite Module | ++----------------------------+-----------------------------------+ +| etc/ssl/* | SSL Certfile and Keyfile | ++-----------------------------+----------------------------------+ + ----------- etc/vm.args ----------- @@ -102,44 +144,11 @@ The name and cookie of Erlang Node should be configured when clustering:: ## Cookie for distributed erlang -setcookie emqttdsecretcookie ------------------ -etc/emqttd.config ------------------ - -This is the main emqttd broker configuration file. - -File Syntax ------------ - -The file use the standard Erlang config syntax and consists of a list of erlang applications and their environments. - -.. code-block:: erlang - - [{kernel, [ - {start_timer, true}, - {start_pg2, true} - ]}, - {sasl, [ - {sasl_error_logger, {file, "log/emqttd_sasl.log"}} - ]}, - - ... - - {emqttd, [ - ... - ]} - ]. - -The file adopts Erlang Term Syntax: - -1. [ ]: List, seperated by comma -2. { }: Tuple, Usually {Env, Value} -3. % : comment - +------------------ Log Level and File ------------------ -Logger of emqttd broker is implemented by 'lager' application: +Logger of emqttd broker is implemented by 'lager' application, which is configured in releases/2.0/sys.config: .. code-block:: erlang @@ -173,44 +182,47 @@ Configure log handlers: ]} ]} -emqttd Application ------------------- +--------------- +etc/emqttd.conf +--------------- -The MQTT broker is implemented by erlang 'emqttd' application: +This is the main configuration file for emqttd broker. + +File Syntax +----------- + +The file uses the Erlang term syntax which is like rebar.config or relx.config: + +1. [ ]: List, seperated by comma +2. { }: Tuple, Usually {Env, Value} +3. % : comment + +MQTT Protocol Parameters +------------------------ + +Maximum ClientId Length +....................... .. code-block:: erlang - {emqttd, [ - %% Authentication and Authorization - {access, [ - ... - ]}, - %% MQTT Protocol Options - {mqtt, [ - ... - ]}, - %% Broker Options - {broker, [ - ... - ]}, - %% Modules - {modules, [ - ... - ]}, - %% Plugins - {plugins, [ - ... - ]}, + %% Max ClientId Length Allowed. + {mqtt_max_clientid_len, 512}. - %% Listeners - {listeners, [ - ... - ]}, +Maximum Packet Size +................... - %% Erlang System Monitor - {sysmon, [ - ]} - ]} +.. code-block:: erlang + + %% Max Packet Size Allowed, 64K by default. + {mqtt_max_packet_size, 65536}. + +MQTT Client Idle Timeout +........................ + +.. code-block:: erlang + + %% Client Idle Timeout. + {mqtt_client_idle_timeout, 30}. % Second Pluggable Authentication ------------------------ @@ -221,54 +233,44 @@ The broker provides Username, ClientId, LDAP and anonymous authentication module .. code-block:: erlang - %% Authetication. Anonymous Default - {auth, [ - %% Authentication with username, password - %% Add users: ./bin/emqttd_ctl users add Username Password - %% {username, [{"test", "public"}]}, + %%-------------------------------------------------------------------- + %% Authentication + %%-------------------------------------------------------------------- - %% Authentication with clientid - % {clientid, [{password, no}, {file, "etc/clients.config"}]}, + %% Anonymous: Allow all + {auth, anonymous, []}. - %% Authentication with LDAP - % {ldap, [ - % {servers, ["localhost"]}, - % {port, 389}, - % {timeout, 30}, - % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - % {ssl, fasle}, - % {sslopts, [ - % {"certfile", "ssl.crt"}, - % {"keyfile", "ssl.key"}]} - % ]}, + %% Authentication with username, password + {auth, username, [{passwd, "etc/modules/passwd.conf"}]}. - %% Allow all - {anonymous, []} - ]}, + %% Authentication with clientId + {auth, clientid, [{config, "etc/modules/client.conf"}, {password, no}]}. The modules enabled at the same time compose an authentication chain:: - ---------------- ---------------- ------------- - Client --> | Username | -ignore-> | ClientID | -ignore-> | Anonymous | - ---------------- ---------------- ------------- + ---------------- ---------------- -------------- + Client --> | Anonymous | -ignore-> | Username | -ignore-> | ClientID | + ---------------- ---------------- -------------- | | | \|/ \|/ \|/ allow | deny allow | deny allow | deny -.. NOTE:: There are also MySQL、PostgreSQL、Redis、MongoDB Authentication Plugins. +.. NOTE:: There are also MySQL, Postgre, Redis, MongoDB and HTTP Authentication Plugins. Username Authentication ....................... .. code-block:: erlang - {username, [{client1, "passwd1"}, {client2, "passwd2"}]}, + %% Authentication with username, password + {auth, username, [{passwd, "etc/modules/passwd.conf"}]}. Two ways to configure users: -1. Configure username and plain password directly:: +1. Configure username and plain password in etc/modules/passwd.conf:: - {username, [{client1, "passwd1"}, {client2, "passwd2"}]}, + {"user1", "passwd1"}. + {"user2", "passwd2"}. 2. Add user by './bin/emqttd_ctl users' command:: @@ -279,388 +281,34 @@ ClientID Authentication .. code-block:: erlang - {clientid, [{password, no}, {file, "etc/clients.config"}]}, + %% Authentication with clientId + {auth, clientid, [{config, "etc/modules/client.conf"}, {password, no}]}. Configure ClientIDs in etc/clients.config:: - testclientid0 - testclientid1 127.0.0.1 - testclientid2 192.168.0.1/24 - -LDAP Authentication -................... - -.. code-block:: erlang - - {ldap, [ - {servers, ["localhost"]}, - {port, 389}, - {timeout, 30}, - {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - {ssl, fasle}, - {sslopts, [ - {"certfile", "ssl.crt"}, - {"keyfile", "ssl.key"}]} - ]}, - + "testclientid0". + {"testclientid1", "127.0.0.1"}. + {"testclientid2", "192.168.0.1/24"}. Anonymous Authentication ........................ Allow any client to connect to the broker:: - {anonymous, []} + %% Anonymous: Allow all + {auth, anonymous, []}. - -ACL ---- +ACL(Authorization) +------------------ Enable the default ACL module: .. code-block:: erlang - {acl, [ - %% Internal ACL module - {internal, [{file, "etc/acl.config"}, {nomatch, allow}]} - ]} + %% Internal ACL config + {acl, internal, [{config, "etc/modules/acl.conf"}, {nomatch, allow}]}. -MQTT Packet and ClientID ------------------------- - -.. code-block:: erlang - - {packet, [ - - %% Max ClientId Length Allowed - {max_clientid_len, 1024}, - - %% Max Packet Size Allowed, 64K default - {max_packet_size, 65536} - ]}, - -MQTT Client Idle Timeout ------------------------- - -.. code-block:: erlang - - {client, [ - %% Socket is connected, but no 'CONNECT' packet received - {idle_timeout, 10} - ]}, - -MQTT Session ------------- - -.. code-block:: erlang - - {session, [ - %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - %% 0 means no limit - {max_inflight, 100}, - - %% Retry interval for unacked QoS1/2 messages. - {unack_retry_interval, 20}, - - %% Awaiting PUBREL Timeout - {await_rel_timeout, 20}, - - %% Max Packets that Awaiting PUBREL, 0 means no limit - {max_awaiting_rel, 0}, - - %% Interval of Statistics Collection(seconds) - {collect_interval, 20}, - - %% Expired after 2 day (unit: minute) - {expired_after, 2880} - - ]}, - -Session parameters: - -+----------------------+----------------------------------------------------------+ -| max_inflight | Max number of QoS1/2 messages that can be delivered in | -| | the same time | -+----------------------+----------------------------------------------------------+ -| unack_retry_interval | Retry interval for unacked QoS1/2 messages. | -+----------------------+----------------------------------------------------------+ -| await_rel_timeout | Awaiting PUBREL Timeout | -+----------------------+----------------------------------------------------------+ -| max_awaiting_rel | Max number of Packets that Awaiting PUBREL | -+----------------------+----------------------------------------------------------+ -| collect_interval | Interval of Statistics Collection | -+----------------------+----------------------------------------------------------+ -| expired_after | Expired after (unit: minute) | -+----------------------+----------------------------------------------------------+ - -MQTT Message Queue ------------------- - -The message queue of session stores: - -1. Offline messages for persistent session. - -2. Pending messages for inflight window is full - -Queue parameters: - -.. code-block:: erlang - - {queue, [ - %% simple | priority - {type, simple}, - - %% Topic Priority: 0~255, Default is 0 - %% {priority, [{"topic/1", 10}, {"topic/2", 8}]}, - - %% Max queue length. Enqueued messages when persistent client disconnected, - %% or inflight window is full. - {max_length, infinity}, - - %% Low-water mark of queued messages - {low_watermark, 0.2}, - - %% High-water mark of queued messages - {high_watermark, 0.6}, - - %% Queue Qos0 messages? - {queue_qos0, true} - ]} - -+----------------------+---------------------------------------------------+ -| type | Queue type: simple or priority | -+----------------------+---------------------------------------------------+ -| priority | Topic priority | -+----------------------+---------------------------------------------------+ -| max_length | Max Queue size, infinity means no limit | -+----------------------+---------------------------------------------------+ -| low_watermark | Low watermark | -+----------------------+---------------------------------------------------+ -| high_watermark | High watermark | -+----------------------+---------------------------------------------------+ -| queue_qos0 | If Qos0 message queued? | -+----------------------+---------------------------------------------------+ - -Sys Interval of Broker ------------------------ - -.. code-block:: erlang - - %% System interval of publishing $SYS messages - {sys_interval, 60}, - -Retained messages ------------------ - -.. code-block:: erlang - - {retained, [ - %% Expired after seconds, never expired if 0 - {expired_after, 0}, - - %% Maximum number of retained messages - {max_message_num, 100000}, - - %% Max Payload Size of retained message - {max_playload_size, 65536} - ]}, - -PubSub and Router ------------------ - -.. code-block:: erlang - - {pubsub, [ - %% PubSub Pool - {pool_size, 8}, - - %% Subscription: true | false - {subscription, true}, - - %% Route aging time(seconds) - {route_aging, 5} - ]}, - -Bridge Parameters ------------------ - -.. code-block:: erlang - - {bridge, [ - %% Bridge Queue Size - {max_queue_len, 10000}, - - %% Ping Interval of bridge node - {ping_down_interval, 1} - ]} - - -Enable Modules --------------- - -'presence' module will publish presence message to $SYS topic when a client connected or disconnected:: - - {presence, [{qos, 0}]}, - -'subscription' module forces the client to subscribe some topics when connected to the broker: - -.. code-block:: erlang - - %% Subscribe topics automatically when client connected - {subscription, [ - %% Subscription from stored table - stored, - - %% $u will be replaced with username - {"$Q/username/$u", 1}, - - %% $c will be replaced with clientid - {"$Q/client/$c", 1} - ]} - -'rewrite' module supports to rewrite the topic path: - -.. code-block:: erlang - - %% Rewrite rules - {rewrite, [{file, "etc/rewrite.config"}]} - -Plugins Folder --------------- - -.. code-block:: erlang - - {plugins, [ - %% Plugin App Library Dir - {plugins_dir, "./plugins"}, - - %% File to store loaded plugin names. - {loaded_file, "./data/loaded_plugins"} - ]}, - - -TCP Listeners -------------- - -Configure the TCP listeners for MQTT, MQTT(SSL) and HTTP Protocols. - -The most important parameter is 'max_clients' - max concurrent clients allowed. - -The TCP Ports occupied by emqttd broker by default: - -+-----------+-----------------------------------+ -| 1883 | MQTT Port | -+-----------+-----------------------------------+ -| 8883 | MQTT(SSL) Port | -+-----------+-----------------------------------+ -| 8083 | MQTT(WebSocket), HTTP API Port | -+-----------+-----------------------------------+ - -.. code-block:: erlang - - {listeners, [ - - {mqtt, 1883, [ - %% Size of acceptor pool - {acceptors, 16}, - - %% Maximum number of concurrent clients - {max_clients, 8192}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% Connection Options - {connopts, [ - %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec - %% {rate_limit, "100,10"} %% 100K burst, 10K rate - ]}, - - %% Socket Options - {sockopts, [ - %Set buffer if hight thoughtput - %{recbuf, 4096}, - %{sndbuf, 4096}, - %{buffer, 4096}, - %{nodelay, true}, - {backlog, 1024} - ]} - ]}, - - {mqtts, 8883, [ - %% Size of acceptor pool - {acceptors, 4}, - - %% Maximum number of concurrent clients - {max_clients, 512}, - - %% Socket Access Control - {access, [{allow, all}]}, - - %% SSL certificate and key files - {ssl, [{certfile, "etc/ssl/ssl.crt"}, - {keyfile, "etc/ssl/ssl.key"}]}, - - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]}, - %% WebSocket over HTTPS Listener - %% {https, 8083, [ - %% %% Size of acceptor pool - %% {acceptors, 4}, - %% %% Maximum number of concurrent clients - %% {max_clients, 512}, - %% %% Socket Access Control - %% {access, [{allow, all}]}, - %% %% SSL certificate and key files - %% {ssl, [{certfile, "etc/ssl/ssl.crt"}, - %% {keyfile, "etc/ssl/ssl.key"}]}, - %% %% Socket Options - %% {sockopts, [ - %% %{buffer, 4096}, - %% {backlog, 1024} - %% ]} - %%]}, - - %% HTTP and WebSocket Listener - {http, 8083, [ - %% Size of acceptor pool - {acceptors, 4}, - %% Maximum number of concurrent clients - {max_clients, 64}, - %% Socket Access Control - {access, [{allow, all}]}, - %% Socket Options - {sockopts, [ - {backlog, 1024} - %{buffer, 4096}, - ]} - ]} - ]}, - -Listener Parameters: - -+-------------+----------------------------------------------------------------+ -| acceptors | TCP Acceptor Pool | -+-------------+----------------------------------------------------------------+ -| max_clients | Maximum number of concurrent TCP connections allowed | -+-------------+----------------------------------------------------------------+ -| access | Access Control by IP, for example: [{allow, "192.168.1.0/24"}] | -+-------------+----------------------------------------------------------------+ -| connopts | Rate Limit Control, for example: {rate_limit, "100,10"} | -+-------------+----------------------------------------------------------------+ -| sockopts | TCP Socket parameters | -+-------------+----------------------------------------------------------------+ - -.. _config_acl: - --------------- -etc/acl.config --------------- - -The 'etc/acl.config' is the default ACL config for emqttd broker. The rules by default: +Define ACL rules in etc/modules/acl.conf. The rules by default: .. code-block:: erlang @@ -686,35 +334,194 @@ An ACL rule is an Erlang tuple. The Access control module of emqttd broker match \|/ \|/ \|/ allow | deny allow | deny allow | deny -.. _config_rewrite: - ------------------- -etc/clients.config ------------------- - -Enable ClientId Authentication in 'etc/emqttd.config': +Sys Interval of Broker +---------------------- .. code-block:: erlang - {auth, [ - %% Authentication with clientid - {clientid, [{password, no}, {file, "etc/clients.config"}]} - ]}, + %% System interval of publishing $SYS messages + {broker_sys_interval, 60}. -Configure all allowed ClientIDs, IP Addresses in etc/clients.config:: +Retained Message Configuration +------------------------------ - testclientid0 - testclientid1 127.0.0.1 - testclientid2 192.168.0.1/24 +Expiration of Retained Message +............................... ------------------- -etc/rewrite.config ------------------- +.. code:: erlang -The Rewrite Rules for emqttd_mod_rewrite: + %% Expired after seconds, never expired if 0 + {retained_expired_after, 0}. + +Maximum Number of Retained Message +................................... + +.. code:: erlang + + %% Max number of retained messages + {retained_max_message_num, 100000}. + +Maximum Size of Retained Message +................................ + +.. code:: erlang + + %% Max Payload Size of retained message + {retained_max_playload_size, 65536}. + +MQTT Session +------------ .. code-block:: erlang + %% Max number of QoS 1 and 2 messages that can be “inflight” at one time. + %% 0 means no limit + {session_max_inflight, 100}. + + %% Retry interval for redelivering QoS1/2 messages. + {session_unack_retry_interval, 60}. + + %% Awaiting PUBREL Timeout + {session_await_rel_timeout, 20}. + + %% Max Packets that Awaiting PUBREL, 0 means no limit + {session_max_awaiting_rel, 0}. + + %% Statistics Collection Interval(seconds) + {session_collect_interval, 0}. + + %% Expired after 2 day (unit: minute) + {session_expired_after, 2880}. + +Session parameters: + ++------------------------------+----------------------------------------------------------+ +| session_max_inflight | Max number of QoS1/2 messages that can be delivered in | +| | the same time | ++------------------------------+----------------------------------------------------------+ +| session_unack_retry_interval | Retry interval for unacked QoS1/2 messages. | ++------------------------------+----------------------------------------------------------+ +| session_await_rel_timeout | Awaiting PUBREL Timeout | ++------------------------------+----------------------------------------------------------+ +| session_max_awaiting_rel | Max number of Packets that Awaiting PUBREL | ++------------------------------+----------------------------------------------------------+ +| session_collect_interval | Interval of Statistics Collection | ++------------------------------+----------------------------------------------------------+ +| session_expired_after | Expired after (unit: minute) | ++------------------------------+----------------------------------------------------------+ + +MQTT Message Queue +------------------ + +The message queue of session stores: + +1. Offline messages for persistent session. + +2. Pending messages for inflight window is full + +Queue parameters: + +.. code-block:: erlang + + %% Type: simple | priority + {queue_type, simple}. + + %% Topic Priority: 0~255, Default is 0 + %% {queue_priority, [{"topic/1", 10}, {"topic/2", 8}]}. + + %% Max queue length. Enqueued messages when persistent client disconnected, + %% or inflight window is full. + {queue_max_length, infinity}. + + %% Low-water mark of queued messages + {queue_low_watermark, 0.2}. + + %% High-water mark of queued messages + {queue_high_watermark, 0.6}. + + %% Queue Qos0 messages? + {queue_qos0, true}. + ++----------------------+---------------------------------------------------+ +| queue_type | Queue type: simple or priority | ++----------------------+---------------------------------------------------+ +| queue_priority | Topic priority | ++----------------------+---------------------------------------------------+ +| queue_max_length | Max Queue size, infinity means no limit | ++----------------------+---------------------------------------------------+ +| queue_low_watermark | Low watermark | ++----------------------+---------------------------------------------------+ +| queue_high_watermark | High watermark | ++----------------------+---------------------------------------------------+ +| queue_qos0 | If Qos0 message queued? | ++----------------------+---------------------------------------------------+ + +PubSub and Router +----------------- + +PubSub Pool Size +................ + +.. code-block:: erlang + + %% PubSub Pool Size. Default should be scheduler numbers. + {pubsub_pool_size, 8}. + +MQTT Bridge Parameters +---------------------- + +Max MQueue Size of Bridge +......................... + +.. code:: erlang + + %% TODO: Bridge Queue Size + {bridge_max_queue_len, 10000}. + +Ping Interval of Bridge +....................... + +.. code:: erlang + + %% Ping Interval of bridge node + {bridge_ping_down_interval, 1}. % second + +Extended Modules +---------------- + +Presence Module +............... + +'presence' module will publish presence message to $SYS topic when a client connected or disconnected: + +.. code:: erlang + + %% Client presence management module. Publish presence messages when + %% client connected or disconnected. + {module, presence, [{qos, 0}]}. + +Subscription Module +................... + +'subscription' module forces the client to subscribe some topics when connected to the broker: + +.. code:: erlang + + %% Subscribe topics automatically when client connected + {module, subscription, [{"$client/$c", 1}]}. + +Rewrite Module +.............. + +'rewrite' module supports to rewrite the topic path: + +.. code:: erlang + + %% [Rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) + {module, rewrite, [{config, "etc/modules/rewrite.conf"}]}. + +Configure rewrite rules in etc/modules/rewrite.conf:: + {topic, "x/#", [ {rewrite, "^x/y/(.+)$", "z/y/$1"}, {rewrite, "^x/(.+)$", "y/$1"} @@ -723,3 +530,161 @@ The Rewrite Rules for emqttd_mod_rewrite: {topic, "y/+/z/#", [ {rewrite, "^y/(.+)/z/(.+)$", "y/z/$2"} ]}. + +Plugins Folder +-------------- + +.. code:: erlang + + %% Dir of plugins' config + {plugins_etc_dir, "etc/plugins/"}. + + %% File to store loaded plugin names. + {plugins_loaded_file, "data/loaded_plugins"}. + + +TCP Listeners +------------- + +Configure the TCP listeners for MQTT, MQTT(SSL) and HTTP Protocols. + +The most important parameter is 'max_clients' - max concurrent clients allowed. + +The TCP Ports occupied by emqttd broker by default: + ++-----------+-----------------------------------+ +| 1883 | MQTT Port | ++-----------+-----------------------------------+ +| 8883 | MQTT(SSL) Port | ++-----------+-----------------------------------+ +| 8083 | MQTT(WebSocket), HTTP API Port | ++-----------+-----------------------------------+ + +.. code-block:: erlang + +Listener Parameters: + ++-------------+----------------------------------------------------------------+ +| acceptors | TCP Acceptor Pool | ++-------------+----------------------------------------------------------------+ +| max_clients | Maximum number of concurrent TCP connections allowed | ++-------------+----------------------------------------------------------------+ +| access | Access Control by IP, for example: [{allow, "192.168.1.0/24"}] | ++-------------+----------------------------------------------------------------+ +| connopts | Rate Limit Control, for example: {rate_limit, "100,10"} | ++-------------+----------------------------------------------------------------+ +| sockopts | TCP Socket parameters | ++-------------+----------------------------------------------------------------+ + +1883 - Plain MQTT +................. + +.. code-block:: erlang + + %% Plain MQTT + {listener, mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 16}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Mount point prefix + %% {mount_point, "prefix/"}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + + %% Socket Options + {sockopts, [ + %Set buffer if hight thoughtput + %{recbuf, 4096}, + %{sndbuf, 4096}, + %{buffer, 4096}, + %{nodelay, true}, + {backlog, 1024} + ]} + ]}. + +8883 - MQTT(SSL) +................ + +.. code-block:: erlang + + %% MQTT/SSL + {listener, mqtts, 8883, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 512}, + + %% Mount point prefix + %% {mount_point, "secure/"}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% SSL certificate and key files + {ssl, [{certfile, "etc/ssl/ssl.crt"}, + {keyfile, "etc/ssl/ssl.key"}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} + ]}. + +8083 - MQTT(WebSocket) +...................... + +.. code-block:: erlang + + %% HTTP and WebSocket Listener + {listener, http, 8083, [ + %% Size of acceptor pool + {acceptors, 4}, + + %% Maximum number of concurrent clients + {max_clients, 64}, + + %% Socket Access Control + {access, [{allow, all}]}, + + %% Socket Options + {sockopts, [ + {backlog, 1024} + %{buffer, 4096}, + ]} + ]}. + +Erlang VM Monitor +----------------- + +.. code:: erlang + + %% Long GC, don't monitor in production mode for: + %% https://github.com/erlang/otp/blob/feb45017da36be78d4c5784d758ede619fa7bfd3/erts/emulator/beam/erl_gc.c#L421 + + {sysmon_long_gc, false}. + + %% Long Schedule(ms) + {sysmon_long_schedule, 240}. + + %% 8M words. 32MB on 32-bit VM, 64MB on 64-bit VM. + %% 8 * 1024 * 1024 + {sysmon_large_heap, 8388608}. + + %% Busy Port + {sysmon_busy_port, false}. + + %% Busy Dist Port + {sysmon_busy_dist_port, true}. + diff --git a/docs/source/design.rst b/docs/source/design.rst index a9d081cc2..adc1f10b8 100644 --- a/docs/source/design.rst +++ b/docs/source/design.rst @@ -15,6 +15,17 @@ The emqttd broker 1.0 is more like a network Switch or Router, not a traditional .. image:: _static/images/concept.png +The EMQ 2.0 seperated the Message Flow Plane and Monitor/Control Plane, the Architecture is something like:: + + Control Plane + -------------------- + | | + FrontEnd -> | Flow Plane | -> BackEnd + | | + Session Router + --------------------- + Monitor Plane + Design Philosophy ----------------- @@ -478,3 +489,32 @@ http://github.com/emqtt/emqttd_plugin_template .. _eSockd: https://github.com/emqtt/esockd .. _Chain-of-responsibility_pattern: https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern .. _emqttd_plugin_template: https://github.com/emqtt/emqttd_plugin_template/blob/master/src/emqttd_plugin_template.erl + +----------------- +Mnesia/ETS Tables +----------------- + ++--------------------+--------+----------------------------------------+ +| Table | Type | Description | ++====================+========+========================================+ +| mqtt_trie | mnesia | Trie Table | ++--------------------+--------+----------------------------------------+ +| mqtt_trie_node | mnesia | Trie Node Table | ++--------------------+--------+----------------------------------------+ +| mqtt_route | mnesia | Global Route Table | ++--------------------+--------+----------------------------------------+ +| mqtt_local_route | mnesia | Local Route Table | ++--------------------+--------+----------------------------------------+ +| mqtt_pubsub | ets | PubSub Tab | ++--------------------+--------+----------------------------------------+ +| mqtt_subscriber | ets | Subscriber Tab | ++--------------------+--------+----------------------------------------+ +| mqtt_subscription | ets | Subscription Tab | ++--------------------+--------+----------------------------------------+ +| mqtt_session | mnesia | Global Session Table | ++--------------------+--------+----------------------------------------+ +| mqtt_local_session | ets | Local Session Table | ++--------------------+--------+----------------------------------------+ +| mqtt_client | ets | Client Table | ++--------------------+--------+----------------------------------------+ + diff --git a/docs/source/getstarted.rst b/docs/source/getstarted.rst index 55b11164f..e5d0fbc6e 100644 --- a/docs/source/getstarted.rst +++ b/docs/source/getstarted.rst @@ -35,6 +35,8 @@ Features * MQTT Over WebSocket(SSL) * HTTP Publish API * STOMP protocol +* MQTT-SN Protocol +* CoAP Protocol * STOMP over SockJS * $SYS/# Topics * ClientID Authentication @@ -63,7 +65,7 @@ Installing on Mac, for example: .. code-block:: bash - unzip emqttd-macosx-1.1-beta-20160601.zip && cd emqttd + unzip emqttd-macosx-2.0-beta1-20160830.zip && cd emqttd # Start emqttd ./bin/emqttd start @@ -119,8 +121,6 @@ Modules +-------------------------+--------------------------------------------+ | emqttd_auth_username | Authentication with Username and Password | +-------------------------+--------------------------------------------+ -| emqttd_auth_ldap | Authentication with LDAP | -+-------------------------+--------------------------------------------+ | emqttd_mod_presence | Publish presence message to $SYS topics | | | when client connected or disconnected | +-------------------------+--------------------------------------------+ @@ -136,22 +136,16 @@ Enable 'emqttd_auth_username' module: .. code-block:: erlang - {access, [ - %% Authetication. Anonymous Default - {auth, [ - %% Authentication with username, password - {username, []}, - - ... + %% Authentication with username, password + {auth, username, [{passwd, "etc/modules/passwd.conf"}]}. Enable 'emqttd_mod_presence' module: .. code-block:: erlang - {modules, [ - %% Client presence management module. - %% Publish messages when client connected or disconnected - {presence, [{qos, 0}]} + %% Client presence management module. Publish presence messages when + %% client connected or disconnected. + {module, presence, [{qos, 0}]}. Plugins ------- @@ -163,17 +157,21 @@ A plugin is an Erlang application to extend the emqttd broker. +----------------------------+-----------------------------------+ | `emqttd_dashboard`_ | Web Dashboard | +----------------------------+-----------------------------------+ +| `emqttd_auth_ldap`_ | LDAP Auth Plugin | ++----------------------------+-----------------------------------+ | `emqttd_auth_http`_ | Authentication/ACL with HTTP API | +----------------------------+-----------------------------------+ -| `emqttd_plugin_mysql`_ | Authentication with MySQL | +| `emqttd_auth_mysql` _ | Authentication with MySQL | +----------------------------+-----------------------------------+ -| `emqttd_plugin_pgsql`_ | Authentication with PostgreSQL | +| `emqttd_auth_pgsql`_ | Authentication with PostgreSQL | +----------------------------+-----------------------------------+ -| `emqttd_plugin_redis`_ | Authentication with Redis | +| `emqttd_auth_redis`_ | Authentication with Redis | +----------------------------+-----------------------------------+ | `emqttd_plugin_mongo`_ | Authentication with MongoDB | +----------------------------+-----------------------------------+ -| `emqttd_stomp`_ | STOMP Protocol Plugin | +| `emqttd_sn`_ | MQTT-SN Protocol Plugin | ++----------------------------+-----------------------------------+ +| `emqttd_stomp`_ | STOMP Protocol Plugin | +----------------------------+-----------------------------------+ | `emqttd_sockjs`_ | SockJS(Stomp) Plugin | +----------------------------+-----------------------------------+ @@ -182,9 +180,9 @@ A plugin is an Erlang application to extend the emqttd broker. A plugin could be enabled by 'bin/emqttd_ctl plugins load' command. -For example, enable 'emqttd_plugin_pgsql' plugin:: +For example, enable 'emqttd_auth_pgsql' plugin:: - ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql + ./bin/emqttd_ctl plugins load emqttd_auth_pgsql ----------------------- One Million Connections @@ -238,26 +236,27 @@ emqttd/etc/vm.args:: emqttd broker ------------- -emqttd/etc/emqttd.config: +emqttd/etc/emqttd.conf: .. code-block:: erlang - {mqtt, 1883, [ - %% Size of acceptor pool - {acceptors, 64}, + {listener, mqtt, 1883, [ + %% Size of acceptor pool + {acceptors, 64}, - %% Maximum number of concurrent clients - {max_clients, 1000000}, + %% Maximum number of concurrent clients + {max_clients, 1000000}, - %% Socket Access Control - {access, [{allow, all}]}, + %% Socket Access Control + {access, [{allow, all}]}, - %% Connection Options - {connopts, [ - %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec - %% {rate_limit, "100,10"} %% 100K burst, 10K rate - ]}, - ... + %% Connection Options + {connopts, [ + %% Rate Limit. Format is 'burst, rate', Unit is KB/Sec + %% {rate_limit, "100,10"} %% 100K burst, 10K rate + ]}, + ... + ]}. Test Client ----------- @@ -291,11 +290,15 @@ GitHub: https://github.com/emqtt .. _emqttd_plugin_template: https://github.com/emqtt/emqttd_plugin_template .. _emqttd_dashboard: https://github.com/emqtt/emqttd_dashboard +.. _emqttd_auth_ldap: https://github.com/emqtt/emqttd_auth_ldap .. _emqttd_auth_http: https://github.com/emqtt/emqttd_auth_http -.. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql -.. _emqttd_plugin_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql -.. _emqttd_plugin_redis: https://github.com/emqtt/emqttd_plugin_redis -.. _emqttd_plugin_mongo: https://github.com/emqtt/emqttd_plugin_mongo +.. _emqttd_auth_mysql: https://github.com/emqtt/emqttd_plugin_mysql +.. _emqttd_auth_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql +.. _emqttd_auth_redis: https://github.com/emqtt/emqttd_plugin_redis +.. _emqttd_auth_mongo: https://github.com/emqtt/emqttd_plugin_mongo +.. _emqttd_reloader: https://github.com/emqtt/emqttd_reloader .. _emqttd_stomp: https://github.com/emqtt/emqttd_stomp .. _emqttd_sockjs: https://github.com/emqtt/emqttd_sockjs .. _emqttd_recon: https://github.com/emqtt/emqttd_recon +.. _emqttd_sn: https://github.com/emqtt/emqttd_sn + diff --git a/docs/source/index.rst b/docs/source/index.rst index 4051035b1..2db72ecdd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,8 +35,6 @@ Sensors, Mobiles, Web Browsers and Application Servers could be connected by emq | Author: | Feng Lee | +---------------+-----------------------------------------+ -.. NOTE:: MQTT-SN,CoAP Protocols are planned to 1.x release. - Contents: .. toctree:: diff --git a/docs/source/install.rst b/docs/source/install.rst index 8bb68e4f8..83a51d2bd 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -35,7 +35,7 @@ Download binary packages from: http://emqtt.io/downloads The package name consists of platform, version and release time. -For example: emqttd-centos64-1.1-beta-20160601.zip +For example: emqttd-centos64-2.0-beta1-20160830.zip .. _install_on_linux: @@ -47,7 +47,7 @@ Download CentOS Package from: http://emqtt.io/downloads/latest/centos, and then .. code-block:: bash - unzip emqttd-centos64-1.1-beta-20160601.zip + unzip emqttd-centos64-2.0-beta-20160830.zip Start the broker in console mode: @@ -80,7 +80,7 @@ If the broker is started successfully, console will print: mqtt listen on 0.0.0.0:1883 with 16 acceptors. mqtts listen on 0.0.0.0:8883 with 4 acceptors. http listen on 0.0.0.0:8083 with 4 acceptors. - Erlang MQTT Broker 1.1 is running now + Erlang MQTT Broker 2.0 is running now Eshell V6.4 (abort with ^G) (emqttd@127.0.0.1)1> @@ -100,7 +100,7 @@ Check the running status of the broker: $ ./bin/emqttd_ctl status Node 'emqttd@127.0.0.1' is started - emqttd 1.1 is running + emqttd 2.0 is running Or check the status by URL:: @@ -130,7 +130,7 @@ We could install the broker on Mac OS X to develop and debug MQTT applications. Download Mac Package from: http://emqtt.io/downloads/latest/macosx -Configure 'lager' log level in 'etc/emqttd.config', all MQTT messages recevied/sent will be printed on console: +Configure 'lager' log level in 'releases/2.0/sys.config', all MQTT messages recevied/sent will be printed on console: .. code-block:: erlang @@ -198,15 +198,15 @@ When all dependencies are ready, clone the emqttd project from github.com and bu .. code-block:: bash - git clone https://github.com/emqtt/emqttd.git + git clone https://github.com/emqtt/emqttd-relx.git - cd emqttd + cd emqttd-relx && make - make && make dist + cd _rel/emqttd && ./bin/emqttd console The binary package output in folder:: - rel/emqttd + _rel/emqttd .. _tcp_ports: @@ -228,19 +228,20 @@ The TCP ports used can be configured in etc/emqttd.config: .. code-block:: erlang - {listeners, [ - {mqtt, 1883, [ - ... - ]}, + %% Plain MQTT + {listener, mqtt, 1883, [ + ... + ]}. - {mqtts, 8883, [ - ... - ]}, - %% HTTP and WebSocket Listener - {http, 8083, [ - ... - ]} - ]}, + %% MQTT/SSL + {listener, mqtts, 8883, [ + ... + ]}. + + %% HTTP and WebSocket Listener + {listener, http, 8083, [ + ... + ]}. The 18083 port is used by Web Dashboard of the broker. Default login: admin, Password: public @@ -255,7 +256,7 @@ Two main configuration files of the emqttd broker: +-------------------+-----------------------------------+ | etc/vm.args | Erlang VM Arguments | +-------------------+-----------------------------------+ -| etc/emqttd.config | emqttd broker Config | +| etc/emqttd.conf | emqttd broker Config | +-------------------+-----------------------------------+ Two important parameters in etc/vm.args: @@ -277,17 +278,18 @@ The maximum number of allowed MQTT clients: .. code-block:: erlang - {listeners, [ - {mqtt, 1883, [ - %% TCP Acceptor Pool - {acceptors, 16}, + %% Plain MQTT + {listener, mqtt, 1883, [ - %% Maximum number of concurrent MQTT clients - {max_clients, 8192}, + %% Size of acceptor pool + {acceptors, 16}, - ... + %% Maximum number of concurrent clients + {max_clients, 8192}, - ]}, + ... + + ]}. .. _init_d_emqttd: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 6c4fa552a..ab4543e06 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -7,24 +7,28 @@ Plugins The emqttd broker could be extended by plugins. Users could develop plugins to customize authentication, ACL and functions of the broker, or integrate the broker with other systems. -The plugins that emqtt project released: +The plugins that emqttd 2.0 released: +---------------------------+---------------------------+ | Plugin | Description | +===========================+===========================+ +| `emqttd_dashboard`_ | Web Dashboard | ++---------------------------+---------------------------+ | `emqttd_plugin_template`_ | Template Plugin | +---------------------------+---------------------------+ -| `emqttd_dashboard`_ | Web Dashboard | +| `emqttd_auth_ldap`_ | LDAP Auth | +---------------------------+---------------------------+ | `emqttd_auth_http`_ | HTTP Auth/ACL Plugin | +---------------------------+---------------------------+ -| `emqttd_plugin_mysql`_ | MySQL Auth/ACL Plugin | +| `emqttd_auth_mysql`_ | MySQL Auth/ACL Plugin | +---------------------------+---------------------------+ -| `emqttd_plugin_pgsql`_ | PostgreSQL Auth/ACL Plugin| +| `emqttd_auth_pgsql`_ | PostgreSQL Auth/ACL Plugin| +---------------------------+---------------------------+ -| `emqttd_plugin_redis`_ | Redis Auth/ACL Plugin | +| `emqttd_auth_redis`_ | Redis Auth/ACL Plugin | +---------------------------+---------------------------+ -| `emqttd_plugin_mongo`_ | MongoDB Auth/ACL Plugin | +| `emqttd_auth_mongo`_ | MongoDB Auth/ACL Plugin | ++---------------------------+---------------------------+ +| `emqttd_sn`_ | MQTT-SN Protocol Plugin | +---------------------------+---------------------------+ | `emqttd_stomp`_ | STOMP Protocol Plugin | +---------------------------+---------------------------+ @@ -39,17 +43,9 @@ The plugins that emqtt project released: emqttd_plugin_template - Template Plugin ---------------------------------------- -A plugin is just a normal Erlang application under the 'emqttd/plugins' folder. Each plugin has e configuration file: 'etc/plugin.config'. +A plugin is just a normal Erlang application which has its own configuration file: 'etc/.config'. -plugins/emqttd_plugin_template is a demo plugin. The folder structure: - -+------------------------+---------------------------+ -| File | Description | -+========================+===========================+ -| etc/plugin.config | Plugin config file | -+------------------------+---------------------------+ -| ebin/ | Erlang program files | -+------------------------+---------------------------+ +emqttd_plugin_template is a demo plugin. Load, unload Plugin ------------------- @@ -78,22 +74,51 @@ The Web Dashboard for emqttd broker. The plugin will be loaded automatically whe .. image:: _static/images/dashboard.png -Configure Dashboard -------------------- +Configure Dashboard Plugin +-------------------------- -emqttd_dashboard/etc/plugin.config: +etc/plugins/emqttd_dashboard.conf: .. code-block:: erlang - [ - {emqttd_dashboard, [ - {listener, - {emqttd_dashboard, 18083, [ - {acceptors, 4}, - {max_clients, 512}]} - } + {listener, + {dashboard, 18083, [ + {acceptors, 4}, + {max_clients, 512} ]} - ]. + }. + +---------------------------------- +emqttd_auth_ldap: LDAP Auth Plugin +---------------------------------- + +LDAP Auth Plugin: https://github.com/emqtt/emqttd_auth_ldap + +.. NOTE:: Supported in 2.0-beta1 release + +Configure LDAP Plugin +--------------------- + +etc/plugins/emqttd_auth_ldap.conf: + +.. code-block:: erlang + + {ldap, [ + {servers, ["localhost"]}, + {port, 389}, + {timeout, 30}, + {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, + {ssl, fasle}, + {sslopts, [ + {certfile, "ssl.crt"}, + {keyfile, "ssl.key"} + ]} + ]}. + +Load LDAP Plugin +---------------- + +./bin/emqttd_ctl plugins load emqttd_auth_ldap --------------------------------------- emqttd_auth_http - HTTP Auth/ACL Plugin @@ -103,61 +128,57 @@ MQTT Authentication/ACL with HTTP API: https://github.com/emqtt/emqttd_auth_http .. NOTE:: Supported in 1.1 release -Configure emqttd_auth_http/etc/plugin.config --------------------------------------------- +Configure HTTP Auth/ACL Plugin +------------------------------ -.. code:: erlang +etc/plugins/emqttd_auth_http.conf: - [ +.. code-block:: erlang - {emqttd_auth_http, [ + %% Variables: %u = username, %c = clientid, %a = ipaddress, %t = topic - %% Variables: %u = username, %c = clientid, %a = ipaddress, %t = topic - - {super_req, [ - {method, post}, - {url, "http://localhost:8080/mqtt/superuser"}, - {params, [ - {username, "%u"}, - {clientid, "%c"} - ]} - ]}, - - {auth_req, [ - {method, post}, - {url, "http://localhost:8080/mqtt/auth"}, - {params, [ - {clientid, "%c"}, - {username, "%u"}, - {password, "%P"} - ]} - ]}, - - %% 'access' parameter: sub = 1, pub = 2 - - {acl_req, [ - {method, post}, - {url, "http://localhost:8080/mqtt/acl"}, - {params, [ - {access, "%A"}, - {username, "%u"}, - {clientid, "%c"}, - {ipaddr, "%a"}, - {topic, "%t"} - ]} - ]} + {super_req, [ + {method, post}, + {url, "http://localhost:8080/mqtt/superuser"}, + {params, [ + {username, "%u"}, + {clientid, "%c"} ]} + ]}. - ]. + {auth_req, [ + {method, post}, + {url, "http://localhost:8080/mqtt/auth"}, + {params, [ + {clientid, "%c"}, + {username, "%u"}, + {password, "%P"} + ]} + ]}. -HTTP API --------- + %% 'access' parameter: sub = 1, pub = 2 + + {acl_req, [ + {method, post}, + {url, "http://localhost:8080/mqtt/acl"}, + {params, [ + {access, "%A"}, + {username, "%u"}, + {clientid, "%c"}, + {ipaddr, "%a"}, + {topic, "%t"} + ]} + ]}. + + +HTTP Auth/ACL API +----------------- Return 200 if ok Return 4xx if unauthorized -Load emqttd_auth_http plugin +Load HTTP Auth/ACL Plugin ---------------------------- .. code:: bash @@ -211,64 +232,57 @@ MQTT ACL Table (6,1,'127.0.0.1',NULL,NULL,2,'#'), (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); -Configure emqttd_plugin_mysql/etc/plugin.config ------------------------------------------------ +Configure MySQL Auth/ACL Plugin +------------------------------- -Configure MySQL host, username, password and database: +etc/plugins/emqttd_plugin_mysql.conf: .. code-block:: erlang - [ + {mysql_pool, [ + %% pool options + {pool_size, 8}, + {auto_reconnect, 1}, - {emqttd_plugin_mysql, [ + %% mysql options + {host, "localhost"}, + {port, 3306}, + {user, ""}, + {password, ""}, + {database, "mqtt"}, + {encoding, utf8}, + {keep_alive, true} + ]}. - {mysql_pool, [ - %% ecpool options - {pool_size, 8}, - {auto_reconnect, 3}, + %% Variables: %u = username, %c = clientid, %a = ipaddress - %% mysql options - {host, "localhost"}, - {port, 3306}, - {user, ""}, - {password, ""}, - {database, "mqtt"}, - {encoding, utf8} - ]}, + %% Superuser Query + {superquery, "select is_superuser from mqtt_user where username = '%u' limit 1"}. - %% Variables: %u = username, %c = clientid, %a = ipaddress + %% Authentication Query: select password only + {authquery, "select password from mqtt_user where username = '%u' limit 1"}. - %% Superuser Query - {superquery, "select is_superuser from mqtt_user where username = '%u' limit 1"}, + %% hash algorithm: plain, md5, sha, sha256, pbkdf2? + {password_hash, sha256}. - %% Authentication Query: select password only - {authquery, "select password from mqtt_user where username = '%u' limit 1"}, + %% select password with salt + %% {authquery, "select password, salt from mqtt_user where username = '%u'"}. - %% hash algorithm: plain, md5, sha, sha256, pbkdf2? - {password_hash, sha256}, + %% sha256 with salt prefix + %% {password_hash, {salt, sha256}}. - %% select password with salt - %% {authquery, "select password, salt from mqtt_user where username = '%u'"}, + %% sha256 with salt suffix + %% {password_hash, {sha256, salt}}. - %% sha256 with salt prefix - %% {password_hash, {salt, sha256}}, + %% '%a' = ipaddress, '%u' = username, '%c' = clientid + %% Comment this query, the acl will be disabled + {aclquery, "select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}. - %% sha256 with salt suffix - %% {password_hash, {sha256, salt}}, + %% If no ACL rules matched, return... + {acl_nomatch, allow}. - %% '%a' = ipaddress, '%u' = username, '%c' = clientid - %% Comment this query, the acl will be disabled - {aclquery, "select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}, - - %% If no ACL rules matched, return... - {acl_nomatch, allow} - - ]} - - ]. - -Load emqttd_plugin_mysql plugin -------------------------------- +Load MySQL Auth/ACL plugin +-------------------------- .. code-block:: bash @@ -280,8 +294,8 @@ emqttd_plugin_pgsql - PostgreSQL Auth/ACL Plugin MQTT Authentication, ACL with PostgreSQL Database. -MQTT User Table ---------------- +Postgre MQTT User Table +----------------------- .. code-block:: sql @@ -293,8 +307,8 @@ MQTT User Table salt character varying(40) ); -MQTT ACL Table --------------- +Postgre MQTT ACL Table +---------------------- .. code-block:: sql @@ -317,67 +331,62 @@ MQTT ACL Table (6,1,'127.0.0.1',NULL,NULL,2,'#'), (7,1,NULL,'dashboard',NULL,1,'$SYS/#'); -Configure emqttd_plugin_pgsql/etc/plugin.config +Configure Postgre Auth/ACL Plugin ----------------------------------------------- +Plugin Config: etc/plugins/emqttd_plugin_pgsql.conf. + Configure host, username, password and database of PostgreSQL: .. code-block:: erlang - [ + {pgsql_pool, [ + %% pool options + {pool_size, 8}, + {auto_reconnect, 3}, - {emqttd_plugin_pgsql, [ + %% pgsql options + {host, "localhost"}, + {port, 5432}, + {ssl, false}, + {username, "feng"}, + {password, ""}, + {database, "mqtt"}, + {encoding, utf8} + ]}. - {pgsql_pool, [ - %% ecpool options - {pool_size, 8}, - {auto_reconnect, 3}, + %% Variables: %u = username, %c = clientid, %a = ipaddress - %% pgsql options - {host, "localhost"}, - {port, 5432}, - {ssl, false}, - {username, "feng"}, - {password, ""}, - {database, "mqtt"}, - {encoding, utf8} - ]}, + %% Superuser Query + {superquery, "select is_superuser from mqtt_user where username = '%u' limit 1"}. - %% Variables: %u = username, %c = clientid, %a = ipaddress + %% Authentication Query: select password only + {authquery, "select password from mqtt_user where username = '%u' limit 1"}. - %% Superuser Query - {superquery, "select is_superuser from mqtt_user where username = '%u' limit 1"}, + %% hash algorithm: plain, md5, sha, sha256, pbkdf2? + {password_hash, sha256}. - %% Authentication Query: select password only - {authquery, "select password from mqtt_user where username = '%u' limit 1"}, + %% select password with salt + %% {authquery, "select password, salt from mqtt_user where username = '%u'"}. - %% hash algorithm: plain, md5, sha, sha256, pbkdf2? - {password_hash, sha256}, + %% sha256 with salt prefix + %% {password_hash, {salt, sha256}}. - %% select password with salt - %% {authquery, "select password, salt from mqtt_user where username = '%u'"}, + %% sha256 with salt suffix + %% {password_hash, {sha256, salt}}. - %% sha256 with salt prefix - %% {password_hash, {salt, sha256}}, + %% Comment this query, the acl will be disabled. Notice: don't edit this query! + {aclquery, "select allow, ipaddr, username, clientid, access, topic from mqtt_acl where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}. - %% sha256 with salt suffix - %% {password_hash, {sha256, salt}}, + %% If no rules matched, return... + {acl_nomatch, allow}. - %% Comment this query, the acl will be disabled. Notice: don't edit this query! - {aclquery, "select allow, ipaddr, username, clientid, access, topic from mqtt_acl - where ipaddr = '%a' or username = '%u' or username = '$all' or clientid = '%c'"}, - - %% If no rules matched, return... - {acl_nomatch, allow} - ]} - ]. - -Load emqttd_plugin_pgsql Plugin -------------------------------- +Load Postgre Auth/ACL Plugin +----------------------------- .. code-block:: bash - ./bin/emqttd_ctl plugins load emqttd_plugin_pgsql + ./bin/emqttd_ctl plugins load emqttd_auth_pgsql ------------------------------------------- emqttd_plugin_redis - Redis Auth/ACL Plugin @@ -385,58 +394,56 @@ emqttd_plugin_redis - Redis Auth/ACL Plugin MQTT Authentication, ACL with Redis: https://github.com/emqtt/emqttd_plugin_redis -Configure emqttd_plugin_redis/etc/plugin.config ------------------------------------------------ +Configure Redis Auth/ACL Plugin +------------------------------- + +etc/plugins/emqttd_auth_redis.conf: .. code-block:: erlang - [ - {emqttd_plugin_redis, [ + {redis_pool, [ + %% pool options + {pool_size, 8}, + {auto_reconnect, 2}, - {eredis_pool, [ - %% ecpool options - {pool_size, 8}, - {auto_reconnect, 2}, + %% redis options + {host, "127.0.0.1"}, + {port, 6379}, + {database, 0}, + {password, ""} + ]}. - %% eredis options - {host, "127.0.0.1"}, - {port, 6379}, - {database, 0}, - {password, ""} - ]}, + %% Variables: %u = username, %c = clientid - %% Variables: %u = username, %c = clientid + %% HMGET mqtt_user:%u is_superuser + {supercmd, ["HGET", "mqtt_user:%u", "is_superuser"]}. - %% HMGET mqtt_user:%u is_superuser - {supercmd, ["HGET", "mqtt_user:%u", "is_superuser"]}, + %% HMGET mqtt_user:%u password + {authcmd, ["HGET", "mqtt_user:%u", "password"]}. - %% HMGET mqtt_user:%u password - {authcmd, ["HGET", "mqtt_user:%u", "password"]}, + %% Password hash algorithm: plain, md5, sha, sha256, pbkdf2? + {password_hash, sha256}. - %% Password hash algorithm: plain, md5, sha, sha256, pbkdf2? - {password_hash, sha256}, + %% SMEMBERS mqtt_acl:%u + {aclcmd, ["SMEMBERS", "mqtt_acl:%u"]}. - %% SMEMBERS mqtt_acl:%u - {aclcmd, ["SMEMBERS", "mqtt_acl:%u"]}, + %% If no rules matched, return... + {acl_nomatch, deny}. - %% If no rules matched, return... - {acl_nomatch, deny}, + %% Load Subscriptions form Redis when client connected. + {subcmd, ["HGETALL", "mqtt_subs:%u"]}. - %% Load Subscriptions form Redis when client connected. - {subcmd, ["HGETALL", "mqtt_subs:%u"]} - ]} - ]. -User HASH ---------- +Redis User HASH +--------------- Set a 'user' hash with 'password' field, for example:: HSET mqtt_user: is_superuser 1 HSET mqtt_user: password "passwd" -ACL Rule SET ------------- +Redis ACL Rule SET +------------------ The plugin uses a redis SET to store ACL rules:: @@ -444,8 +451,8 @@ The plugin uses a redis SET to store ACL rules:: SADD mqtt_acl: "subscribe topic2" SADD mqtt_acl: "pubsub topic3" -Subscription HASH ------------------ +Redis Subscription HASH +----------------------- The plugin can store static subscriptions in a redis Hash:: @@ -453,12 +460,12 @@ The plugin can store static subscriptions in a redis Hash:: HSET mqtt_subs: topic2 1 HSET mqtt_subs: topic3 2 -Load emqttd_plugin_redis Plugin -------------------------------- +Load Redis Auth/ACL Plugin +-------------------------- .. code-block:: bash - ./bin/emqttd_ctl plugins load emqttd_plugin_redis + ./bin/emqttd_ctl plugins load emqttd_auth_redis --------------------------------------------- emqttd_plugin_mongo - MongoDB Auth/ACL Plugin @@ -466,55 +473,51 @@ emqttd_plugin_mongo - MongoDB Auth/ACL Plugin MQTT Authentication, ACL with MongoDB: https://github.com/emqtt/emqttd_plugin_mongo -Configure emqttd_plugin_mongo/etc/plugin.config ------------------------------------------------ +Configure MongoDB Auth/ACL Plugin +--------------------------------- + +etc/plugins/emqttd_plugin_mongo.conf: .. code-block:: erlang - [ - {emqttd_plugin_mongo, [ + {mongo_pool, [ + {pool_size, 8}, + {auto_reconnect, 3}, - {mongo_pool, [ - {pool_size, 8}, - {auto_reconnect, 3}, + %% Mongodb Opts + {host, "localhost"}, + {port, 27017}, + %% {login, ""}, + %% {password, ""}, + {database, "mqtt"} + ]}. - %% Mongodb Driver Opts - {host, "localhost"}, - {port, 27017}, - %% {login, ""}, - %% {password, ""}, - {database, "mqtt"} - ]}, + %% Variables: %u = username, %c = clientid - %% Variables: %u = username, %c = clientid + %% Superuser Query + {superquery, pool, [ + {collection, "mqtt_user"}, + {super_field, "is_superuser"}, + {selector, {"username", "%u"}} + ]}. - %% Superuser Query - {superquery, [ - {collection, "mqtt_user"}, - {super_field, "is_superuser"}, - {selector, {"username", "%u"}} - ]}, + %% Authentication Query + {authquery, pool, [ + {collection, "mqtt_user"}, + {password_field, "password"}, + %% Hash Algorithm: plain, md5, sha, sha256, pbkdf2? + {password_hash, sha256}, + {selector, {"username", "%u"}} + ]}. - %% Authentication Query - {authquery, [ - {collection, "mqtt_user"}, - {password_field, "password"}, - %% Hash Algorithm: plain, md5, sha, sha256, pbkdf2? - {password_hash, sha256}, - {selector, {"username", "%u"}} - ]}, + %% ACL Query: "%u" = username, "%c" = clientid + {aclquery, pool, [ + {collection, "mqtt_acl"}, + {selector, {"username", "%u"}} + ]}. - %% ACL Query: "%u" = username, "%c" = clientid - {aclquery, [ - {collection, "mqtt_acl"}, - {selector, {"username", "%u"}} - ]}, - - %% If no ACL rules matched, return... - {acl_nomatch, deny} - - ]} - ]. + %% If no ACL rules matched, return... + {acl_nomatch, deny}. MongoDB Database ---------------- @@ -526,8 +529,8 @@ MongoDB Database db.createCollection("mqtt_acl") db.mqtt_user.ensureIndex({"username":1}) -User Collection ---------------- +MongoDB User Collection +----------------------- .. code-block:: json @@ -543,8 +546,8 @@ For example:: db.mqtt_user.insert({username: "test", password: "password hash", is_superuser: false}) db.mqtt_user:insert({username: "root", is_superuser: true}) -ACL Collection --------------- +MongoDB ACL Collection +---------------------- .. code-block:: json @@ -561,12 +564,34 @@ For example:: db.mqtt_acl.insert({username: "test", publish: ["t/1", "t/2"], subscribe: ["user/%u", "client/%c"]}) db.mqtt_acl.insert({username: "admin", pubsub: ["#"]}) -Load emqttd_plugin_mongo Plugin -------------------------------- +Load MongoDB Auth/ACL Plugin +---------------------------- .. code-block:: bash - ./bin/emqttd_ctl plugins load emqttd_plugin_mongo + ./bin/emqttd_ctl plugins load emqttd_auth_mongo + +--------------------------- +emqttd_sn: MQTT-SN Protocol +-------------------------- + +MQTT-SN Protocol/Gateway Plugin. + +Configure MQTT-SN Plugin +------------------------- + +.. NOTE:: UDP Port for MQTT-SN: 1884 + +etc/plugins/emqttd_sn.conf:: + + {listener, {1884, []}}. + +Load MQTT-SN Plugin +------------------- + +.. code:: + + ./bin/emqttd_ctl plugins load emqttd_sn ----------------------------- emqttd_stomp - STOMP Protocol @@ -574,48 +599,41 @@ emqttd_stomp - STOMP Protocol Support STOMP 1.0/1.1/1.2 clients to connect to emqttd broker and communicate with MQTT Clients. -Configure emqttd_stomp/etc/plugin.config ----------------------------------------- +Configure Stomp Plugin +---------------------- + +etc/plugins/emqttd_stomp.conf: .. NOTE:: Default Port for STOMP Protocol: 61613 .. code-block:: erlang - [ - {emqttd_stomp, [ + {default_user, [ + {login, "guest"}, + {passcode, "guest"} + ]}. - {default_user, [ - {login, "guest"}, - {passcode, "guest"} - ]}, + {allow_anonymous, true}. - {allow_anonymous, true}, + {frame, [ + {max_headers, 10}, + {max_header_length, 1024}, + {max_body_length, 8192} + ]}. - %%TODO: unused... - {frame, [ - {max_headers, 10}, - {max_header_length, 1024}, - {max_body_length, 8192} - ]}, + {listener, emqttd_stomp, 61613, [ + {acceptors, 4}, + {max_clients, 512} + ]}. - {listeners, [ - {emqttd_stomp, 61613, [ - {acceptors, 4}, - {max_clients, 512} - ]} - ]} - ]} - ]. - -Load emqttd_stomp Plugin ------------------------- +Load Stomp Plugin +----------------- .. code-block:: bash ./bin/emqttd_ctl plugins load emqttd_stomp - ----------------------------------- emqttd_sockjs - STOMP/SockJS Plugin ----------------------------------- @@ -629,18 +647,21 @@ Configure emqttd_sockjs .. code-block:: erlang - [ - {emqttd_sockjs, [ + {sockjs, []}. - {sockjs, []}, - - {cowboy_listener, {stomp_sockjs, 61616, 4}}, + {cowboy_listener, {stomp_sockjs, 61616, 4}}. + %% TODO: unused... + {stomp, [ + {frame, [ + {max_headers, 10}, + {max_header_length, 1024}, + {max_body_length, 8192} ]} - ]. + ]}. -Load emqttd_sockjs Plugin -------------------------- +Load SockJS Plugin +------------------ .. NOTE:: emqttd_stomp Plugin required. @@ -661,8 +682,8 @@ emqttd_recon - Recon Plugin The plugin loads `recon`_ library on a running emqttd broker. Recon libray helps debug and optimize an Erlang application. -Load emqttd_recon Plugin ------------------------- +Load Recon Plugin +----------------- .. code-block:: bash @@ -689,8 +710,8 @@ Erlang Module Reloader for Development .. NOTE:: Don't load the plugin in production! -Load emqttd_reloader Plugin ---------------------------- +Load 'Reloader' Plugin +---------------------- .. code-block:: bash @@ -712,15 +733,22 @@ Plugin Development Guide Create a Plugin Project ----------------------- -Clone emqttd source from github.com:: +Clone emqttd_plugin_template source from github.com:: - git clone https://github.com/emqtt/emqttd.git + git clone https://github.com/emqtt/emqttd_plugin_template.git -Create a plugin project under 'plugins' folder:: +Create a plugin project with erlang.mk and depends on 'emqttd' application, the 'Makefile':: - cd plugins && mkdir emqttd_my_plugin + PROJECT = emqttd_plugin_abc + PROJECT_DESCRIPTION = emqttd abc plugin + PROJECT_VERSION = 1.0 - cd emqttd_my_plugin && rebar create-app appid=emqttd_my_plugin + DEPS = emqttd + dep_emqttd = git https://github.com/emqtt/emqttd emq20 + + COVER = true + + include erlang.mk Template Plugin: https://github.com/emqtt/emqttd_plugin_template @@ -735,7 +763,7 @@ emqttd_auth_demo.erl - demo authentication module: -behaviour(emqttd_auth_mod). - -include("../../../include/emqttd.hrl"). + -include_lib("emqttd/include/emqttd.hrl"). -export([init/1, check/3, description/0]). @@ -754,7 +782,7 @@ emqttd_acl_demo.erl - demo ACL module: -module(emqttd_acl_demo). - -include("../../../include/emqttd.hrl"). + -include_lib("emqttd/include/emqttd.hrl"). %% ACL callbacks -export([init/1, check_acl/2, reload_acl/1, description/0]). @@ -830,7 +858,7 @@ emqttd_cli_demo.erl: -module(emqttd_cli_demo). - -include("../../../include/emqttd_cli.hrl"). + -include_lib("emqttd/include/emqttd_cli.hrl"). -export([cmd/1]). @@ -850,13 +878,14 @@ There will be a new CLI after the plugin loaded:: ./bin/emqttd_ctl cmd arg1 arg2 - .. _emqttd_dashboard: https://github.com/emqtt/emqttd_dashboard +.. _emqttd_auth_ldap: https://github.com/emqtt/emqttd_auth_ldap .. _emqttd_auth_http: https://github.com/emqtt/emqttd_auth_http -.. _emqttd_plugin_mysql: https://github.com/emqtt/emqttd_plugin_mysql -.. _emqttd_plugin_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql -.. _emqttd_plugin_redis: https://github.com/emqtt/emqttd_plugin_redis -.. _emqttd_plugin_mongo: https://github.com/emqtt/emqttd_plugin_mongo +.. _emqttd_auth_mysql: https://github.com/emqtt/emqttd_plugin_mysql +.. _emqttd_auth_pgsql: https://github.com/emqtt/emqttd_plugin_pgsql +.. _emqttd_auth_redis: https://github.com/emqtt/emqttd_plugin_redis +.. _emqttd_auth_mongo: https://github.com/emqtt/emqttd_plugin_mongo +.. _emqttd_sn: https://github.com/emqtt/emqttd_sn .. _emqttd_stomp: https://github.com/emqtt/emqttd_stomp .. _emqttd_sockjs: https://github.com/emqtt/emqttd_sockjs .. _emqttd_recon: https://github.com/emqtt/emqttd_recon From 0ee2a82a494df857b6a3b355a74a98581868509d Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 30 Aug 2016 21:35:09 +0800 Subject: [PATCH 113/116] misc fix --- docs/source/changes.rst | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index ce5bd6191..024857a23 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -7,20 +7,18 @@ Changes .. _release_2.0: -------------------------------- -Version 2.0 (West of West Lake) -------------------------------- +------------------------------------- +Version 2.0-beta1 (West of West Lake) +------------------------------------- *Release Date: 2016-08-30* *Release Name: West of West Lake* -.. NOTE:: Dont' upgrade 1.x production deployment to 2.0-beta1 release. - EMQ - Shortened Project Name ---------------------------- -Adopt a shortened projectname: EMQ(Erlang/Enterprise/Elastic MQTT Broker),E means Erlang/OTP, Enterprise and Elastic. +Adopt a shortened project name: EMQ(Erlang/Enterprise/Elastic MQTT Broker),E means Erlang/OTP, Enterprise and Elastic. Improve the Release Management ------------------------------ @@ -85,19 +83,19 @@ MQTT-SN Protocol Plugin The MQTT-SN Protocol Plugin `emqttd_sn`_ has been ready in 2.0-beta1 release. The default UDP port of MQTT-SN is 1884. -Load the plugin:: +Load the plugin:: ./bin/emqttd_ctl plugins load emqttd_sn -Improve Design of PubSub and Router ------------------------------------ +Improve The PubSub Design +------------------------- .. image:: _static/images/publish.png -Improve Plugin Management -------------------------- +Improve The Plugin Management +----------------------------- -The plugin of EMQ 2.0 broker is a normal erlang application which depends on and extends 'emqttd'. User can create a standalone plugin application project, and add it to `emqttd-relx`_ Makefiel as a dependency. +The plugin of EMQ 2.0 broker is a normal erlang application which depends on and extends 'emqttd'. You can create a standalone plugin application project, and add it to `emqttd-relx`_ Makefile as a DEP. All the plugins' config files will be copied to emqttd/etc/plugins/ folder when making emqttd brinary packages in `emqttd-relx`_ project:: From ff1c6f363a244733b64f5cd83f4810c11c2ebc21 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 30 Aug 2016 21:36:52 +0800 Subject: [PATCH 114/116] 2.0 Documentation --- docs/source/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 024857a23..ebf3c8b0c 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -65,7 +65,7 @@ Improve Git Branches Management New Config Syntax ----------------- -Since 2.0-beta1 release the configuration file of the broker and plugins adopt a new syntax like rebar.config or relx.config: +Since 2.0-beta1 release the configuration file of the broker and plugins adopt a new syntax like rebar.config and relx.config: etc/emqttd.conf for example:: @@ -121,7 +121,7 @@ All the plugins' config files will be copied to emqttd/etc/plugins/ folder when EMQ 2.0 Documentation --------------------- -http://emqtt.io/docs/v2/index.html or http://docs.emqtt.com/emq20/ +http://emqtt.io/docs/v2/index.html .. _release_1.1.3: From 49a30471b08066c4e7582eb52e90e4df726f3063 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 30 Aug 2016 21:38:41 +0800 Subject: [PATCH 115/116] the --- docs/source/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index ebf3c8b0c..01a4ad3b1 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -87,12 +87,12 @@ Load the plugin:: ./bin/emqttd_ctl plugins load emqttd_sn -Improve The PubSub Design +Improve the PubSub Design ------------------------- .. image:: _static/images/publish.png -Improve The Plugin Management +Improve the Plugin Management ----------------------------- The plugin of EMQ 2.0 broker is a normal erlang application which depends on and extends 'emqttd'. You can create a standalone plugin application project, and add it to `emqttd-relx`_ Makefile as a DEP. From decaaeacdf1f978be33b564dbe398db6ae3a3ecb Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 30 Aug 2016 21:46:58 +0800 Subject: [PATCH 116/116] Git Branch --- docs/source/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index 01a4ad3b1..33c400101 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -45,8 +45,8 @@ The rebar which is used in 1.x release is replaced by `erlang.mk`_ and `relx`_ t You can check the 'Makefile' and 'relx.config' in the release project of the borker: `emqttd-relx`_ . -Improve Git Branches Management -------------------------------- +Improve Git Branch Management +----------------------------- +------------+-------------------------------------------+ | stable | 1.x Stable Branch |