From 750fb939b1246c4eb0d1616f0ed2474c0e2fa293 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 27 May 2015 15:32:04 +0800 Subject: [PATCH 001/104] mod presence --- apps/emqttd/include/emqttd_systop.hrl | 12 +-- apps/emqttd/src/emqttd_app.erl | 3 +- apps/emqttd/src/emqttd_broker.erl | 16 ++++ apps/emqttd/src/emqttd_client.erl | 6 +- apps/emqttd/src/emqttd_cluster.erl | 1 + apps/emqttd/src/emqttd_event.erl | 122 ------------------------ apps/emqttd/src/emqttd_metrics.erl | 8 +- apps/emqttd/src/emqttd_mod_presence.erl | 53 ++++++++++ apps/emqttd/src/emqttd_protocol.erl | 4 +- apps/emqttd/src/emqttd_stats.erl | 7 +- 10 files changed, 91 insertions(+), 141 deletions(-) delete mode 100644 apps/emqttd/src/emqttd_event.erl create mode 100644 apps/emqttd/src/emqttd_mod_presence.erl diff --git a/apps/emqttd/include/emqttd_systop.hrl b/apps/emqttd/include/emqttd_systop.hrl index d2c929273..9308ba682 100644 --- a/apps/emqttd/include/emqttd_systop.hrl +++ b/apps/emqttd/include/emqttd_systop.hrl @@ -43,8 +43,6 @@ -define(SYSTOP_CLIENTS, [ 'clients/count', % clients connected current 'clients/max' % max clients connected - %'clients/connected', - %'clients/disconnected', ]). %%------------------------------------------------------------------------------ @@ -59,12 +57,12 @@ %% $SYS Topics for Subscribers %%------------------------------------------------------------------------------ -define(SYSTOP_PUBSUB, [ - 'queues/count', % ... - 'queues/max', % ... - 'topics/count', % ... + 'topics/count', % ... 'topics/max', % ... - 'subscribers/count', % ... - 'subscribers/max' % ... + 'subscribers/count', % ... + 'subscribers/max', % ... + 'queues/count', % ... + 'queues/max' % ... ]). %%------------------------------------------------------------------------------ diff --git a/apps/emqttd/src/emqttd_app.erl b/apps/emqttd/src/emqttd_app.erl index 5a8daf60f..138808b08 100644 --- a/apps/emqttd/src/emqttd_app.erl +++ b/apps/emqttd/src/emqttd_app.erl @@ -68,8 +68,7 @@ print_vsn() -> ?PRINT("~s ~s is running now~n", [Desc, Vsn]). start_servers(Sup) -> - Servers = [{"emqttd event", emqttd_event}, - {"emqttd trace", emqttd_trace}, + Servers = [{"emqttd trace", emqttd_trace}, {"emqttd pooler", {supervisor, emqttd_pooler_sup}}, {"emqttd client manager", {supervisor, emqttd_cm_sup}}, {"emqttd session manager", {supervisor, emqttd_sm_sup}}, diff --git a/apps/emqttd/src/emqttd_broker.erl b/apps/emqttd/src/emqttd_broker.erl index bff331d9c..526d824b4 100644 --- a/apps/emqttd/src/emqttd_broker.erl +++ b/apps/emqttd/src/emqttd_broker.erl @@ -39,6 +39,9 @@ %% API Function Exports -export([start_link/0]). +%% Running nodes +-export([running_nodes/0]). + %% Event API -export([subscribe/1, notify/2]). @@ -71,6 +74,13 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +%%------------------------------------------------------------------------------ +%% @doc Get running nodes +%% @end +%%------------------------------------------------------------------------------ +running_nodes() -> + mnesia:system_info(running_db_nodes). + %%------------------------------------------------------------------------------ %% @doc Subscribe broker event %% @end @@ -205,6 +215,7 @@ init([]) -> random:seed(now()), ets:new(?BROKER_TAB, [set, public, named_table]), % Create $SYS Topics + emqttd_pubsub:create(<<"$SYS/brokers">>), [ok = create_topic(Topic) || Topic <- ?SYSTOP_BROKERS], % Tick {ok, #state{started_at = os:timestamp(), tick_tref = start_tick(tick)}, hibernate}. @@ -244,6 +255,7 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info(tick, State) -> + retain(brokers), retain(version, list_to_binary(version())), retain(sysdescr, list_to_binary(sysdescr())), publish(uptime, list_to_binary(uptime(State))), @@ -266,6 +278,10 @@ code_change(_OldVsn, State, _Extra) -> create_topic(Topic) -> emqttd_pubsub:create(emqtt_topic:systop(Topic)). +retain(brokers) -> + Payload = list_to_binary(string:join([atom_to_list(N) || N <- running_nodes()], ",")), + publish(#mqtt_message{retain = true, topic = <<"$SYS/brokers">>, payload = Payload}). + retain(Topic, Payload) when is_binary(Payload) -> publish(#mqtt_message{retain = true, topic = emqtt_topic:systop(Topic), diff --git a/apps/emqttd/src/emqttd_client.erl b/apps/emqttd/src/emqttd_client.erl index 0c9e2413a..0fd1db69a 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/apps/emqttd/src/emqttd_client.erl @@ -257,8 +257,10 @@ inc(_) -> notify(disconnected, _Reason, undefined) -> ingore; notify(disconnected, {shutdown, Reason}, ProtoState) -> - emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}); + %emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}); + ok; notify(disconnected, Reason, ProtoState) -> - emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}). + %emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}). + ok. diff --git a/apps/emqttd/src/emqttd_cluster.erl b/apps/emqttd/src/emqttd_cluster.erl index 242661119..bd5a1a2ac 100644 --- a/apps/emqttd/src/emqttd_cluster.erl +++ b/apps/emqttd/src/emqttd_cluster.erl @@ -34,6 +34,7 @@ %% @doc Get running nodes %% @end %%------------------------------------------------------------------------------ +%%TODO: remove... running_nodes() -> mnesia:system_info(running_db_nodes). diff --git a/apps/emqttd/src/emqttd_event.erl b/apps/emqttd/src/emqttd_event.erl deleted file mode 100644 index e226de5de..000000000 --- a/apps/emqttd/src/emqttd_event.erl +++ /dev/null @@ -1,122 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd event manager. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_event). - --author("Feng Lee "). - --include_lib("emqtt/include/emqtt.hrl"). - -%% API Function Exports --export([start_link/0, add_handler/2, notify/1]). - -%% gen_event Function Exports --export([init/1, handle_event/2, handle_call/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {systop}). - -%%------------------------------------------------------------------------------ -%% @doc Start event manager -%% @end -%%------------------------------------------------------------------------------ --spec start_link() -> {ok, pid()} | {error, any()}. -start_link() -> - case gen_event:start_link({local, ?MODULE}) of - {ok, Pid} -> - add_handler(?MODULE, []), - {ok, Pid}; - {error, Reason} -> - {error, Reason} - end. - -add_handler(Handler, Args) -> - gen_event:add_handler(?MODULE, Handler, Args). - -notify(Event) -> - gen_event:notify(?MODULE, Event). - -%%%============================================================================= -%%% gen_event callbacks -%%%============================================================================= - -init([]) -> - SysTop = list_to_binary(lists:concat(["$SYS/brokers/", node(), "/"])), - {ok, #state{systop = SysTop}}. - -handle_event({connected, ClientId, Params}, State = #state{systop = SysTop}) -> - Topic = <>, - Msg = #mqtt_message{topic = Topic, payload = payload(connected, Params)}, - emqttd_pubsub:publish(event, Msg), - {ok, State}; - -%%TODO: Protect from undefined clientId... -handle_event({disconnected, undefined, Reason}, State = #state{systop = SysTop}) -> - {ok, State}; - -handle_event({disconnected, ClientId, Reason}, State = #state{systop = SysTop}) -> - Topic = <>, - Msg = #mqtt_message{topic = Topic, payload = payload(disconnected, Reason)}, - emqttd_pubsub:publish(event, Msg), - {ok, State}; - -handle_event({subscribed, ClientId, TopicTable}, State) -> - lager:error("TODO: subscribed ~s, ~p", [ClientId, TopicTable]), - {ok, State}; - -handle_event({unsubscribed, ClientId, Topics}, State) -> - lager:error("TODO: unsubscribed ~s, ~p", [ClientId, Topics]), - {ok, State}; - -handle_event(_Event, State) -> - {ok, State}. - -handle_call(_Request, State) -> - Reply = ok, - {ok, Reply, State}. - -handle_info(_Info, State) -> - {ok, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -payload(connected, Params) -> - From = proplists:get_value(from, Params), - Proto = proplists:get_value(protocol, Params), - Sess = proplists:get_value(session, Params), - iolist_to_binary(io_lib:format("from: ~s~nprotocol: ~p~nsession: ~s", [From, Proto, Sess])); - -payload(disconnected, Reason) -> - list_to_binary(io_lib:format("reason: ~p", [Reason])). - diff --git a/apps/emqttd/src/emqttd_metrics.erl b/apps/emqttd/src/emqttd_metrics.erl index e4a7fea15..550d5fcd7 100644 --- a/apps/emqttd/src/emqttd_metrics.erl +++ b/apps/emqttd/src/emqttd_metrics.erl @@ -163,7 +163,7 @@ init([]) -> % Init metrics [create_metric(Metric) || Metric <- Metrics], % $SYS Topics for metrics - [ok = create_topic(Topic) || {_, Topic} <- Metrics], + [ok = emqttd_pubsub:create(metric_topic(Topic)) || {_, Topic} <- Metrics], % Tick to publish metrics {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. @@ -192,7 +192,7 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= publish(Metric, Val) -> - emqttd_pubsub:publish(metrics, #mqtt_message{topic = emqtt_topic:systop(Metric), + emqttd_pubsub:publish(metrics, #mqtt_message{topic = metric_topic(Metric), payload = emqttd_util:integer_to_binary(Val)}). create_metric({gauge, Name}) -> @@ -202,7 +202,7 @@ create_metric({counter, Name}) -> Schedulers = lists:seq(1, erlang:system_info(schedulers)), [ets:insert(?METRIC_TAB, {{Name, I}, 0}) || I <- Schedulers]. -create_topic(Topic) -> - emqttd_pubsub:create(emqtt_topic:systop(Topic)). +metric_topic(Metric) -> + emqtt_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/apps/emqttd/src/emqttd_mod_presence.erl new file mode 100644 index 000000000..ca66177e7 --- /dev/null +++ b/apps/emqttd/src/emqttd_mod_presence.erl @@ -0,0 +1,53 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd presence management module. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_mod_presence). + +-include_lib("emqtt/include/emqtt.hrl"). + +-export([load/1, unload/1]). + +-export([client_connected/2, client_disconnected/2]). + +load(Opts) -> + emqttd_broker:hook(client_connected, {?MODULE, client_connected}, {?MODULE, client_connected, [Opts]}), + emqttd_broker:hook(client_disconnected, {?MODULE, client_disconnected}, {?MODULE, client_disconnected, [Opts]}), + {ok, Opts}. + +client_connected({Client, ClientId}, _Opts) -> + Topic = emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])), + Payload = iolist_to_binary(mochijson2:encode([{ts, emqttd_util:timestamp()}])), + emqttd_pubsub:publish(presence, #mqtt_message{topic = Topic, payload = Payload}). + +client_disconnected({ClientId, Reason}, _Opts) -> + Topic = emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])), + Payload = iolist_to_binary(mochijson2:encode([{reason, Reason}, {ts, emqttd_util:timestamp()}])), + emqttd_pubsub:publish(presence, #mqtt_message{topic = Topic, payload = Payload}). + +unload(_Opts) -> + emqttd_broker:unhook(client_connected, {?MODULE, client_connected}), + emqttd_broker:unhook(client_disconnected, {?MODULE, client_disconnected}). + diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 08b24674f..30013ac3e 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -422,7 +422,7 @@ notify(connected, ReturnCode, #proto_state{peername = Peername, Params = [{from, emqttd_net:format(Peername)}, {protocol, ProtoVer}, {session, Sess}, - {connack, ReturnCode}], - emqttd_event:notify({connected, ClientId, Params}). + {connack, ReturnCode}]. + %emqttd_event:notify({connected, ClientId, Params}). diff --git a/apps/emqttd/src/emqttd_stats.erl b/apps/emqttd/src/emqttd_stats.erl index d55a5942c..0b5e65522 100644 --- a/apps/emqttd/src/emqttd_stats.erl +++ b/apps/emqttd/src/emqttd_stats.erl @@ -126,7 +126,7 @@ init([]) -> Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB, [ets:insert(?STATS_TAB, {Topic, 0}) || Topic <- Topics], % Create $SYS Topics - [ok = emqttd_pubsub:create(emqtt_topic:systop(Topic)) || Topic <- Topics], + [ok = emqttd_pubsub:create(stats_topic(Topic)) || Topic <- Topics], % Tick to publish stats {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. @@ -154,6 +154,9 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= publish(Stat, Val) -> - emqttd_pubsub:publish(stats, #mqtt_message{topic = emqtt_topic:systop(Stat), + emqttd_pubsub:publish(stats, #mqtt_message{topic = stats_topic(Stat), payload = emqttd_util:integer_to_binary(Val)}). +stats_topic(Stat) -> + emqtt_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). + From b94fd7ffd63577938cfc9dbca5c4be1e43405c0a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 27 May 2015 15:32:49 +0800 Subject: [PATCH 002/104] mod presence --- rel/files/emqttd.config | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 6a723470c..aa63385a5 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -108,8 +108,13 @@ ]}, %% Modules {modules, [ + %% Client presence management module. + %% Publish messages when client connected or disconnected + {presence, []}, + %% Subscribe topics automatically when client connected {autosub, [{"$Q/client/$c", 0}]} + %% Rewrite rules %% {rewrite, [{file, "etc/rewrite.config"}]} From c3836027831a5d005fda1051acf631b0632c163e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 00:30:55 +0800 Subject: [PATCH 003/104] {incl_cond, exclude} --- rel/reltool.config | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/rel/reltool.config b/rel/reltool.config index ac5d09e83..023774fe7 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,19 +4,23 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.7.0", + {rel, "emqttd", "0.8.0", [ kernel, stdlib, sasl, + asn1, syntax_tools, ssl, crypto, %mnesia, + xmerl, os_mon, inets, goldrush, + compiler, lager, + {gen_logger, load}, gproc, esockd, mochiweb, @@ -30,7 +34,7 @@ ]}, {boot_rel, "emqttd"}, {profile, embedded}, - {incl_cond, derived}, + {incl_cond, exclude}, %{mod_cond, derived}, {excl_archive_filters, [".*"]}, %% Do not archive built libs {excl_sys_filters, ["^bin/(?!start_clean.boot)", @@ -40,16 +44,20 @@ {app, kernel, [{incl_cond, include}]}, {app, stdlib, [{incl_cond, include}]}, {app, sasl, [{incl_cond, include}]}, - {app, crypto, [{mod_cond, app}, {incl_cond, include}]}, - {app, ssl, [{mod_cond, app}, {incl_cond, include}]}, - {app, os_mon, [{mod_cond, app}, {incl_cond, include}]}, - {app, syntax_tools, [{mod_cond, app}, {incl_cond, include}]}, - {app, public_key, [{mod_cond, app}, {incl_cond, include}]}, - {app, mnesia, [{mod_cond, app}, {incl_cond, include}]}, - {app, inets, [{mod_cond, app},{incl_cond, include}]}, - {app, goldrush, [{mod_cond, app}, {incl_cond, include}]}, - {app, lager, [{mod_cond, app}, {incl_cond, include}]}, - {app, gproc, [{mod_cond, app}, {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, inets, [{incl_cond, include}]}, + {app, compiler, [{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, emqtt, [{mod_cond, app}, {incl_cond, include}]}, From 5eed2f1d7f69204e6ef9d8d2f35501a7148abb6f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 00:54:20 +0800 Subject: [PATCH 004/104] rm emqttd_dashboard --- .../src/emqttd_dashboard.app.src | 12 ------ .../emqttd_dashboard/src/emqttd_dashboard.erl | 38 ------------------- .../src/emqttd_dashboard_app.erl | 27 ------------- .../src/emqttd_dashboard_sup.erl | 27 ------------- 4 files changed, 104 deletions(-) delete mode 100644 plugins/emqttd_dashboard/src/emqttd_dashboard.app.src delete mode 100644 plugins/emqttd_dashboard/src/emqttd_dashboard.erl delete mode 100644 plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl delete mode 100644 plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard.app.src b/plugins/emqttd_dashboard/src/emqttd_dashboard.app.src deleted file mode 100644 index 12342ddc5..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_dashboard, - [ - {description, "emqttd management dashboard"}, - {vsn, "0.1"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {emqttd_dashboard_app, []}}, - {env, []} -]}. diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard.erl b/plugins/emqttd_dashboard/src/emqttd_dashboard.erl deleted file mode 100644 index fae297b16..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard.erl +++ /dev/null @@ -1,38 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd management dashboard. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_dashboard). - --author("Feng Lee "). - --export([handle_request/1]). - -%%TODO... - -handle_request(Req) -> - Req:ok("hello!"). - - diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl b/plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl deleted file mode 100644 index 8e0898679..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(emqttd_dashboard_app). - --behaviour(application). - -%% Application callbacks --export([start/2, stop/1]). - -%% =================================================================== -%% Application callbacks -%% =================================================================== - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqttd_dashboard_sup:start_link(), - open_listener(application:get_env(listener)), - {ok, Sup}. - -stop(_State) -> - ok. - -%% open http port -open_listener({_Http, Port, Options}) -> - MFArgs = {emqttd_dashboard, handle_request, []}, - mochiweb:start_http(Port, Options, MFArgs). - -close_listener(Port) -> - mochiweb:stop_http(Port). - diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl b/plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl deleted file mode 100644 index 895f00d83..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(emqttd_dashboard_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). - -%% =================================================================== -%% API functions -%% =================================================================== - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% =================================================================== -%% Supervisor callbacks -%% =================================================================== - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - From 3c00b26fe2170a17e17ba7acb46a39953727a4a6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 01:02:02 +0800 Subject: [PATCH 005/104] submodule --- .gitmodules | 3 ++ plugins/emqttd_dashboard | 1 + tests/benchmarks/high-mqtt.xml | 64 +++++++++++++++++++++++++++++ tests/org.eclipse.paho.mqtt.testing | 2 +- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 160000 plugins/emqttd_dashboard create mode 100644 tests/benchmarks/high-mqtt.xml diff --git a/.gitmodules b/.gitmodules index 61767a9fb..2d15edfc9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "tests/org.eclipse.paho.mqtt.testing"] path = tests/org.eclipse.paho.mqtt.testing url = git://git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.testing.git +[submodule "plugins/emqttd_dashboard"] + path = plugins/emqttd_dashboard + url = https://github.com/emqtt/emqttd_dashboard.git diff --git a/plugins/emqttd_dashboard b/plugins/emqttd_dashboard new file mode 160000 index 000000000..300bb7445 --- /dev/null +++ b/plugins/emqttd_dashboard @@ -0,0 +1 @@ +Subproject commit 300bb74452764ac27c261ae000495791397b2a28 diff --git a/tests/benchmarks/high-mqtt.xml b/tests/benchmarks/high-mqtt.xml new file mode 100644 index 000000000..56907bb2b --- /dev/null +++ b/tests/benchmarks/high-mqtt.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test_message + + + + + + + + + + diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index 17afdf7fb..bdc690db8 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 +Subproject commit bdc690db847cec1c682e604dca571c72ff756305 From 500b9cb4f81fc39b994a7eca9dcd426f8740b244 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 01:11:36 +0800 Subject: [PATCH 006/104] rm plguins/emqttd_auth_plugins --- .../emqttd}/src/emqttd_auth_ldap.erl | 0 plugins/emqttd_auth_ldap/.placeholder | 0 plugins/emqttd_auth_ldap/README.md | 23 -------- plugins/emqttd_auth_ldap/etc/plugin.config | 12 ---- .../src/emqttd_auth_ldap.app.src | 12 ---- .../src/emqttd_auth_ldap_app.erl | 58 ------------------- 6 files changed, 105 deletions(-) rename {plugins/emqttd_auth_ldap => apps/emqttd}/src/emqttd_auth_ldap.erl (100%) delete mode 100644 plugins/emqttd_auth_ldap/.placeholder delete mode 100644 plugins/emqttd_auth_ldap/README.md delete mode 100644 plugins/emqttd_auth_ldap/etc/plugin.config delete mode 100644 plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src delete mode 100644 plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl b/apps/emqttd/src/emqttd_auth_ldap.erl similarity index 100% rename from plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl rename to apps/emqttd/src/emqttd_auth_ldap.erl diff --git a/plugins/emqttd_auth_ldap/.placeholder b/plugins/emqttd_auth_ldap/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_auth_ldap/README.md b/plugins/emqttd_auth_ldap/README.md deleted file mode 100644 index 083b318ac..000000000 --- a/plugins/emqttd_auth_ldap/README.md +++ /dev/null @@ -1,23 +0,0 @@ -## Overview - -Authentication with LDAP. - -## Plugin Config - -``` - {emqttd_auth_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 Plugin - -Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded automatically. - diff --git a/plugins/emqttd_auth_ldap/etc/plugin.config b/plugins/emqttd_auth_ldap/etc/plugin.config deleted file mode 100644 index ac582d7d4..000000000 --- a/plugins/emqttd_auth_ldap/etc/plugin.config +++ /dev/null @@ -1,12 +0,0 @@ -[ - {emqttd_auth_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"}]} - ]} -]. diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src b/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src deleted file mode 100644 index e699fdba3..000000000 --- a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_auth_ldap, - [ - {description, "emqttd LDAP Authentication Plugin"}, - {vsn, "1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, { emqttd_auth_ldap_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl b/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl deleted file mode 100644 index 1cea23075..000000000 --- a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl +++ /dev/null @@ -1,58 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% LDAP Authentication Plugin. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_ldap_app). - --behaviour(application). -%% Application callbacks --export([start/2, prep_stop/1, stop/1]). - --behaviour(supervisor). -%% Supervisor callbacks --export([init/1]). - -%%%============================================================================= -%%% Application callbacks -%%%============================================================================= - -start(_StartType, _StartArgs) -> - Env = application:get_all_env(emqttd_auth_ldap), - emqttd_access_control:register_mod(auth, emqttd_auth_ldap, Env), - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -prep_stop(State) -> - emqttd_access_control:unregister_mod(auth, emqttd_auth_ldap), State. - -stop(_State) -> - ok. - -%%%============================================================================= -%%% Supervisor callbacks(Dummy) -%%%============================================================================= - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - From 4f83c40b62c5f6d5d1f635a95188d360506bb389 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 01:26:40 +0800 Subject: [PATCH 007/104] eldap --- rel/reltool.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rel/reltool.config b/rel/reltool.config index 023774fe7..268f19f83 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -14,6 +14,7 @@ ssl, crypto, %mnesia, + eldap, xmerl, os_mon, inets, @@ -52,6 +53,7 @@ {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, goldrush, [{incl_cond, include}]}, From 2b19f12e25b9cbce2b1ddf00f795c06f24cbfe55 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 01:27:22 +0800 Subject: [PATCH 008/104] ldap --- rel/files/emqttd.config | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index aa63385a5..97cdc9a18 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -45,8 +45,22 @@ {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, []} ]}, From b96206e1aeafd58a67fde1b8b8b31dc711361dd1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 01:28:33 +0800 Subject: [PATCH 009/104] mv to emqttd_plugin_mysql project --- plugins/emqttd_plugin_mysql/.placehodler | 0 plugins/emqttd_plugin_mysql/Makefile | 32 -- plugins/emqttd_plugin_mysql/README.md | 49 --- plugins/emqttd_plugin_mysql/c_src/base64.c | 151 ---------- plugins/emqttd_plugin_mysql/c_src/base64.h | 47 --- .../c_src/emqttd_plugin_mysql_app.c | 60 ---- .../c_src/emqttd_plugin_mysql_app.h | 44 --- .../emqttd_plugin_mysql/c_src/pbkdf2_check.c | 278 ------------------ plugins/emqttd_plugin_mysql/c_src/util.c | 26 -- plugins/emqttd_plugin_mysql/etc/plugin.config | 23 -- .../emqttd_plugin_mysql/etc/plugins.config | 23 -- .../emqttd_plugin_mysql/include/emqttd.hrl | 112 ------- plugins/emqttd_plugin_mysql/priv/.placeholder | 0 plugins/emqttd_plugin_mysql/rebar | Bin 131900 -> 0 bytes plugins/emqttd_plugin_mysql/rebar.config | 32 -- .../src/emqttd_acl_mysql.erl | 70 ----- .../src/emqttd_auth_mysql.erl | 110 ------- .../src/emqttd_plugin_mysql.app.src | 12 - .../src/emqttd_plugin_mysql_app.erl | 80 ----- 19 files changed, 1149 deletions(-) delete mode 100644 plugins/emqttd_plugin_mysql/.placehodler delete mode 100755 plugins/emqttd_plugin_mysql/Makefile delete mode 100644 plugins/emqttd_plugin_mysql/README.md delete mode 100755 plugins/emqttd_plugin_mysql/c_src/base64.c delete mode 100755 plugins/emqttd_plugin_mysql/c_src/base64.h delete mode 100755 plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c delete mode 100755 plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h delete mode 100644 plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c delete mode 100755 plugins/emqttd_plugin_mysql/c_src/util.c delete mode 100644 plugins/emqttd_plugin_mysql/etc/plugin.config delete mode 100644 plugins/emqttd_plugin_mysql/etc/plugins.config delete mode 100644 plugins/emqttd_plugin_mysql/include/emqttd.hrl delete mode 100644 plugins/emqttd_plugin_mysql/priv/.placeholder delete mode 100755 plugins/emqttd_plugin_mysql/rebar delete mode 100755 plugins/emqttd_plugin_mysql/rebar.config delete mode 100644 plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl delete mode 100644 plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl delete mode 100644 plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src delete mode 100644 plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl diff --git a/plugins/emqttd_plugin_mysql/.placehodler b/plugins/emqttd_plugin_mysql/.placehodler deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_plugin_mysql/Makefile b/plugins/emqttd_plugin_mysql/Makefile deleted file mode 100755 index 086a1fa40..000000000 --- a/plugins/emqttd_plugin_mysql/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -REBAR?=./rebar - - -all: build - - -clean: - $(REBAR) clean - rm -rf logs - rm -rf .eunit - rm -f test/*.beam - - -distclean: clean - git clean -fxd - -build: depends - $(REBAR) compile - - -eunit: - $(REBAR) eunit skip_deps=true - - -check: build eunit - - -%.beam: %.erl - erlc -o test/ $< - - -.PHONY: all clean distclean depends build eunit check diff --git a/plugins/emqttd_plugin_mysql/README.md b/plugins/emqttd_plugin_mysql/README.md deleted file mode 100644 index 481a8f3e7..000000000 --- a/plugins/emqttd_plugin_mysql/README.md +++ /dev/null @@ -1,49 +0,0 @@ -## Overview - -Authentication with user table of MySQL database. - -## etc/plugin.config - -```erlang -[ - {emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, ""}, - {password, ""}, - {database, "mqtt"}, - {encoding, utf8} - ]}, - {emqttd_auth_mysql, [ - {user_table, mqtt_users}, - %% plain password only - {password_hash, plain}, - {field_mapper, [ - {username, username}, - {password, password} - ]} - ]} -]. -``` - -## Users Table(Demo) - -Notice: This is a demo table. You could authenticate with any user tables. - -``` -CREATE TABLE `mqtt_users` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(60) DEFAULT NULL, - `password` varchar(60) DEFAULT NULL, - `salt` varchar(20) DEFAULT NULL, - `created` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `mqtt_users_username` (`username`) -) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -``` - -## Load Plugin - -Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded by the broker. - diff --git a/plugins/emqttd_plugin_mysql/c_src/base64.c b/plugins/emqttd_plugin_mysql/c_src/base64.c deleted file mode 100755 index 457c1a138..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/base64.c +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 1995, 1996, 1997 Kungliga Tekniska Hgskolan - * (Royal Institute of Technology, Stockholm, Sweden). - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * This product includes software developed by the Kungliga Tekniska - * Hgskolan and its contributors. - * - * 4. Neither the name of the Institute nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include -/*RCSID("$Id: base64.c,v 1.1 2005/02/11 07:34:35 jpm Exp jpm $");*/ -#endif -#include -#include -#include "base64.h" - -static char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -static int pos(char c) -{ - char *p; - for(p = base64; *p; p++) - if(*p == c) - return p - base64; - return -1; -} - -int base64_encode(const void *data, int size, char **str) -{ - char *s, *p; - int i; - int c; - unsigned char *q; - - p = s = (char*)malloc(size*4/3+4); - if (p == NULL) - return -1; - q = (unsigned char*)data; - i=0; - for(i = 0; i < size;){ - c=q[i++]; - c*=256; - if(i < size) - c+=q[i]; - i++; - c*=256; - if(i < size) - c+=q[i]; - i++; - p[0]=base64[(c&0x00fc0000) >> 18]; - p[1]=base64[(c&0x0003f000) >> 12]; - p[2]=base64[(c&0x00000fc0) >> 6]; - p[3]=base64[(c&0x0000003f) >> 0]; - if(i > size) - p[3]='='; - if(i > size+1) - p[2]='='; - p+=4; - } - *p=0; - *str = s; - return strlen(s); -} - -int base64_decode(const char *str, void *data) -{ - const char *p; - unsigned char *q; - int c; - int x; - int done = 0; - q=(unsigned char*)data; - for(p=str; *p && !done; p+=4){ - x = pos(p[0]); - if(x >= 0) - c = x; - else{ - done = 3; - break; - } - c*=64; - - x = pos(p[1]); - if(x >= 0) - c += x; - else - return -1; - c*=64; - - if(p[2] == '=') - done++; - else{ - x = pos(p[2]); - if(x >= 0) - c += x; - else - return -1; - } - c*=64; - - if(p[3] == '=') - done++; - else{ - if(done) - return -1; - x = pos(p[3]); - if(x >= 0) - c += x; - else - return -1; - } - if(done < 3) - *q++=(c&0x00ff0000)>>16; - - if(done < 2) - *q++=(c&0x0000ff00)>>8; - if(done < 1) - *q++=(c&0x000000ff)>>0; - } - return q - (unsigned char*)data; -} diff --git a/plugins/emqttd_plugin_mysql/c_src/base64.h b/plugins/emqttd_plugin_mysql/c_src/base64.h deleted file mode 100755 index 380a31d49..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/base64.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 1995, 1996, 1997 Kungliga Tekniska Hgskolan - * (Royal Institute of Technology, Stockholm, Sweden). - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * This product includes software developed by the Kungliga Tekniska - * Hgskolan and its contributors. - * - * 4. Neither the name of the Institute nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -/* $Id: base64.h,v 1.1 2005/02/11 07:34:35 jpm Exp jpm $ */ - -#ifndef _BASE64_H_ -#define _BASE64_H_ - -int base64_encode(const void *data, int size, char **str); -int base64_decode(const char *str, void *data); - -#endif diff --git a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c deleted file mode 100755 index 374de99ac..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of Jiffy released under the MIT license. -// See the LICENSE file for more information. - -#include "emqttd_plugin_mysql_app.h" - -static int -load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) -{ - emqttd_plugin_mysql_app_st* st = enif_alloc(sizeof(emqttd_plugin_mysql_app_st)); - if(st == NULL) { - return 1; - } - - st->atom_ok = make_atom(env, "ok"); - st->atom_error = make_atom(env, "error"); - st->atom_null = make_atom(env, "null"); - st->atom_true = make_atom(env, "true"); - st->atom_false = make_atom(env, "false"); - st->atom_bignum = make_atom(env, "bignum"); - st->atom_bignum_e = make_atom(env, "bignum_e"); - st->atom_bigdbl = make_atom(env, "bigdbl"); - st->atom_partial = make_atom(env, "partial"); - st->atom_uescape = make_atom(env, "uescape"); - st->atom_pretty = make_atom(env, "pretty"); - st->atom_force_utf8 = make_atom(env, "force_utf8"); - - // Markers used in encoding - st->ref_object = make_atom(env, "$object_ref$"); - st->ref_array = make_atom(env, "$array_ref$"); - - *priv = (void*) st; - - return 0; -} - -static int -reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) -{ - return 0; -} - -static int -upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) -{ - return load(env, priv, info); -} - -static void -unload(ErlNifEnv* env, void* priv) -{ - enif_free(priv); - return; -} - -static ErlNifFunc funcs[] = -{ - {"nif_pbkdf2_check", 2, pbkdf2_check} -}; - -ERL_NIF_INIT(emqttd_plugin_mysql_app, funcs, &load, &reload, &upgrade, &unload); diff --git a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h deleted file mode 100755 index e77ae3b29..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h +++ /dev/null @@ -1,44 +0,0 @@ -// This file is part of Jiffy released under the MIT license. -// See the LICENSE file for more information. - -#ifndef EMQTTD_PLUGIN_MYSQL_APP_H -#define EMQTTD_PLUGIN_MYSQL_APP_H - -#include "erl_nif.h" - -typedef struct { - ERL_NIF_TERM atom_ok; - ERL_NIF_TERM atom_error; - ERL_NIF_TERM atom_null; - ERL_NIF_TERM atom_true; - ERL_NIF_TERM atom_false; - ERL_NIF_TERM atom_bignum; - ERL_NIF_TERM atom_bignum_e; - ERL_NIF_TERM atom_bigdbl; - ERL_NIF_TERM atom_partial; - ERL_NIF_TERM atom_uescape; - ERL_NIF_TERM atom_pretty; - ERL_NIF_TERM atom_force_utf8; - - ERL_NIF_TERM ref_object; - ERL_NIF_TERM ref_array; -} emqttd_plugin_mysql_app_st; - -ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* name); -ERL_NIF_TERM make_ok(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, ERL_NIF_TERM data); -ERL_NIF_TERM make_error(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, const char* error); - -ERL_NIF_TERM pbkdf2_check(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); - -int int_from_hex(const unsigned char* p); -int int_to_hex(int val, char* p); -int utf8_len(int c); -int utf8_esc_len(int c); -int utf8_validate(unsigned char* data, size_t size); -int utf8_to_unicode(unsigned char* buf, size_t size); -int unicode_to_utf8(int c, unsigned char* buf); -int unicode_from_pair(int hi, int lo); -int unicode_uescape(int c, char* buf); -int double_to_shortest(char *buf, size_t size, size_t* len, double val); - -#endif // Included EMQTTD_PLUGIN_MYSQL_APP_H diff --git a/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c b/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c deleted file mode 100644 index 0e40b933b..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2013 Jan-Piet Mens - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of mosquitto nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include -#include -#include -#include -#include "base64.h" -#include "erl_nif.h" -#include "emqttd_plugin_mysql_app.h" - -#define KEY_LENGTH 24 -#define SEPARATOR "$" -#define SEPARATOR1 "_" -#define TRUE (1) -#define FALSE (0) - -/* - * Split PBKDF2$... string into their components. The caller must free() - * the strings. - */ - -static int detoken(char *pbkstr, char **sha, int *iter, char **salt, char **key) -{ - char *p, *s, *save; - int rc = 1; - - save = s = strdup(pbkstr); - - if ((p = strsep(&s, SEPARATOR1)) == NULL) - goto out; - if (strcmp(p, "pbkdf2") != 0) - goto out; - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *sha = strdup(p); - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *iter = atoi(p); - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *salt = strdup(p); - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *key = strdup(p); - - rc = 0; - -out: - free(save); - return rc; -} - - ERL_NIF_TERM -pbkdf2_check(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -{ - ERL_NIF_TERM ret; - ErlNifBinary binps, binhash; - emqttd_plugin_mysql_app_st* st = enif_alloc(sizeof(emqttd_plugin_mysql_app_st)); - if(st == NULL) { - return make_atom(env, "alloc_error"); - } - - st->atom_ok = make_atom(env, "ok"); - st->atom_error = make_atom(env, "error"); - st->atom_null = make_atom(env, "null"); - st->atom_true = make_atom(env, "true"); - st->atom_false = make_atom(env, "false"); - st->atom_bignum = make_atom(env, "bignum"); - st->atom_bignum_e = make_atom(env, "bignum_e"); - st->atom_bigdbl = make_atom(env, "bigdbl"); - st->atom_partial = make_atom(env, "partial"); - st->atom_uescape = make_atom(env, "uescape"); - st->atom_pretty = make_atom(env, "pretty"); - st->atom_force_utf8 = make_atom(env, "force_utf8"); - - // Markers used in encoding - st->ref_object = make_atom(env, "$object_ref$"); - st->ref_array = make_atom(env, "$array_ref$"); - - if(argc != 2) { - return make_error(st, env, "Bad args"); - } else if(!enif_inspect_binary(env, argv[0], &binps)|!enif_inspect_binary(env, argv[1], &binhash)) { - return make_error(st, env, "Bad args password or username inspect error"); - } - - char* password = (char*)binps.data; - char* hash = (char*)binhash.data; - static char *sha, *salt, *h_pw; - int iterations, saltlen, blen; - char *b64, *keybuf; - unsigned char *out; - int match = FALSE; - const EVP_MD *evpmd; - int keylen, rc; - - if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) - return match; - - /* Determine key length by decoding base64 */ - if ((keybuf = malloc(strlen(h_pw) + 1)) == NULL) { - return make_error(st, env, "internal_error: Out Of memory"); - } - keylen = base64_decode(h_pw, keybuf); - if (keylen < 1) { - free(keybuf); - return make_atom(env, "false"); - } - free(keybuf); - - if ((out = malloc(keylen)) == NULL) { - return make_error(st, env, "Cannot allocate out; out of memory\n"); - } - -#ifdef PWDEBUG - fprintf(stderr, "sha =[%s]\n", sha); - fprintf(stderr, "iterations =%d\n", iterations); - fprintf(stderr, "salt =[%s]\n", salt); - fprintf(stderr, "h_pw =[%s]\n", h_pw); - fprintf(stderr, "kenlen =[%d]\n", keylen); -#endif - - saltlen = strlen((char *)salt); - - evpmd = EVP_sha256(); - if (strcmp(sha, "sha1") == 0) { - evpmd = EVP_sha1(); - } else if (strcmp(sha, "sha512") == 0) { - evpmd = EVP_sha512(); - } - - rc = PKCS5_PBKDF2_HMAC(password, strlen(password), - (unsigned char *)salt, saltlen, - iterations, - evpmd, keylen, out); - if (rc != 1) { - goto out; - } - - blen = base64_encode(out, keylen, &b64); - if (blen > 0) { - int i, diff = 0, hlen = strlen(h_pw); -#ifdef PWDEBUG - fprintf(stderr, "HMAC b64 =[%s]\n", b64); -#endif - - /* "manual" strcmp() to ensure constant time */ - for (i = 0; (i < blen) && (i < hlen); i++) { - diff |= h_pw[i] ^ b64[i]; - } - - match = diff == 0; - if (hlen != blen) - match = 0; - - free(b64); - } - -out: - free(sha); - free(salt); - free(h_pw); - free(out); - - if(match == 0){ - ret = make_atom(env, "false"); - }else{ - ret = make_atom(env, "true"); - } - return ret; -} - -int pbkdf2_check_native(char *password, char *hash) -{ - static char *sha, *salt, *h_pw; - int iterations, saltlen, blen; - char *b64; - unsigned char key[128]; - int match = FALSE; - const EVP_MD *evpmd; - - if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) - return match; - -#ifdef PWDEBUG - fprintf(stderr, "sha =[%s]\n", sha); - fprintf(stderr, "iterations =%d\n", iterations); - fprintf(stderr, "salt =[%s]\n", salt); - fprintf(stderr, "h_pw =[%s]\n", h_pw); -#endif - - saltlen = strlen((char *)salt); - - evpmd = EVP_sha256(); - if (strcmp(sha, "sha1") == 0) { - evpmd = EVP_sha1(); - } else if (strcmp(sha, "sha512") == 0) { - evpmd = EVP_sha512(); - } - - PKCS5_PBKDF2_HMAC(password, strlen(password), - (unsigned char *)salt, saltlen, - iterations, - evpmd, KEY_LENGTH, key); - - blen = base64_encode(key, KEY_LENGTH, &b64); - if (blen > 0) { - int i, diff = 0, hlen = strlen(h_pw); -#ifdef PWDEBUG - fprintf(stderr, "HMAC b64 =[%s]\n", b64); -#endif - - /* "manual" strcmp() to ensure constant time */ - for (i = 0; (i < blen) && (i < hlen); i++) { - diff |= h_pw[i] ^ b64[i]; - } - - match = diff == 0; - if (hlen != blen) - match = 0; - - free(b64); - } - - free(sha); - free(salt); - free(h_pw); - - return match; -} -int main() -{ - // char password[] = "hello"; - // char PB1[] = "PBKDF2$sha256$10000$eytf9sEo8EprP9P3$2eO6tROHiqI3bm+gg+vpmWooWMpz1zji"; - char password[] = "supersecret"; - //char PB1[] = "PBKDF2$sha256$10000$YEbSTt8FaMRDq/ib$Kt97+sMCYg00mqMOBAYinqZlnxX8HqHk"; - char PB1[] = "pbkdf2_sha256$10000$YEbSTt8FaMRDq/ib$Kt97+sMCYg00mqMOBAYinqZlnxX8HqHk"; - // char PB1[] = "PBKDF2$sha1$10000$XWfyPLeC9gsD6SbI$HOnjU4Ux7RpeBHdqYxpIGH1R5qCCtNA1"; - // char PB1[] = "PBKDF2$sha512$10000$v/aaCgBZ+VZN5L8n$BpgjSTyb4weVxr9cA2mvQ+jaCyaAPeYe"; - int match; - - printf("Checking password [%s] for %s\n", password, PB1); - - match = pbkdf2_check_native(password, PB1); - printf("match == %d\n", match); - return match; -} diff --git a/plugins/emqttd_plugin_mysql/c_src/util.c b/plugins/emqttd_plugin_mysql/c_src/util.c deleted file mode 100755 index 8c0291b22..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/util.c +++ /dev/null @@ -1,26 +0,0 @@ -// This file is part of Jiffy released under the MIT license. -// See the LICENSE file for more information. - -#include "emqttd_plugin_mysql_app.h" - -ERL_NIF_TERM -make_atom(ErlNifEnv* env, const char* name) -{ - ERL_NIF_TERM ret; - if(enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) { - return ret; - } - return enif_make_atom(env, name); -} - -ERL_NIF_TERM -make_ok(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, ERL_NIF_TERM value) -{ - return enif_make_tuple2(env, st->atom_ok, value); -} - -ERL_NIF_TERM -make_error(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, const char* error) -{ - return enif_make_tuple2(env, st->atom_error, make_atom(env, error)); -} diff --git a/plugins/emqttd_plugin_mysql/etc/plugin.config b/plugins/emqttd_plugin_mysql/etc/plugin.config deleted file mode 100644 index 4b5044bde..000000000 --- a/plugins/emqttd_plugin_mysql/etc/plugin.config +++ /dev/null @@ -1,23 +0,0 @@ -[ - {emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, "root"}, - {password, "root"}, - {database, "emqtt"}, - {encoding, utf8} - ]}, - {emqttd_plugin_mysql, [ - {users_table, auth_user}, - {acls_table, auth_acl}, - {field_mapper, [ - {username, username}, - {password, password, pbkdf2}, - {user_super, is_super_user}, - {acl_username, username}, - {acl_rw, rw}, - {acl_topic, topic} - ]} - ]} -]. diff --git a/plugins/emqttd_plugin_mysql/etc/plugins.config b/plugins/emqttd_plugin_mysql/etc/plugins.config deleted file mode 100644 index 7b132dd3f..000000000 --- a/plugins/emqttd_plugin_mysql/etc/plugins.config +++ /dev/null @@ -1,23 +0,0 @@ -[ - {emysql, [ - {pool, 4}, - {host, "59.188.253.198"}, - {port, 3306}, - {username, "root"}, - {password, "lhroot."}, - {database, "musicfield"}, - {encoding, utf8} - ]}, - {emqttd_plugin_mysql, [ - {users_table, auth_user}, - {acls_table, auth_acl}, - {field_mapper, [ - {username, username}, - {password, password, pbkdf2}, - {user_super, is_super_user}, - {acl_username, username}, - {acl_rw, rw}, - {acl_topic, topic} - ]} - ]} -]. diff --git a/plugins/emqttd_plugin_mysql/include/emqttd.hrl b/plugins/emqttd_plugin_mysql/include/emqttd.hrl deleted file mode 100644 index 9c2ab934a..000000000 --- a/plugins/emqttd_plugin_mysql/include/emqttd.hrl +++ /dev/null @@ -1,112 +0,0 @@ -%%------------------------------------------------------------------------------ -%% Copyright (c) 2012-2015, Feng Lee -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in all -%% copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%% SOFTWARE. -%%------------------------------------------------------------------------------ -%%% @doc -%%% MQTT Broker Header. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% Banner -%%------------------------------------------------------------------------------ --define(COPYRIGHT, "Copyright (C) 2012-2015, Feng Lee "). - --define(LICENSE_MESSAGE, "Licensed under MIT"). - --define(PROTOCOL_VERSION, "MQTT/3.1.1"). - --define(ERTS_MINIMUM, "6.0"). - -%%------------------------------------------------------------------------------ -%% PubSub -%%------------------------------------------------------------------------------ --type pubsub() :: publish | subscribe. - -%%------------------------------------------------------------------------------ -%% MQTT Topic -%%------------------------------------------------------------------------------ --record(mqtt_topic, { - topic :: binary(), - node :: node() -}). - --type mqtt_topic() :: #mqtt_topic{}. - -%%------------------------------------------------------------------------------ -%% MQTT Subscriber -%%------------------------------------------------------------------------------ --record(mqtt_subscriber, { - topic :: binary(), - qos = 0 :: 0 | 1 | 2, - pid :: pid() -}). - --type mqtt_subscriber() :: #mqtt_subscriber{}. - -%%------------------------------------------------------------------------------ -%% P2P Queue Subscriber -%%------------------------------------------------------------------------------ --record(mqtt_queue, { - name :: binary(), - subpid :: pid(), - qos = 0 :: 0 | 1 | 2 -}). - --type mqtt_queue() :: #mqtt_queue{}. - -%%------------------------------------------------------------------------------ -%% MQTT Client -%%------------------------------------------------------------------------------ --record(mqtt_client, { - clientid :: binary(), - username :: binary() | undefined, - ipaddr :: inet:ip_address() -}). - --type mqtt_client() :: #mqtt_client{}. - -%%------------------------------------------------------------------------------ -%% MQTT Session -%%------------------------------------------------------------------------------ --record(mqtt_session, { - clientid, - session_pid, - subscriptions = [], - awaiting_ack, - awaiting_rel -}). - --type mqtt_session() :: #mqtt_session{}. - -%%------------------------------------------------------------------------------ -%% MQTT Plugin -%%------------------------------------------------------------------------------ --record(mqtt_plugin, { - name, - version, - attrs, - description -}). - --type mqtt_plugin() :: #mqtt_plugin{}. - - diff --git a/plugins/emqttd_plugin_mysql/priv/.placeholder b/plugins/emqttd_plugin_mysql/priv/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_plugin_mysql/rebar b/plugins/emqttd_plugin_mysql/rebar deleted file mode 100755 index 36ef01107949f290211f6358a7293176f8462b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131900 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

YEv)S?Ewt7v{Jk?P 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 -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd demo acl module. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_acl_mysql). - --include("emqttd.hrl"). - --behaviour(emqttd_acl_mod). - -%% ACL callbacks --export([init/1, check_acl/2, reload_acl/1, description/0]). --record(state, {user_table, acl_table, acl_username_field, acl_topic_field, acl_rw_field, user_name_field, user_super_field}). - -init(Opts) -> - Mapper = proplists:get_value(field_mapper, Opts), - State = - #state{ - user_table = proplists:get_value(users_table, Opts, auth_user), - user_super_field = proplists:get_value(is_super, Mapper, is_superuser), - user_name_field = proplists:get_value(username, Mapper, username), - acl_table = proplists:get_value(acls_table, Opts, auth_acl), - acl_username_field = proplists:get_value(acl_username, Mapper, username), - acl_rw_field = proplists:get_value(acl_rw, Mapper, rw), - acl_topic_field = proplists:get_value(acl_topic, Mapper, topic) - }, - {ok, State}. - -check_acl({#mqtt_client{username = Username}, PubSub, Topic}, #state{user_table = UserTab, acl_table = AclTab, user_name_field = UsernameField, user_super_field = SuperField, acl_topic_field = TopicField, acl_username_field = AclUserField, acl_rw_field = AclRwField}) -> - Flag = case PubSub of publish -> 2; subscribe -> 1; pubsub -> 2 end, - Where = {'and', {'>=', AclRwField, Flag}, {TopicField, Topic}}, - Where1 = {'or', {AclUserField, Username}, {AclUserField, "*"}}, - Where2 = {'and', Where, Where1}, - case emysql:select(UserTab, {'and', {UsernameField, Username}, {SuperField, 1}}) of - {ok, []} -> - case emysql:select(UserTab, {UsernameField, Username}) of - {ok, []} -> ignore; - {ok, _} -> case emysql:select(AclTab, Where2) of - {ok, []} -> deny; - {ok, _Record} -> allow - end - end; - {ok, _} -> allow - end. - -reload_acl(_State) -> ok. - -description() -> "ACL Module by Mysql". diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl b/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl deleted file mode 100644 index a913dd587..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl +++ /dev/null @@ -1,110 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd authentication by mysql 'user' table. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_mysql). - --author("Feng Lee "). - --include("emqttd.hrl"). - --behaviour(emqttd_auth_mod). - --export([init/1, check/3, description/0]). - --define(NOT_LOADED, not_loaded(?LINE)). - --record(state, {user_table, name_field, pass_field, pass_hash}). - -init(Opts) -> - Mapper = proplists:get_value(field_mapper, Opts), - {ok, #state{user_table = proplists:get_value(user_table, Opts, auth_user), - name_field = proplists:get_value(username, Mapper), - pass_field = proplists:get_value(password, Mapper), - pass_hash = proplists:get_value(Opts, password_hash)}}. - -check(#mqtt_client{username = undefined}, _Password, _State) -> - {error, "Username undefined"}; -check(_Client, undefined, _State) -> - {error, "Password undefined"}; -check(#mqtt_client{username = Username}, Password, - #state{user_table = UserTab, pass_hash = Type, - name_field = NameField, pass_field = PassField}) -> - Where = {'and', {NameField, Username}, {PassField, hash(Type, Password)}}, - if Type =:= pbkdf2 -> - case emysql:select(UserTab, [PassField], {NameField, Username}) of - {ok, []} -> {error, "User not exist"}; - {ok, Records} -> - if length(Records) =:= 1 -> - case pbkdf2_check(Password, lists:nth(Records, 1)) of - true -> - {ok, []}; - false -> - {error, "UserName or Password is invalid"}; - ErrorInfo -> - {error, ErrorInfo} - end; - true -> - {error, "UserName is ambiguous"} - end - end; - true -> - case emysql:select(UserTab, Where) of - {ok, []} -> {error, "Username or Password "}; - {ok, _Record} -> ok - end - end. - -description() -> "Authentication by MySQL". - -hash(plain, Password) -> - Password; - -hash(md5, Password) -> - hexstring(crypto:hash(md5, Password)); - -hash(sha, Password) -> - hexstring(crypto:hash(sha, Password)). - -hexstring(<>) -> - lists:flatten(io_lib:format("~32.16.0b", [X])); - -hexstring(<>) -> - lists:flatten(io_lib:format("~40.16.0b", [X])). - -not_loaded(Line) -> - erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). - -pbkdf2_check(Password, Pbkstr) -> - case nif_pbkdf2_check(Password, Pbkstr) of - {error, _} = Error -> - throw(Error); - IOData -> - IOData - end. - -nif_pbkdf2_check(Password, Pbkstr) -> - ?NOT_LOADED. - diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src deleted file mode 100644 index 389bd33cb..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_plugin_mysql, - [ - {description, "emqttd MySQL Authentication Plugin"}, - {vsn, "1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {emqttd_plugin_mysql_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl deleted file mode 100644 index 712e75ea6..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl +++ /dev/null @@ -1,80 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd mysql authentication app. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_plugin_mysql_app). --on_load(init/0). --behaviour(application). -%% Application callbacks --export([start/2, prep_stop/1, stop/1, nif_pbkdf2_check/2]). - --behaviour(supervisor). -%% Supervisor callbacks --export([init/1]). --define(NOT_LOADED, not_loaded(?LINE)). - - -%%%============================================================================= -%%% Application callbacks -%%%============================================================================= - -start(_StartType, _StartArgs) -> - Env = application:get_all_env(), - emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), - emqttd_access_control:register_mod(acl, emqttd_acl_mysql, Env), - crypto:start(), - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -prep_stop(State) -> - emqttd_access_control:unregister_mod(auth, emqttd_auth_mysql), State, - emqttd_access_control:unregister_mod(acl, emqttd_acl_mysql), State, - crypto:stop(). - -stop(_State) -> - ok. - -init() -> - PrivDir = case code:priv_dir(?MODULE) of - {error, _} -> - EbinDir = filename:dirname(code:which(?MODULE)), - AppPath = filename:dirname(EbinDir), - filename:join(AppPath, "priv"); - Path -> - Path - end, - erlang:load_nif(filename:join(PrivDir, "emqttd_plugin_mysql_app"), 0). - -%%%============================================================================= -%%% Supervisor callbacks(Dummy) -%%%============================================================================= - -init([]) -> - {ok, {{one_for_one, 5, 10}, []}}. - -not_loaded(Line) -> - erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). - -nif_pbkdf2_check(Password, Hash) -> - ?NOT_LOADED. From f165321d2bb0af6b400ec7a86f7c11d6ec0df6bf Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 09:37:50 +0800 Subject: [PATCH 010/104] rm --- plugins/emqttd_dashboard | 1 - 1 file changed, 1 deletion(-) delete mode 160000 plugins/emqttd_dashboard diff --git a/plugins/emqttd_dashboard b/plugins/emqttd_dashboard deleted file mode 160000 index 300bb7445..000000000 --- a/plugins/emqttd_dashboard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 300bb74452764ac27c261ae000495791397b2a28 From 3ed65b419cdf479f17b7775b7404597f41f04808 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 09:45:37 +0800 Subject: [PATCH 011/104] fix websocket url --- apps/emqttd/priv/www/websocket.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqttd/priv/www/websocket.html b/apps/emqttd/priv/www/websocket.html index 7ac1fda17..eb7784226 100644 --- a/apps/emqttd/priv/www/websocket.html +++ b/apps/emqttd/priv/www/websocket.html @@ -31,7 +31,7 @@ return document.getElementById(id); } function go() { - ws = new WebSocket("ws://" + location.host + "/mqtt/wsocket"); + ws = new WebSocket("ws://" + location.host + "/mqtt"); ws.onopen = function () { $('connstate').innerHTML = 'CONNECTED'; } From f84a99f05f711b304326f6c7a316573096c3ce09 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 09:47:02 +0800 Subject: [PATCH 012/104] 0.8.1 --- CHANGELOG.md | 2 ++ tests/org.eclipse.paho.mqtt.testing | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700532931..5c1610db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ emqttd ChangeLog Bugfix: issue #138 - when client disconnected normally, broker will not publish disconnected $SYS message +Bugfix: fix websocket url in emqttd/priv/www/websocket.html + Improve: issue #136 - $SYS topics result should not include $SYS messages diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index 17afdf7fb..bdc690db8 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 +Subproject commit bdc690db847cec1c682e604dca571c72ff756305 From 54245b61ebe6c9e71c4c49ca9703d6af56bae58e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 18:49:54 +0800 Subject: [PATCH 013/104] tests --- tests/org.eclipse.paho.mqtt.testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index bdc690db8..17afdf7fb 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit bdc690db847cec1c682e604dca571c72ff756305 +Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 From d3616a27015c4f414e63fe3dedc9ce96fa2b223a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 28 May 2015 21:55:20 +0800 Subject: [PATCH 014/104] client presence management --- apps/emqttd/include/emqttd.hrl | 7 +- apps/emqttd/src/emqttd_access_rule.erl | 4 +- apps/emqttd/src/emqttd_auth_clientid.erl | 8 +- apps/emqttd/src/emqttd_client.erl | 32 ++---- apps/emqttd/src/emqttd_cm.erl | 6 +- apps/emqttd/src/emqttd_mod_autosub.erl | 22 +++- apps/emqttd/src/emqttd_mod_presence.erl | 44 ++++++-- apps/emqttd/src/emqttd_protocol.erl | 135 ++++++++++++----------- apps/emqttd/src/emqttd_util.erl | 10 +- apps/emqttd/src/emqttd_vm.erl | 11 ++ 10 files changed, 162 insertions(+), 117 deletions(-) diff --git a/apps/emqttd/include/emqttd.hrl b/apps/emqttd/include/emqttd.hrl index 9c2ab934a..182959df6 100644 --- a/apps/emqttd/include/emqttd.hrl +++ b/apps/emqttd/include/emqttd.hrl @@ -77,9 +77,12 @@ %% MQTT Client %%------------------------------------------------------------------------------ -record(mqtt_client, { - clientid :: binary(), + clientid :: binary() | undefined, username :: binary() | undefined, - ipaddr :: inet:ip_address() + ipaddress :: inet:ip_address(), + clean_sess :: boolean(), + proto_ver :: 3 | 4, + client_pid :: pid() }). -type mqtt_client() :: #mqtt_client{}. diff --git a/apps/emqttd/src/emqttd_access_rule.erl b/apps/emqttd/src/emqttd_access_rule.erl index 8f7761822..a492136cf 100644 --- a/apps/emqttd/src/emqttd_access_rule.erl +++ b/apps/emqttd/src/emqttd_access_rule.erl @@ -114,9 +114,9 @@ match_who(#mqtt_client{clientid = ClientId}, {client, ClientId}) -> true; match_who(#mqtt_client{username = Username}, {user, Username}) -> true; -match_who(#mqtt_client{ipaddr = undefined}, {ipaddr, _Tup}) -> +match_who(#mqtt_client{ipaddress = undefined}, {ipaddr, _Tup}) -> false; -match_who(#mqtt_client{ipaddr = IP}, {ipaddr, {_CDIR, Start, End}}) -> +match_who(#mqtt_client{ipaddress = IP}, {ipaddr, {_CDIR, Start, End}}) -> I = esockd_access:atoi(IP), I >= Start andalso I =< End; match_who(_Client, _Who) -> diff --git a/apps/emqttd/src/emqttd_auth_clientid.erl b/apps/emqttd/src/emqttd_auth_clientid.erl index 2f3d51617..e5171a239 100644 --- a/apps/emqttd/src/emqttd_auth_clientid.erl +++ b/apps/emqttd/src/emqttd_auth_clientid.erl @@ -101,10 +101,10 @@ init(Opts) -> check(#mqtt_client{clientid = undefined}, _Password, []) -> {error, "ClientId undefined"}; -check(#mqtt_client{clientid = ClientId, ipaddr = IpAddr}, _Password, []) -> - check_clientid_only(ClientId, IpAddr); -check(#mqtt_client{clientid = ClientId, ipaddr = IpAddr}, _Password, [{password, no}|_]) -> - check_clientid_only(ClientId, IpAddr); +check(#mqtt_client{clientid = ClientId, ipaddress = IpAddress}, _Password, []) -> + check_clientid_only(ClientId, IpAddress); +check(#mqtt_client{clientid = ClientId, ipaddress = IpAddress}, _Password, [{password, no}|_]) -> + check_clientid_only(ClientId, IpAddress); check(_Client, undefined, [{password, yes}|_]) -> {error, "Password undefined"}; check(#mqtt_client{clientid = ClientId}, Password, [{password, yes}|_]) -> diff --git a/apps/emqttd/src/emqttd_client.erl b/apps/emqttd/src/emqttd_client.erl index 0fd1db69a..3d8e170b4 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/apps/emqttd/src/emqttd_client.erl @@ -87,11 +87,13 @@ handle_call(info, _From, State = #state{conn_name=ConnName, proto_state = ProtoState}) -> {reply, [{conn_name, ConnName} | emqttd_protocol:info(ProtoState)], State}; -handle_call(Req, _From, State) -> - {stop, {badreq, Req}, State}. +handle_call(Req, _From, State = #state{peername = Peername}) -> + lager:critical("Client ~s: unexpected request - ~p",[emqttd_net:format(Peername), Req]), + {reply, {error, unsupported_request}, State}. -handle_cast(Msg, State) -> - {stop, {badmsg, Msg}, State}. +handle_cast(Msg, State = #state{peername = Peername}) -> + lager:critical("Client ~s: unexpected msg - ~p",[emqttd_net:format(Peername), Msg]), + {noreply, State}. handle_info(timeout, State) -> stop({shutdown, timeout}, State); @@ -102,7 +104,7 @@ handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState %% need transfer data??? %% emqttd_client:transfer(NewPid, Data), lager:error("Shutdown for duplicate clientid: ~s, conn:~s", - [emqttd_protocol:clientid(ProtoState), ConnName]), + [emqttd_protocol:clientid(ProtoState), ConnName]), stop({shutdown, duplicate_id}, State); %%TODO: ok?? @@ -158,17 +160,16 @@ handle_info({keepalive, timeout}, State = #state{peername = Peername, keepalive handle_info(Info, State = #state{peername = Peername}) -> lager:critical("Client ~s: unexpected info ~p",[emqttd_net:format(Peername), Info]), - {stop, {badinfo, Info}, State}. + {noreply, State}. terminate(Reason, #state{peername = Peername, keepalive = KeepAlive, proto_state = ProtoState}) -> - lager:info("Client ~s: ~p terminated, reason: ~p~n", [emqttd_net:format(Peername), self(), Reason]), - notify(disconnected, Reason, ProtoState), + lager:info("Client ~s terminated, reason: ~p", [emqttd_net:format(Peername), Reason]), emqttd_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of {undefined, _} -> ok; {_, {shutdown, Error}} -> emqttd_protocol:shutdown(Error, ProtoState); - {_, Reason} -> + {_, Reason} -> emqttd_protocol:shutdown(Reason, ProtoState) end. @@ -231,7 +232,7 @@ control_throttle(State = #state{conn_state = Flow, {_, _} -> run_socket(State) end. -stop(Reason, State ) -> +stop(Reason, State) -> {stop, Reason, State}. received_stats(?PACKET(Type)) -> @@ -253,14 +254,3 @@ inc(?DISCONNECT) -> inc(_) -> ignore. -%%TODO: should be moved to emqttd_protocol... for event emitted when protocol shutdown... -notify(disconnected, _Reason, undefined) -> ingore; - -notify(disconnected, {shutdown, Reason}, ProtoState) -> - %emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}); - ok; - -notify(disconnected, Reason, ProtoState) -> - %emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}). - ok. - diff --git a/apps/emqttd/src/emqttd_cm.erl b/apps/emqttd/src/emqttd_cm.erl index 7758f8ec0..1c109536e 100644 --- a/apps/emqttd/src/emqttd_cm.erl +++ b/apps/emqttd/src/emqttd_cm.erl @@ -28,6 +28,8 @@ -author("Feng Lee "). +-include("emqttd.hrl"). + -behaviour(gen_server). -define(SERVER, ?MODULE). @@ -69,10 +71,10 @@ table() -> ?CLIENT_TAB. %% @doc Lookup client pid with clientId %% @end %%------------------------------------------------------------------------------ --spec lookup(ClientId :: binary()) -> pid() | undefined. +-spec lookup(ClientId :: binary()) -> mqtt_client() | undefined. lookup(ClientId) when is_binary(ClientId) -> case ets:lookup(?CLIENT_TAB, ClientId) of - [{_, Pid, _}] -> Pid; + [Client] -> Client; [] -> undefined end. diff --git a/apps/emqttd/src/emqttd_mod_autosub.erl b/apps/emqttd/src/emqttd_mod_autosub.erl index 0ed1be5e1..a28db1642 100644 --- a/apps/emqttd/src/emqttd_mod_autosub.erl +++ b/apps/emqttd/src/emqttd_mod_autosub.erl @@ -29,22 +29,32 @@ -author("Feng Lee "). +-include_lib("emqtt/include/emqtt.hrl"). + +-include_lib("emqtt/include/emqtt_packet.hrl"). + +-include("emqttd.hrl"). + -behaviour(emqttd_gen_mod). --export([load/1, subscribe/2, unload/1]). +-export([load/1, client_connected/3, unload/1]). -record(state, {topics}). load(Opts) -> Topics = [{list_to_binary(Topic), Qos} || {Topic, Qos} <- Opts, 0 =< Qos, Qos =< 2], - emqttd_broker:hook(client_connected, {?MODULE, subscribe}, - {?MODULE, subscribe, [Topics]}), + emqttd_broker:hook(client_connected, {?MODULE, client_connected}, + {?MODULE, client_connected, [Topics]}), {ok, #state{topics = Topics}}. -subscribe({Client, ClientId}, Topics) -> +client_connected(?CONNACK_ACCEPT, #mqtt_client{clientid = ClientId, client_pid = ClientPid}, Topics) -> F = fun(Topic) -> emqtt_topic:feed_var(<<"$c">>, ClientId, Topic) end, - [Client ! {subscribe, F(Topic), Qos} || {Topic, Qos} <- Topics]. + [ClientPid ! {subscribe, F(Topic), Qos} || {Topic, Qos} <- Topics]; + +client_connected(_ConnAck, _Client, _Topics) -> + ignore. unload(_Opts) -> - emqttd_broker:unhook(client_connected, {?MODULE, subscribe}). + emqttd_broker:unhook(client_connected, {?MODULE, client_connected}). + diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/apps/emqttd/src/emqttd_mod_presence.erl index ca66177e7..6432e0189 100644 --- a/apps/emqttd/src/emqttd_mod_presence.erl +++ b/apps/emqttd/src/emqttd_mod_presence.erl @@ -28,26 +28,52 @@ -include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). + -export([load/1, unload/1]). --export([client_connected/2, client_disconnected/2]). +-export([client_connected/3, client_disconnected/3]). load(Opts) -> emqttd_broker:hook(client_connected, {?MODULE, client_connected}, {?MODULE, client_connected, [Opts]}), emqttd_broker:hook(client_disconnected, {?MODULE, client_disconnected}, {?MODULE, client_disconnected, [Opts]}), {ok, Opts}. -client_connected({Client, ClientId}, _Opts) -> - Topic = emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])), - Payload = iolist_to_binary(mochijson2:encode([{ts, emqttd_util:timestamp()}])), - emqttd_pubsub:publish(presence, #mqtt_message{topic = Topic, payload = Payload}). +client_connected(ConnAck, #mqtt_client{clientid = ClientId, + username = Username, + ipaddress = IpAddress, + clean_sess = CleanSess, + proto_ver = ProtoVer}, _Opts) -> + Sess = case CleanSess of + true -> false; + false -> true + end, + Json = mochijson2:encode([{username, Username}, + {ipaddress, emqttd_net:ntoa(IpAddress)}, + {session, Sess}, + {protocol, ProtoVer}, + {connack, ConnAck}, + {ts, emqttd_vm:timestamp()}]), + Message = #mqtt_message{topic = topic(connected, ClientId), + payload = iolist_to_binary(Json)}, + emqttd_pubsub:publish(presence, Message). -client_disconnected({ClientId, Reason}, _Opts) -> - Topic = emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])), - Payload = iolist_to_binary(mochijson2:encode([{reason, Reason}, {ts, emqttd_util:timestamp()}])), - emqttd_pubsub:publish(presence, #mqtt_message{topic = Topic, payload = Payload}). +client_disconnected(Reason, ClientId, _Opts) -> + Json = mochijson2:encode([{reason, reason(Reason)}, {ts, emqttd_vm:timestamp()}]), + emqttd_pubsub:publish(presence, #mqtt_message{topic = topic(disconnected, ClientId), + payload = iolist_to_binary(Json)}). unload(_Opts) -> emqttd_broker:unhook(client_connected, {?MODULE, client_connected}), emqttd_broker:unhook(client_disconnected, {?MODULE, client_disconnected}). + +topic(connected, ClientId) -> + emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); +topic(disconnected, ClientId) -> + emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). + +reason(Reason) when is_atom(Reason) -> Reason; +reason({Error, _}) when is_atom(Error) -> Error; +reason(_) -> internal_error. + diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 30013ac3e..4afa85fc3 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -29,19 +29,18 @@ -author("Feng Lee "). -include_lib("emqtt/include/emqtt.hrl"). + -include_lib("emqtt/include/emqtt_packet.hrl"). -include("emqttd.hrl"). %% API --export([init/3, clientid/1]). +-export([init/3, info/1, clientid/1, client/1]). -export([received/2, send/2, redeliver/2, shutdown/2]). -export([handle/2]). --export([info/1]). - %% Protocol State -record(proto_state, { peername, @@ -49,30 +48,29 @@ connected = false, %received CONNECT action? proto_ver, proto_name, - %packet_id, username, clientid, clean_sess, - session, %% session state or session pid + session, %% session state or session pid will_msg, - max_clientid_len = ?MAX_CLIENTID_LEN + max_clientid_len = ?MAX_CLIENTID_LEN, + client_pid }). -type proto_state() :: #proto_state{}. +%%------------------------------------------------------------------------------ +%% @doc Init protocol +%% @end +%%------------------------------------------------------------------------------ init(Peername, SendFun, Opts) -> MaxLen = proplists:get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), #proto_state{ peername = Peername, sendfun = SendFun, - max_clientid_len = MaxLen}. + max_clientid_len = MaxLen, + client_pid = self()}. -clientid(#proto_state{clientid = ClientId}) -> ClientId. - -client(#proto_state{peername = {Addr, _Port}, clientid = ClientId, username = Username}) -> - #mqtt_client{clientid = ClientId, username = Username, ipaddr = Addr}. - -%%SHOULD be registered in emqttd_cm info(#proto_state{proto_ver = ProtoVer, proto_name = ProtoName, clientid = ClientId, @@ -80,11 +78,27 @@ info(#proto_state{proto_ver = ProtoVer, will_msg = WillMsg}) -> [{proto_ver, ProtoVer}, {proto_name, ProtoName}, - {clientid, ClientId}, + {clientid, ClientId}, {clean_sess, CleanSess}, {will_msg, WillMsg}]. -%%CONNECT – Client requests a connection to a Server +clientid(#proto_state{clientid = ClientId}) -> + ClientId. + +client(#proto_state{peername = {Addr, _Port}, + clientid = ClientId, + username = Username, + clean_sess = CleanSess, + proto_ver = ProtoVer, + client_pid = Pid}) -> + #mqtt_client{clientid = ClientId, + username = Username, + ipaddress = Addr, + clean_sess = CleanSess, + proto_ver = ProtoVer, + client_pid = Pid}. + +%% CONNECT – Client requests a connection to a Server %%A Client can only send the CONNECT Packet once over a Network Connection. -spec received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, any()}. @@ -107,42 +121,45 @@ received(Packet = ?PACKET(_Type), State) -> {error, Reason, State} end. -handle(Packet = ?CONNECT_PACKET(Var), State = #proto_state{peername = Peername = {Addr, _}}) -> +handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername = {Addr, _}}) -> #mqtt_packet_connect{proto_ver = ProtoVer, + proto_name = ProtoName, username = Username, password = Password, clean_sess = CleanSess, keep_alive = KeepAlive, - clientid = ClientId} = Var, + clientid = ClientId} = Var, - trace(recv, Packet, State#proto_state{clientid = ClientId}), %%TODO: fix later... + State1 = State0#proto_state{proto_ver = ProtoVer, + proto_name = ProtoName, + username = Username, + clientid = ClientId, + clean_sess = CleanSess}, - State1 = State#proto_state{proto_ver = ProtoVer, - username = Username, - clientid = ClientId, - clean_sess = CleanSess}, - {ReturnCode1, State2} = - case validate_connect(Var, State) of + trace(recv, Packet, State1), + + {ReturnCode1, State3} = + case validate_connect(Var, State1) of ?CONNACK_ACCEPT -> - Client = #mqtt_client{clientid = ClientId, username = Username, ipaddr = Addr}, - case emqttd_access_control:auth(Client, Password) of + case emqttd_access_control:auth(client(State1), Password) of ok -> - %% Generate one if null - ClientId1 = clientid(ClientId, State), - %% Register clientId - emqttd_cm:register(ClientId1), + %% Generate clientId if null + State2 = State1#proto_state{clientid = clientid(ClientId, State1)}, + + %% Register the client to cm + emqttd_cm:register(client(State2)), + %%Starting session - {ok, Session} = emqttd_session:start({CleanSess, ClientId1, self()}), + {ok, Session} = emqttd_session:start({CleanSess, clientid(State2), self()}), + %% Start keepalive start_keepalive(KeepAlive), - %% Run hooks - emqttd_broker:foreach_hooks(client_connected, [{self(), ClientId1}]), - {?CONNACK_ACCEPT, State1#proto_state{clientid = ClientId1, - session = Session, - will_msg = willmsg(Var)}}; + + %% ACCEPT + {?CONNACK_ACCEPT, State2#proto_state{session = Session, will_msg = willmsg(Var)}}; {error, Reason}-> - lager:error("~s@~s: username '~s' login failed - ~s", + lager:error("~s@~s: username '~s', login failed - ~s", [ClientId, emqttd_net:format(Peername), Username, Reason]), {?CONNACK_CREDENTIALS, State1} @@ -150,9 +167,10 @@ handle(Packet = ?CONNECT_PACKET(Var), State = #proto_state{peername = Peername = ReturnCode -> {ReturnCode, State1} end, - %%TODO: this is not right... - notify(connected, ReturnCode1, State2), - send(?CONNACK_PACKET(ReturnCode1), State2); + %% Run hooks + emqttd_broker:foreach_hooks(client_connected, [ReturnCode1, client(State3)]), + %% Send connack + send(?CONNACK_PACKET(ReturnCode1), State3); handle(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), State = #proto_state{clientid = ClientId, session = Session}) -> @@ -251,7 +269,6 @@ send({_From, Message = #mqtt_message{qos = ?QOS_0}}, State) -> %% message from session send({_From = SessPid, Message}, State = #proto_state{session = SessPid}) when is_pid(SessPid) -> send(emqtt_message:to_packet(Message), State); - %% message(qos1, qos2) not from session send({_From, Message = #mqtt_message{qos = Qos}}, State = #proto_state{session = Session}) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> @@ -279,12 +296,21 @@ trace(send, Packet, #proto_state{peername = Peername, clientid = ClientId}) -> redeliver({?PUBREL, PacketId}, State) -> send(?PUBREL_PACKET(PacketId), State). +shutdown(duplicate_id, _State) -> + quiet; %% + +shutdown(normal, #proto_state{peername = Peername, clientid = ClientId}) -> + lager:info([{client, ClientId}], "Client ~s@~s: normal shutdown", + [ClientId, emqttd_net:format(Peername)]), + try_unregister(ClientId), + emqttd_broker:foreach_hooks(client_disconnected, [normal, ClientId]); + shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> - send_willmsg(ClientId, WillMsg), - try_unregister(ClientId, self()), - lager:info([{client, ClientId}], "Protocol ~s@~s Shutdown: ~p", + lager:info([{client, ClientId}], "Protocol ~s@~s: Shutdown for ~p", [ClientId, emqttd_net:format(Peername), Error]), - ok. + send_willmsg(ClientId, WillMsg), + try_unregister(ClientId), + emqttd_broker:foreach_hooks(client_disconnected, [Error, ClientId]). willmsg(Packet) when is_record(Packet, mqtt_packet_connect) -> emqtt_message:from_packet(Packet). @@ -377,8 +403,8 @@ validate_qos(undefined) -> true; validate_qos(Qos) when Qos =< ?QOS_2 -> true; validate_qos(_) -> false. -try_unregister(undefined, _) -> ok; -try_unregister(ClientId, _) -> emqttd_cm:unregister(ClientId). +try_unregister(undefined) -> ok; +try_unregister(ClientId) -> emqttd_cm:unregister(ClientId). %% publish ACL is cached in process dictionary. check_acl(publish, Topic, State) -> @@ -411,18 +437,3 @@ inc(?PINGRESP) -> inc(_) -> ingore. -notify(connected, ReturnCode, #proto_state{peername = Peername, - proto_ver = ProtoVer, - clientid = ClientId, - clean_sess = CleanSess}) -> - Sess = case CleanSess of - true -> false; - false -> true - end, - Params = [{from, emqttd_net:format(Peername)}, - {protocol, ProtoVer}, - {session, Sess}, - {connack, ReturnCode}]. - %emqttd_event:notify({connected, ClientId, Params}). - - diff --git a/apps/emqttd/src/emqttd_util.erl b/apps/emqttd/src/emqttd_util.erl index eca61b4b0..7ec6459e3 100644 --- a/apps/emqttd/src/emqttd_util.erl +++ b/apps/emqttd/src/emqttd_util.erl @@ -30,8 +30,7 @@ -export([apply_module_attributes/1, all_module_attributes/1, - cancel_timer/1, - timestamp/0, microsecs/0]). + cancel_timer/1]). -export([integer_to_binary/1]). @@ -91,11 +90,4 @@ cancel_timer(Ref) -> integer_to_binary(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). -timestamp() -> - {MegaSecs, Secs, _MicroSecs} = os:timestamp(), - MegaSecs * 1000000 + Secs. - -microsecs() -> - {Mega, Sec, Micro} = erlang:now(), - (Mega * 1000000 + Sec) * 1000000 + Micro. diff --git a/apps/emqttd/src/emqttd_vm.erl b/apps/emqttd/src/emqttd_vm.erl index 1e4624a03..217cf0092 100644 --- a/apps/emqttd/src/emqttd_vm.erl +++ b/apps/emqttd/src/emqttd_vm.erl @@ -24,12 +24,23 @@ %%% %%% @end %%%----------------------------------------------------------------------------- + -module(emqttd_vm). -author("Feng Lee "). +-export([timestamp/0, microsecs/0]). + -export([loads/0]). +timestamp() -> + {MegaSecs, Secs, _MicroSecs} = os:timestamp(), + MegaSecs * 1000000 + Secs. + +microsecs() -> + {Mega, Sec, Micro} = erlang:now(), + (Mega * 1000000 + Sec) * 1000000 + Micro. + loads() -> [{load1, ftos(cpu_sup:avg1()/256)}, {load5, ftos(cpu_sup:avg5()/256)}, From 230a348f51c7bdf3e79670846540b5bf1d4527a7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 00:53:47 +0800 Subject: [PATCH 015/104] 0.9.0 --- apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- rel/reltool.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index ea5191f92..656a62d5b 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.0"}, + {vsn, "0.9.0"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index f598c13ba..4decbcf7a 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.0"}, + {vsn, "0.9.0"}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/rel/reltool.config b/rel/reltool.config index 268f19f83..9deb03833 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.8.0", + {rel, "emqttd", "0.9.0", [ kernel, stdlib, From 5a2dfd2a10c4b476cfb4956393621aa048c690a1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 09:54:51 +0800 Subject: [PATCH 016/104] presence and misc fix --- apps/emqttd/src/emqttd_client.erl | 1 - apps/emqttd/src/emqttd_mod_presence.erl | 12 +++++++----- apps/emqttd/src/emqttd_net.erl | 2 +- apps/emqttd/src/emqttd_protocol.erl | 5 ++++- apps/emqttd/src/emqttd_session.erl | 2 +- rel/files/emqttd.config | 2 +- tests/org.eclipse.paho.mqtt.testing | 2 +- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/emqttd/src/emqttd_client.erl b/apps/emqttd/src/emqttd_client.erl index 3d8e170b4..de5d65d45 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/apps/emqttd/src/emqttd_client.erl @@ -100,7 +100,6 @@ handle_info(timeout, State) -> handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState, conn_name=ConnName}) -> - %% TODO: to... %% need transfer data??? %% emqttd_client:transfer(NewPid, Data), lager:error("Shutdown for duplicate clientid: ~s, conn:~s", diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/apps/emqttd/src/emqttd_mod_presence.erl index 6432e0189..981cb4350 100644 --- a/apps/emqttd/src/emqttd_mod_presence.erl +++ b/apps/emqttd/src/emqttd_mod_presence.erl @@ -43,24 +43,26 @@ client_connected(ConnAck, #mqtt_client{clientid = ClientId, username = Username, ipaddress = IpAddress, clean_sess = CleanSess, - proto_ver = ProtoVer}, _Opts) -> + proto_ver = ProtoVer}, Opts) -> Sess = case CleanSess of true -> false; false -> true end, Json = mochijson2:encode([{username, Username}, - {ipaddress, emqttd_net:ntoa(IpAddress)}, + {ipaddress, list_to_binary(emqttd_net:ntoa(IpAddress))}, {session, Sess}, {protocol, ProtoVer}, {connack, ConnAck}, {ts, emqttd_vm:timestamp()}]), - Message = #mqtt_message{topic = topic(connected, ClientId), + Message = #mqtt_message{qos = proplists:get_value(qos, Opts, 0), + topic = topic(connected, ClientId), payload = iolist_to_binary(Json)}, emqttd_pubsub:publish(presence, Message). -client_disconnected(Reason, ClientId, _Opts) -> +client_disconnected(Reason, ClientId, Opts) -> Json = mochijson2:encode([{reason, reason(Reason)}, {ts, emqttd_vm:timestamp()}]), - emqttd_pubsub:publish(presence, #mqtt_message{topic = topic(disconnected, ClientId), + emqttd_pubsub:publish(presence, #mqtt_message{qos = proplists:get_value(qos, Opts, 0), + topic = topic(disconnected, ClientId), payload = iolist_to_binary(Json)}). unload(_Opts) -> diff --git a/apps/emqttd/src/emqttd_net.erl b/apps/emqttd/src/emqttd_net.erl index 0b0517f6b..8488fe76e 100644 --- a/apps/emqttd/src/emqttd_net.erl +++ b/apps/emqttd/src/emqttd_net.erl @@ -32,7 +32,7 @@ -export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2, port_to_listeners/1]). --export([peername/1, sockname/1, format/2, format/1, connection_string/2]). +-export([peername/1, sockname/1, format/2, format/1, connection_string/2, ntoa/1]). -define(FIRST_TEST_BIND_PORT, 10000). diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 4afa85fc3..d519a182e 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -121,7 +121,7 @@ received(Packet = ?PACKET(_Type), State) -> {error, Reason, State} end. -handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername = {Addr, _}}) -> +handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername}) -> #mqtt_packet_connect{proto_ver = ProtoVer, proto_name = ProtoName, @@ -299,6 +299,9 @@ redeliver({?PUBREL, PacketId}, State) -> shutdown(duplicate_id, _State) -> quiet; %% +shutdown(_, #proto_state{clientid = undefined}) -> + ignore; + shutdown(normal, #proto_state{peername = Peername, clientid = ClientId}) -> lager:info([{client, ClientId}], "Client ~s@~s: normal shutdown", [ClientId, emqttd_net:format(Peername)]), diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 454363f11..15a465d4d 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -360,7 +360,7 @@ handle_info({dispatch, {_From, Message}}, State) -> handle_info({'EXIT', ClientPid, Reason}, State = #session_state{clientid = ClientId, client_pid = ClientPid}) -> - lager:error("Session: client ~s@~p exited, caused by ~p", [ClientId, ClientPid, Reason]), + lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), {noreply, start_expire_timer(State#session_state{client_pid = undefined})}; handle_info({'EXIT', ClientPid0, _Reason}, State = #session_state{client_pid = ClientPid}) -> diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 97cdc9a18..628c53bc1 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -124,7 +124,7 @@ {modules, [ %% Client presence management module. %% Publish messages when client connected or disconnected - {presence, []}, + {presence, [{qos, 0}]}, %% Subscribe topics automatically when client connected {autosub, [{"$Q/client/$c", 0}]} diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index 17afdf7fb..bdc690db8 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 +Subproject commit bdc690db847cec1c682e604dca571c72ff756305 From b18b35ff7cb1d48abc9eb479d6e3a95fb61ce677 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 15:50:36 +0800 Subject: [PATCH 017/104] rm emqttd_amqp --- plugins/emqttd_amqp/src/emqttd_amqp.app.src | 12 --------- plugins/emqttd_amqp/src/emqttd_amqp_app.erl | 16 ------------ plugins/emqttd_amqp/src/emqttd_amqp_sup.erl | 27 --------------------- tests/org.eclipse.paho.mqtt.testing | 2 +- 4 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 plugins/emqttd_amqp/src/emqttd_amqp.app.src delete mode 100644 plugins/emqttd_amqp/src/emqttd_amqp_app.erl delete mode 100644 plugins/emqttd_amqp/src/emqttd_amqp_sup.erl diff --git a/plugins/emqttd_amqp/src/emqttd_amqp.app.src b/plugins/emqttd_amqp/src/emqttd_amqp.app.src deleted file mode 100644 index 1664bee18..000000000 --- a/plugins/emqttd_amqp/src/emqttd_amqp.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_amqp, - [ - {description, ""}, - {vsn, "1"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, { emqttd_amqp_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_amqp/src/emqttd_amqp_app.erl b/plugins/emqttd_amqp/src/emqttd_amqp_app.erl deleted file mode 100644 index 0087e7a7d..000000000 --- a/plugins/emqttd_amqp/src/emqttd_amqp_app.erl +++ /dev/null @@ -1,16 +0,0 @@ --module(emqttd_amqp_app). - --behaviour(application). - -%% Application callbacks --export([start/2, stop/1]). - -%% =================================================================== -%% Application callbacks -%% =================================================================== - -start(_StartType, _StartArgs) -> - emqttd_amqp_sup:start_link(). - -stop(_State) -> - ok. diff --git a/plugins/emqttd_amqp/src/emqttd_amqp_sup.erl b/plugins/emqttd_amqp/src/emqttd_amqp_sup.erl deleted file mode 100644 index 79ef00dff..000000000 --- a/plugins/emqttd_amqp/src/emqttd_amqp_sup.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(emqttd_amqp_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). - -%% =================================================================== -%% API functions -%% =================================================================== - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% =================================================================== -%% Supervisor callbacks -%% =================================================================== - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index bdc690db8..17afdf7fb 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit bdc690db847cec1c682e604dca571c72ff756305 +Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 From f25ceb1b2b455de421a1fad3135d80b4ddfd8f3f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 15:53:47 +0800 Subject: [PATCH 018/104] ignore examples --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2dcf3c065..15f67b8e0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ plugins/*/ebin log/ *.swp *.so +examples From d1ae4436500b9d2b9509a17f2272121ed1c2eb0e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 16:05:17 +0800 Subject: [PATCH 019/104] 0.8.1 --- apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- rel/reltool.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index 656a62d5b..488ebbb8e 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.9.0"}, + {vsn, "0.8.1"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index 4decbcf7a..b7459e519 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.9.0"}, + {vsn, "0.8.1"}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/rel/reltool.config b/rel/reltool.config index 9deb03833..ace833401 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.9.0", + {rel, "emqttd", "0.8.1", [ kernel, stdlib, From 37a89c3a443885354d88d494e4053eeb59904aca Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 16:07:43 +0800 Subject: [PATCH 020/104] allow all for websocket --- rel/files/emqttd.config | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 628c53bc1..39e92334e 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -174,10 +174,7 @@ %% Maximum number of concurrent clients {max_clients, 512}, %% Socket Access Control - {access, [ - {allow, "127.0.0.1"}, - {deny, all} - ]}, + {access, [{allow, all}]}, %% Socket Options {sockopts, [ {backlog, 1024} From a5e594e1e3c5ed0eeecc34acaa7e3df17e00fcf1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 16:32:48 +0800 Subject: [PATCH 021/104] 0.8.1 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1610db3..c32652458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,15 @@ emqttd ChangeLog 0.8.1-alpha (2015-05-28) ------------------------- +Client [Presence](https://github.com/emqtt/emqttd/wiki/Presence) Support and [$SYS Topics](https://github.com/emqtt/emqttd/wiki/$SYS-Topics) Redesigned! + Bugfix: issue #138 - when client disconnected normally, broker will not publish disconnected $SYS message Bugfix: fix websocket url in emqttd/priv/www/websocket.html -Improve: issue #136 - $SYS topics result should not include $SYS messages +Improve: etc/emqttd.config to allow websocket connections from any hosts + +Improve: rel/reltool.config to exclude unnecessary apps. 0.8.0-alpha (2015-05-25) From dabddbb6d4ff6c4339bcdfb26e672f7bd8b03059 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 29 May 2015 16:36:39 +0800 Subject: [PATCH 022/104] up --- tests/org.eclipse.paho.mqtt.testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index 17afdf7fb..bdc690db8 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 +Subproject commit bdc690db847cec1c682e604dca571c72ff756305 From d6f65cebc678836a936d9f427724cd80284878f8 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 30 May 2015 12:43:30 +0800 Subject: [PATCH 023/104] tests --- tests/org.eclipse.paho.mqtt.testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing index 17afdf7fb..bdc690db8 160000 --- a/tests/org.eclipse.paho.mqtt.testing +++ b/tests/org.eclipse.paho.mqtt.testing @@ -1 +1 @@ -Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84 +Subproject commit bdc690db847cec1c682e604dca571c72ff756305 From 73cb2389d93997397592a3ecf6504e0790bc18fe Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 31 May 2015 11:38:26 +0800 Subject: [PATCH 024/104] add 'sys' flag for mqtt_message --- apps/emqtt/include/emqtt.hrl | 2 ++ apps/emqttd/src/emqttd_cluster.erl | 40 ------------------------- apps/emqttd/src/emqttd_mod_presence.erl | 2 ++ rel/files/emqttd.config | 28 ++++++++++++----- 4 files changed, 24 insertions(+), 48 deletions(-) delete mode 100644 apps/emqttd/src/emqttd_cluster.erl diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl index 1184aab01..bbd1f95c4 100644 --- a/apps/emqtt/include/emqtt.hrl +++ b/apps/emqtt/include/emqtt.hrl @@ -61,6 +61,8 @@ qos = ?QOS_0 :: mqtt_qos(), retain = false :: boolean(), dup = false :: boolean(), + %% $SYS message flag + sys = false :: boolean(), msgid :: mqtt_msgid(), payload :: binary() }). diff --git a/apps/emqttd/src/emqttd_cluster.erl b/apps/emqttd/src/emqttd_cluster.erl deleted file mode 100644 index bd5a1a2ac..000000000 --- a/apps/emqttd/src/emqttd_cluster.erl +++ /dev/null @@ -1,40 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd cluster to monitor clusted nodes. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_cluster). - --author("Feng Lee "). - --export([running_nodes/0]). - -%%------------------------------------------------------------------------------ -%% @doc Get running nodes -%% @end -%%------------------------------------------------------------------------------ -%%TODO: remove... -running_nodes() -> - mnesia:system_info(running_db_nodes). - diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/apps/emqttd/src/emqttd_mod_presence.erl index 981cb4350..3e5dbccb2 100644 --- a/apps/emqttd/src/emqttd_mod_presence.erl +++ b/apps/emqttd/src/emqttd_mod_presence.erl @@ -26,6 +26,8 @@ %%%----------------------------------------------------------------------------- -module(emqttd_mod_presence). +-author("Feng Lee "). + -include_lib("emqtt/include/emqtt.hrl"). -include("emqttd.hrl"). diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 39e92334e..977101cce 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -1,4 +1,4 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +h% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et [{kernel, [{start_timer, true}, @@ -87,13 +87,25 @@ ]}, %% Session {session, [ - %% Expired after 24 hours - {expires, 24}, - %% Max offline message queue - {max_queue, 100}, - %% Store Qos0? - {store_qos0, false} - ]} + %% Expired after 2 days + {expired_after, 48}, + %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. + {max_inflight_messages, 20}, + %% Max retries for unacknolege Qos1/2 messages + {max_unack_retries, 3}, + %% Retry after 10 seconds + {unack_retry_after, 10} + ]}, + {queue, [ + %% Max messages queued when client is disconnected, or inflight messsages is overload + {max_queued_messages, 100}, + %% High watermark of queued messsages + {high_queue_watermark, 0.6}, + %% Low watermark of queued messsages + {low_queue_watermark, 0.2}, + %% Queue Qos0 offline messages? + {queue_qos0_messages, false} + ]}, ]}, %% Broker Options {broker, [ From 253717d5a111109d6848488a1d4bfcdd90f7c800 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 31 May 2015 11:40:07 +0800 Subject: [PATCH 025/104] ordered_set --- apps/emqttd/src/emqttd_cm_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_cm_sup.erl b/apps/emqttd/src/emqttd_cm_sup.erl index 739fc08ec..bf9500630 100644 --- a/apps/emqttd/src/emqttd_cm_sup.erl +++ b/apps/emqttd/src/emqttd_cm_sup.erl @@ -42,7 +42,7 @@ start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - ets:new(emqttd_cm:table(), [set, named_table, public, {keypos, 2}, + ets:new(emqttd_cm:table(), [ordered_set, named_table, public, {keypos, 2}, {write_concurrency, true}]), Schedulers = erlang:system_info(schedulers), gproc_pool:new(emqttd_cm:pool(), hash, [{size, Schedulers}]), From 4c38cb37f5bdedae9941b6f12898801c91497ac1 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 1 Jun 2015 12:36:30 +0800 Subject: [PATCH 026/104] use re:replace/4 to fill username --- plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl b/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl index ba1bdca4d..965aaeefe 100644 --- a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl +++ b/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl @@ -82,10 +82,7 @@ ldap_bind(LDAP, UserDn, Password) -> end. fill(Username, UserDn) -> - lists:append(lists:map( - fun("$u") -> Username; - (S) -> S - end, string:tokens(UserDn, ",="))). + re:replace(UserDn, "\\$u", Username, [global, {return, list}]). description() -> "LDAP Authentication Module". From 8aa2b8fbedbf3e306eec065b69dd6b60d3a304a8 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 1 Jun 2015 16:41:59 +0800 Subject: [PATCH 027/104] fix the issue that websocket client cannot subscribe '/queue/#' --- apps/emqttd/src/emqttd_ws_client.erl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/apps/emqttd/src/emqttd_ws_client.erl index 23ac6aff0..52a7ba7d9 100644 --- a/apps/emqttd/src/emqttd_ws_client.erl +++ b/apps/emqttd/src/emqttd_ws_client.erl @@ -145,6 +145,10 @@ handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; +handle_info({subscribe, Topic, Qos}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:handle({subscribe, Topic, Qos}, ProtoState), + {noreply, State#state{proto_state = ProtoState1}}; + handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState}) -> lager:error("Shutdown for duplicate clientid: ~s", [emqttd_protocol:clientid(ProtoState)]), stop({shutdown, duplicate_id}, State); @@ -169,7 +173,8 @@ handle_info({keepalive, timeout}, State = #state{request = Req, keepalive = Keep handle_info({'EXIT', WsPid, Reason}, State = #state{ws_pid = WsPid}) -> stop(Reason, State); -handle_info(_Info, State) -> +handle_info(Info, State = #state{request = Req}) -> + lager:critical("Client(WebSocket) ~s: Unexpected Info - ~p", [Req:get(peer), Info]), {noreply, State}. terminate(Reason, #state{proto_state = ProtoState, keepalive = KeepAlive}) -> From bc4c8a94010d217727394ce2ba3655df93243cb7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 1 Jun 2015 17:05:13 +0800 Subject: [PATCH 028/104] 0.8.2 --- CHANGELOG.md | 8 ++++++++ README.md | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c32652458..40566d051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ emqttd ChangeLog ================== +0.8.2-alpha (2015-06-01) +------------------------- + +Bugfix: issue #147 - WebSocket client cannot subscribe queue '$Q/queue/${clientId}' + +Bugfix: issue #146 - emqttd_auth_ldap: fill(Username, UserDn) is not right + + 0.8.1-alpha (2015-05-28) ------------------------- diff --git a/README.md b/README.md index bc86472e7..c09b3f05b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ emqttd is aimed to provide a solid, enterprise grade, extensible open-source MQT * Passed eclipse paho interoperability tests +## Plugins + +* [emqttd_auth_clientid](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with ClientIds +* emqttd_auth_mysql - Authentication with MySQL +* emqttd_auth_ldap - Authentication with LDAP +* emqttd_mod_autosub - Subscribe some topics automatically when client connected +* [emqttd_mod_presence](https://github.com/emqtt/emqttd/wiki/Presence) - Publish presence message to $SYS topics when client connected or disconnected +* [emqttd_mod_rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) - Topics rewrite like HTTP rewrite module + + ## Design ![emqttd architecture](http://emqtt.io/static/img/Architecture.png) From f3dbb7ba540863962f960202016f0b00c3a59b24 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 1 Jun 2015 17:45:45 +0800 Subject: [PATCH 029/104] 0.8.2 --- apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- rel/reltool.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index 488ebbb8e..6f545b0bc 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.1"}, + {vsn, "0.8.2"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index b7459e519..a6004f473 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.1"}, + {vsn, "0.8.2"}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/rel/reltool.config b/rel/reltool.config index ace833401..1ea929f55 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.8.1", + {rel, "emqttd", "0.8.2", [ kernel, stdlib, From 5a75e59dd1842cbfe062de5e0e5b74938d918bd9 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 2 Jun 2015 12:52:40 +0800 Subject: [PATCH 030/104] fix mysql auth error --- apps/emqttd/src/emqttd_access_control.erl | 1 + plugins/emqttd_auth_mysql/etc/plugin.config | 3 ++- plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl | 8 ++++++-- plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl | 2 +- rel/files/plugins.config | 7 ++++++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/emqttd/src/emqttd_access_control.erl b/apps/emqttd/src/emqttd_access_control.erl index 3f1ca1b36..30efa0ad5 100644 --- a/apps/emqttd/src/emqttd_access_control.erl +++ b/apps/emqttd/src/emqttd_access_control.erl @@ -179,6 +179,7 @@ handle_call({register_mod, Type, Mod, Opts}, _From, State) -> ets:insert(?ACCESS_CONTROL_TAB, {tab_key(Type), [{Mod, ModState}|Mods]}), ok; {'EXIT', Error} -> + lager:error("Access Control: register ~s error - ~p", [Mod, Error]), {error, Error} end; _ -> diff --git a/plugins/emqttd_auth_mysql/etc/plugin.config b/plugins/emqttd_auth_mysql/etc/plugin.config index a5ef4bc41..367983f5f 100644 --- a/plugins/emqttd_auth_mysql/etc/plugin.config +++ b/plugins/emqttd_auth_mysql/etc/plugin.config @@ -9,8 +9,9 @@ ]}, {emqttd_auth_mysql, [ {users_table, mqtt_users}, + {password_hash, plain}, {field_mapper, [ {username, username}, - {password, password, plain} + {password, password} ]} ]} diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl index cf2d32cbc..be8685103 100644 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl +++ b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl @@ -38,15 +38,19 @@ init(Opts) -> Mapper = proplists:get_value(field_mapper, Opts), - {ok, #state{user_table = proplists:get_value(user_table, Opts, mqtt_users), + {ok, #state{user_table = proplists:get_value(user_table, Opts), name_field = proplists:get_value(username, Mapper), pass_field = proplists:get_value(password, Mapper), - pass_hash = proplists:get_value(Opts, password_hash)}}. + pass_hash = proplists:get_value(password_hash, Opts)}}. check(#mqtt_client{username = undefined}, _Password, _State) -> {error, "Username undefined"}; +check(#mqtt_client{username = <<>>}, _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{user_table = UserTab, pass_hash = Type, name_field = NameField, pass_field = PassField}) -> diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl index 86881a4bb..11734fbad 100644 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl +++ b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl @@ -40,7 +40,7 @@ start(_StartType, _StartArgs) -> Env = application:get_all_env(), - emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), + ok = emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), supervisor:start_link({local, ?MODULE}, ?MODULE, []). prep_stop(State) -> diff --git a/rel/files/plugins.config b/rel/files/plugins.config index 57dcb8abf..c2fde4d51 100644 --- a/rel/files/plugins.config +++ b/rel/files/plugins.config @@ -9,7 +9,12 @@ % {encoding, utf8} % ]}, % {emqttd_auth_mysql, [ -% {user_table, mqtt_users} +% {users_table, mqtt_users}, +% {password_hash, plain}, +% {field_mapper, [ +% {username, username}, +% {password, password} +% ]} % ]} % % {emqttd_dashboard, [ From 0044514a878da363539a5d089b20abc72df9eb88 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 2 Jun 2015 15:29:53 +0800 Subject: [PATCH 031/104] rename 'users_table' plugin config to 'user_table': --- plugins/emqttd_auth_mysql/etc/plugin.config | 2 +- rel/files/plugins.config | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/emqttd_auth_mysql/etc/plugin.config b/plugins/emqttd_auth_mysql/etc/plugin.config index 367983f5f..1ab1a6e07 100644 --- a/plugins/emqttd_auth_mysql/etc/plugin.config +++ b/plugins/emqttd_auth_mysql/etc/plugin.config @@ -8,7 +8,7 @@ {encoding, utf8} ]}, {emqttd_auth_mysql, [ - {users_table, mqtt_users}, + {user_table, mqtt_users}, {password_hash, plain}, {field_mapper, [ {username, username}, diff --git a/rel/files/plugins.config b/rel/files/plugins.config index c2fde4d51..da93a5b03 100644 --- a/rel/files/plugins.config +++ b/rel/files/plugins.config @@ -9,14 +9,13 @@ % {encoding, utf8} % ]}, % {emqttd_auth_mysql, [ -% {users_table, mqtt_users}, +% {user_table, mqtt_users}, % {password_hash, plain}, % {field_mapper, [ % {username, username}, % {password, password} % ]} % ]} -% % {emqttd_dashboard, [ % {listener, % {http, 18083, [ From 95e530758b6a1bd0ef9bee2bfc39cf4dd1f1c44e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 3 Jun 2015 22:59:29 +0800 Subject: [PATCH 032/104] rm 'h' --- rel/files/emqttd.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 977101cce..ba90430b8 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -1,4 +1,4 @@ -h% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et [{kernel, [{start_timer, true}, From 4309f0a239722c13da6775cc7316edaf49acd6eb Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 3 Jun 2015 23:02:46 +0800 Subject: [PATCH 033/104] fix --- rel/files/emqttd.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index ba90430b8..442c801d7 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -105,7 +105,7 @@ {low_queue_watermark, 0.2}, %% Queue Qos0 offline messages? {queue_qos0_messages, false} - ]}, + ]} ]}, %% Broker Options {broker, [ From 053ddf61136700d9706c29083d6db56376c605b2 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 3 Jun 2015 23:50:02 +0800 Subject: [PATCH 034/104] fix issue #155 --- apps/emqtt/src/emqtt_parser.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqtt/src/emqtt_parser.erl b/apps/emqtt/src/emqtt_parser.erl index d2a70993e..387f041d4 100644 --- a/apps/emqtt/src/emqtt_parser.erl +++ b/apps/emqtt/src/emqtt_parser.erl @@ -196,7 +196,7 @@ wrap(Header, Rest) -> % parse_qos(Rest, [QoS | Acc]). parse_topics(_, <<>>, Topics) -> - Topics; + lists:reverse(Topics); parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> {Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin), parse_topics(Sub, Rest, [{Name, QoS}| Topics]); From ead7b23330ab50c34a37eb6d4387a7b6926cb5fe Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 3 Jun 2015 23:50:31 +0800 Subject: [PATCH 035/104] 0.8.3 --- apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- rel/reltool.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index 6f545b0bc..c444276fa 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.2"}, + {vsn, "0.8.3"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index a6004f473..5c3347f27 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.2"}, + {vsn, "0.8.3"}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/rel/reltool.config b/rel/reltool.config index 1ea929f55..f26654f74 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.8.2", + {rel, "emqttd", "0.8.3", [ kernel, stdlib, From 7bbd7bb5247102821f0f7805cc5160f35d2f0e4c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 4 Jun 2015 21:18:44 +0800 Subject: [PATCH 036/104] fix doc --- apps/emqttd/src/emqttd_sm_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_sm_sup.erl b/apps/emqttd/src/emqttd_sm_sup.erl index ece44dd38..fc50956d1 100644 --- a/apps/emqttd/src/emqttd_sm_sup.erl +++ b/apps/emqttd/src/emqttd_sm_sup.erl @@ -20,7 +20,7 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqttd client manager supervisor. +%%% emqttd session manager supervisor. %%% %%% @end %%%----------------------------------------------------------------------------- From b544bdc10b1c31be9f4542b2c9f66863f3f0fc95 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 4 Jun 2015 23:13:59 +0800 Subject: [PATCH 037/104] queue:in new message after old one dropped --- apps/emqttd/src/emqttd_queue.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqttd/src/emqttd_queue.erl b/apps/emqttd/src/emqttd_queue.erl index 34b34a58f..d9897a3f8 100644 --- a/apps/emqttd/src/emqttd_queue.erl +++ b/apps/emqttd/src/emqttd_queue.erl @@ -25,7 +25,7 @@ %%% @end %%%----------------------------------------------------------------------------- -%% TODO: this module should be removed... +%% TODO: this module should be rewrited... -module(emqttd_queue). @@ -69,12 +69,12 @@ in(ClientId, Message = #mqtt_message{qos = Qos}, false -> % full if Qos =:= ?QOS_0 -> - lager:warning("Queue ~s drop qos0 message: ~p", [ClientId, Message]), + lager:error("Queue ~s drop qos0 message: ~p", [ClientId, Message]), Wrapper; true -> {{value, Msg}, Queue1} = queue:drop(Queue), - lager:warning("Queue ~s drop message: ~p", [ClientId, Msg]), - Wrapper#mqtt_queue_wrapper{queue = Queue1} + lager:error("Queue ~s drop message: ~p", [ClientId, Msg]), + Wrapper#mqtt_queue_wrapper{queue = queue:in(Message, Queue1)} end end. From 3de4764e5359bd68c18420c25c63e07d22412256 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 5 Jun 2015 11:22:25 +0800 Subject: [PATCH 038/104] contributors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c09b3f05b..edfba9761 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The MIT License (MIT) [@turtleDeng](https://github.com/turtleDeng) [@Hades32](https://github.com/Hades32) [@huangdan](https://github.com/huangdan) - +[@callbay](https://github.com/callbay) ## Author From 0d92fa92352a628f00c5c26808c42b69ab588b1c Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 5 Jun 2015 11:22:39 +0800 Subject: [PATCH 039/104] 0.8.3 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40566d051..be3055b2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ emqttd ChangeLog ================== +0.8.3-beta (2015-06-05) +------------------------- + +Bugfix: issue #158 - should queue:in new message after old one dropped + +Bugfix: issue #155 - emqtt_parser.erl: parse_topics/3 should reverse topics + +Bugfix: issue #149 - Forget to merge plugins/emqttd_auth_mysql from 'dev' branch to 'master' in 0.8.x release + + 0.8.2-alpha (2015-06-01) ------------------------- From be0adc14c31ffa5e28a6ca51c84884a21966f594 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 5 Jun 2015 11:22:49 +0800 Subject: [PATCH 040/104] , --- rel/files/plugins.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rel/files/plugins.config b/rel/files/plugins.config index da93a5b03..ce65e0625 100644 --- a/rel/files/plugins.config +++ b/rel/files/plugins.config @@ -15,7 +15,7 @@ % {username, username}, % {password, password} % ]} -% ]} +% ]}, % {emqttd_dashboard, [ % {listener, % {http, 18083, [ From 789e482a305b25f734509e1742c803fbb1e07266 Mon Sep 17 00:00:00 2001 From: Feng Date: Sat, 6 Jun 2015 19:04:02 +0800 Subject: [PATCH 041/104] inflight, queued --- apps/emqttd/src/emqttd_session.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 15a465d4d..3052f059a 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -57,6 +57,8 @@ client_pid :: pid(), message_id = 1, submap :: map(), + inflight_messages, + queued_messages, msg_queue, %% do not receive rel awaiting_ack :: map(), awaiting_rel :: map(), From 9d5223dd1ace63ea2b07dfddc7db7d99a5e846d5 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 8 Jun 2015 23:31:20 +0800 Subject: [PATCH 042/104] contributors --- README.md | 12 +++++------ apps/emqttd/src/emqttd_session.erl | 32 +++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index edfba9761..6ab9acaba 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,12 @@ The MIT License (MIT) ## Contributors -[@hejin1026](https://github.com/hejin1026) -[@desoulter](https://github.com/desoulter) -[@turtleDeng](https://github.com/turtleDeng) -[@Hades32](https://github.com/Hades32) -[@huangdan](https://github.com/huangdan) -[@callbay](https://github.com/callbay) +* [@hejin1026](https://github.com/hejin1026) +* [@desoulter](https://github.com/desoulter) +* [@turtleDeng](https://github.com/turtleDeng) +* [@Hades32](https://github.com/Hades32) +* [@huangdan](https://github.com/huangdan) +* [@callbay](https://github.com/callbay) ## Author diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 15a465d4d..a629e3b08 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -173,17 +173,31 @@ puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> %%------------------------------------------------------------------------------ -spec subscribe(session(), [{binary(), mqtt_qos()}]) -> {ok, session(), [mqtt_qos()]}. subscribe(SessState = #session_state{clientid = ClientId, submap = SubMap}, Topics) -> - Resubs = [Topic || {Name, _Qos} = Topic <- Topics, maps:is_key(Name, SubMap)], - case Resubs of - [] -> ok; - _ -> lager:warning("~s resubscribe ~p", [ClientId, Resubs]) - end, - SubMap1 = lists:foldl(fun({Name, Qos}, Acc) -> maps:put(Name, Qos, Acc) end, SubMap, Topics), + + %% subscribe first and don't care if the subscriptions have been existed {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), + lager:info([{client, ClientId}], "Client ~s subscribe ~p. Granted QoS: ~p", - [ClientId, Topics, GrantedQos]), - %%TODO: should be gen_event and notification... - [emqttd_msg_store:redeliver(Name, self()) || {Name, _} <- Topics], + [ClientId, Topics, GrantedQos]), + + + %% : 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. + lists:foreach(fun({Name, _Qos}) -> + case maps:is_key(Name, SubMap) of + true -> + lager:warning("~s resubscribe ~p", [ClientId, Name]); + false -> + %%TODO: this is not right, rewrite later... + emqttd_msg_store:redeliver(Name, self()) + end + end, Topics), + + SubMap1 = lists:foldl(fun({Name, Qos}, Acc) -> + maps:put(Name, Qos, Acc) + end, SubMap, Topics), + {ok, SessState#session_state{submap = SubMap1}, GrantedQos}; subscribe(SessPid, Topics) when is_pid(SessPid) -> From bfc83b8c4554c7d71ab62cd9d13e2236f697b091 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 8 Jun 2015 23:31:33 +0800 Subject: [PATCH 043/104] 0.8.4 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be3055b2d..cb598e803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ emqttd ChangeLog ================== +0.8.4-beta (2015-06-08) +------------------------- + +Bugfix: issue #165 - duplicated message when publish 'retained' message to persistent client + + 0.8.3-beta (2015-06-05) ------------------------- From f0583a1c29a91edf949dc75c58b46263d9b23079 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 8 Jun 2015 23:34:09 +0800 Subject: [PATCH 044/104] 0.8.4 --- CONTRIBUTORS | 10 ++++++++++ apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- rel/reltool.config | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTORS diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 000000000..8f1f4c344 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,10 @@ + +# CONTRIBUTORS + +* [@callbay](https://github.com/callbay) +* [@hejin1026](https://github.com/hejin1026) +* [@desoulter](https://github.com/desoulter) +* [@turtleDeng](https://github.com/turtleDeng) +* [@Hades32](https://github.com/Hades32) +* [@huangdan](https://github.com/huangdan) + diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index c444276fa..06751dfac 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.3"}, + {vsn, "0.8.4"}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index 5c3347f27..a4e0d72e7 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.3"}, + {vsn, "0.8.4"}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/rel/reltool.config b/rel/reltool.config index f26654f74..6e5d1dde7 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.8.3", + {rel, "emqttd", "0.8.4", [ kernel, stdlib, From 29540946194fe7bf32edf74b82a656ddac1e5d45 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 9 Jun 2015 11:50:53 +0800 Subject: [PATCH 045/104] fix issue #53 - client will receive duplicate messages when overlapping subscription --- apps/emqttd/src/emqttd_pubsub.erl | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/apps/emqttd/src/emqttd_pubsub.erl index b52037d29..b89355c76 100644 --- a/apps/emqttd/src/emqttd_pubsub.erl +++ b/apps/emqttd/src/emqttd_pubsub.erl @@ -24,6 +24,7 @@ %%% %%% @end %%%----------------------------------------------------------------------------- + -module(emqttd_pubsub). -author("Feng Lee "). @@ -217,6 +218,7 @@ match(Topic) when is_binary(Topic) -> init([Id, _Opts]) -> process_flag(min_heap_size, 1024*1024), gproc_pool:connect_worker(pubsub, {?MODULE, Id}), + %%TODO: gb_trees to replace maps? {ok, #state{id = Id, submap = maps:new()}}. handle_call({subscribe, SubPid, Topics}, _From, State) -> @@ -384,9 +386,24 @@ add_topic(TopicR = #mqtt_topic{topic = Topic}) -> end end. -add_subscriber({TopicR, Subscriber}) when is_record(TopicR, mqtt_topic) -> +%% Fix issue #53 - Remove Overlapping Subscriptions +add_subscriber({TopicR, Subscriber = #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}}) + when is_record(TopicR, mqtt_topic) -> case add_topic(TopicR) of ok -> + OverlapSubs = [Sub || Sub = #mqtt_subscriber{topic = SubTopic, qos = SubQos} + <- mnesia:index_read(subscriber, SubPid, #mqtt_subscriber.pid), + SubTopic =:= Topic, SubQos =/= Qos], + + %% remove overlapping subscribers + if + length(OverlapSubs) =:= 0 -> ok; + true -> + lager:warning("Remove overlapping subscribers: ~p", [OverlapSubs]), + [mnesia:delete_object(subscriber, OverlapSub, write) || OverlapSub <- OverlapSubs] + end, + + %% insert subscriber mnesia:write(subscriber, Subscriber, write); Error -> Error From 553fb394f37ce68437a2d8c977bdff73c30659f4 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 9 Jun 2015 11:57:44 +0800 Subject: [PATCH 046/104] vsn 'git' --- apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- rel/reltool.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index 06751dfac..c84d855f6 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.4"}, + {vsn, git}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index a4e0d72e7..024fcdac9 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.4"}, + {vsn, git}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/rel/reltool.config b/rel/reltool.config index 6e5d1dde7..2dbb76ea5 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.8.4", + {rel, "emqttd", git, [ kernel, stdlib, From d5e35b2423bd0d60f9de3141b93bbd05379a9376 Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 9 Jun 2015 11:58:37 +0800 Subject: [PATCH 047/104] 0.8.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb598e803..6943606bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ emqttd ChangeLog ================== +0.8.5-beta (2015-06-12) +------------------------- + +Bugfix: issue #53 - client will receive duplicate messages when overlapping subscription + + 0.8.4-beta (2015-06-08) ------------------------- From 9bc84d2b53f3b2267fd80ff520f2e6a723506fdb Mon Sep 17 00:00:00 2001 From: Feng Date: Tue, 9 Jun 2015 12:01:14 +0800 Subject: [PATCH 048/104] rm .swp --- doc/.retain.md.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/.retain.md.swp diff --git a/doc/.retain.md.swp b/doc/.retain.md.swp deleted file mode 100644 index 648c9731dc496c528162ce136a590152afb2b1ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&KQ9D97{~EB5|M}y-oU!8?H*UqDzCWYB6s?+``~2HGCK+ll{cYLD|KE8Dis~? z!Lw&IiY=~N`A+i7lga$a?5Eqz9Ci+R{a`2R8f~k_oZLPQ7w#YDAC`;}ca@2h1NF_0 zoz9E0Jh55b%G Date: Wed, 10 Jun 2015 16:24:31 +0800 Subject: [PATCH 049/104] 0.8.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6943606bf..a8b61e4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ emqttd ChangeLog ================== -0.8.5-beta (2015-06-12) +0.8.5-beta (2015-06-10) ------------------------- Bugfix: issue #53 - client will receive duplicate messages when overlapping subscription From 5559cd7f58170c25700db662b99948dd34cf8a80 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 00:01:25 +0800 Subject: [PATCH 050/104] add alarm --- apps/emqttd/src/emqttd_alarm.erl | 98 +++++++++++++++++++ .../{emqttd_queue.erl => emqttd_mqueue.erl} | 0 2 files changed, 98 insertions(+) create mode 100644 apps/emqttd/src/emqttd_alarm.erl rename apps/emqttd/src/{emqttd_queue.erl => emqttd_mqueue.erl} (100%) diff --git a/apps/emqttd/src/emqttd_alarm.erl b/apps/emqttd/src/emqttd_alarm.erl new file mode 100644 index 000000000..73c432534 --- /dev/null +++ b/apps/emqttd/src/emqttd_alarm.erl @@ -0,0 +1,98 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% copy alarm_handler. +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +-module(emqttd_alarm). + +-export([start_link/0, set_alarm/1, clear_alarm/1, get_alarms/0, + add_alarm_handler/1, add_alarm_handler/2, + delete_alarm_handler/1]). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2]). + +-define(SERVER, ?MODULE). + +-type alarm() :: {AlarmId :: any(), AlarmDescription :: string() | binary()}. + +start_link() -> + case gen_event:start_link({local, ?SERVER}) of + {ok, Pid} -> + gen_event:add_handler(?SERVER, ?MODULE, []), + {ok, Pid}; + Error -> + Error + end. + +-spec set_alarm(alarm()) -> ok. +set_alarm(Alarm) -> + gen_event:notify(?SERVER, {set_alarm, Alarm}). + +-spec clear_alarm(any()) -> ok. +clear_alarm(AlarmId) -> + gen_event:notify(?SERVER, {clear_alarm, AlarmId}). + +get_alarms() -> + gen_event:call(?SERVER, ?MODULE, get_alarms). + +add_alarm_handler(Module) when is_atom(Module) -> + gen_event:add_handler(?SERVER, Module, []). + +add_alarm_handler(Module, Args) when is_atom(Module) -> + gen_event:add_handler(?SERVER, Module, Args). + +delete_alarm_handler(Module) when is_atom(Module) -> + gen_event:delete_handler(?SERVER, Module, []). + +%%----------------------------------------------------------------- +%% Default Alarm handler +%%----------------------------------------------------------------- + +init(_) -> {ok, []}. + +handle_event({set_alarm, Alarm}, Alarms)-> + %%TODO: publish to $SYS + {ok, [Alarm | Alarms]}; + +handle_event({clear_alarm, AlarmId}, Alarms)-> + %TODO: publish to $SYS + {ok, lists:keydelete(AlarmId, 1, Alarms)}; + +handle_event(_, Alarms)-> + {ok, Alarms}. + +handle_info(_, Alarms) -> {ok, Alarms}. + +handle_call(get_alarms, Alarms) -> {ok, Alarms, Alarms}; + +handle_call(_Query, Alarms) -> {ok, {error, bad_query}, Alarms}. + +terminate(swap, Alarms) -> + {?MODULE, Alarms}; + +terminate(_, _) -> + ok. + diff --git a/apps/emqttd/src/emqttd_queue.erl b/apps/emqttd/src/emqttd_mqueue.erl similarity index 100% rename from apps/emqttd/src/emqttd_queue.erl rename to apps/emqttd/src/emqttd_mqueue.erl From 50d65897ce1894078c521e56e2037d099c9a8e1f Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 00:02:45 +0800 Subject: [PATCH 051/104] session, queue config --- rel/files/emqttd.config | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 442c801d7..4f08c5e9a 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -90,21 +90,21 @@ %% Expired after 2 days {expired_after, 48}, %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - {max_inflight_messages, 20}, + {max_inflight_message, 20}, %% Max retries for unacknolege Qos1/2 messages {max_unack_retries, 3}, %% Retry after 10 seconds {unack_retry_after, 10} ]}, {queue, [ - %% Max messages queued when client is disconnected, or inflight messsages is overload - {max_queued_messages, 100}, + %% Max messages queued when client is disconnected, or inflight messsage window is overload + {max_queued_messages, 200}, %% High watermark of queued messsages {high_queue_watermark, 0.6}, %% Low watermark of queued messsages {low_queue_watermark, 0.2}, %% Queue Qos0 offline messages? - {queue_qos0_messages, false} + {queue_qos0_messages, true} ]} ]}, %% Broker Options From db2cc7ba0b776fe3818266bdc097cef967e46073 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 00:03:03 +0800 Subject: [PATCH 052/104] git --- apps/emqttd/src/emqttd.app.src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index 5c3347f27..024fcdac9 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.3"}, + {vsn, git}, {modules, []}, {registered, []}, {applications, [kernel, From c4027dfc16b3940ab3f78e619ec0daa5338888a6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 00:05:20 +0800 Subject: [PATCH 053/104] new queue --- apps/emqttd/src/emqttd_mqueue.erl | 142 +++++++++++++++++++----------- 1 file changed, 93 insertions(+), 49 deletions(-) diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index d9897a3f8..a76c5ef97 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -20,39 +20,76 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqttd simple queue. +%%% simple message queue. +%%% +%%% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client +%%% should be online in most of the time. +%%% +%%% This module wraps an erlang queue to store offline messages temporarily for MQTT +%%% persistent session. +%%% +%%% If the broker restarted or crashed, all the messages stored will be gone. %%% %%% @end %%%----------------------------------------------------------------------------- -%% TODO: this module should be rewrited... - --module(emqttd_queue). +-module(emqttd_mqueue). -author("Feng Lee "). -include_lib("emqtt/include/emqtt.hrl"). --export([new/1, new/2, in/3, all/1, clear/1]). +-export([new/2, name/1, + is_empty/1, len/1, + in/2, out/1, + peek/1, + to_list/1]). --define(DEFAULT_MAX_LEN, 1000). +%% in_r/2, out_r/1, --record(mqtt_queue_wrapper, {queue = queue:new(), - max_len = ?DEFAULT_MAX_LEN, - store_qos0 = false}). +-define(MAX_LEN, 600). --type mqtt_queue() :: #mqtt_queue_wrapper{}. +-define(HIGH_WM, 0.6). + +-define(LOW_WM, 0.2). + +-record(mqueue, {name, + len = 0, + max_len = ?MAX_LEN, + queue = queue:new(), + store_qos0 = false, + high_watermark = ?HIGH_WM, + low_watermark = ?LOW_WM, + alert = false}). + +-type mqueue() :: #mqueue{}. + +-type queue_option() :: {max_queued_messages, pos_integer()} %% Max messages queued + | {high_queue_watermark, float()} %% High watermark + | {low_queue_watermark, float()} %% Low watermark + | {queue_qos0_messages, boolean()}. %% Queue Qos0 messages? %%------------------------------------------------------------------------------ -%% @doc -%% New Queue. -%% +%% @doc New Queue. %% @end %%------------------------------------------------------------------------------ --spec new(non_neg_integer()) -> mqtt_queue(). -new(MaxLen) -> #mqtt_queue_wrapper{max_len = MaxLen}. +-spec new(binary() | string(), list(queue_option())) -> mqueue(). +new(Name, Opts) -> + MaxLen = emqttd_opts:g(max_queued_messages, Opts, ?MAX_LEN), + HighWM = round(MaxLen * emqttd_opts:g(high_queue_watermark, Opts, ?HIGH_WM)), + LowWM = round(MaxLen * emqttd_opts:g(low_queue_watermark, Opts, ?LOW_WM)), + #mqueue{name = Name, max_len = MaxLen, + store_qos0 = emqttd_opts:g(queue_qos0_messages, Opts, false), + high_watermark = HighWM, low_watermark = LowWM}. -new(MaxLen, StoreQos0) -> #mqtt_queue_wrapper{max_len = MaxLen, store_qos0 = StoreQos0}. +name(#mqueue{name = Name}) -> + Name. + +len(#mqueue{len = Len}) -> + Len. + +is_empty(#mqueue{len = 0}) -> true; +is_empty(_Q) -> false. %%------------------------------------------------------------------------------ %% @doc @@ -60,39 +97,46 @@ new(MaxLen, StoreQos0) -> #mqtt_queue_wrapper{max_len = MaxLen, store_qos0 = Sto %% %% @end %%------------------------------------------------------------------------------ --spec in(binary(), mqtt_message(), mqtt_queue()) -> mqtt_queue(). -in(ClientId, Message = #mqtt_message{qos = Qos}, - Wrapper = #mqtt_queue_wrapper{queue = Queue, max_len = MaxLen}) -> - case queue:len(Queue) < MaxLen of - true -> - Wrapper#mqtt_queue_wrapper{queue = queue:in(Message, Queue)}; - false -> % full - if - Qos =:= ?QOS_0 -> - lager:error("Queue ~s drop qos0 message: ~p", [ClientId, Message]), - Wrapper; - true -> - {{value, Msg}, Queue1} = queue:drop(Queue), - lager:error("Queue ~s drop message: ~p", [ClientId, Msg]), - Wrapper#mqtt_queue_wrapper{queue = queue:in(Message, Queue1)} - end - end. +-spec in(mqtt_message(), mqueue()) -> mqueue(). +in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{store_qos0 = false}) -> + MQ; +%% queue is full, drop the oldest +in(Msg, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen, queue = Q}) when Len =:= MaxLen -> + Q2 = case queue:out(Q) of + {{value, OldMsg}, Q1} -> + %%TODO: publish the dropped message to $SYS? + lager:error("Queue(~s) drop message: ~p", [Name, OldMsg]), + Q1; + {empty, Q1} -> %% maybe max_len is 1 + Q1 + end, + MQ#mqueue{queue = queue:in(Msg, Q2)}; +in(Msg, MQ = #mqueue{len = Len, queue = Q}) -> + maybe_set_alarm(MQ#mqueue{len = Len+1, queue = queue:in(Msg, Q)}). -%%------------------------------------------------------------------------------ -%% @doc -%% Get all messages in queue. -%% -%% @end -%%------------------------------------------------------------------------------ --spec all(mqtt_queue()) -> list(). -all(#mqtt_queue_wrapper { queue = Queue }) -> queue:to_list(Queue). +out(MQ = #mqueue{len = 0, queue = _Q}) -> + {empty, MQ}; +out(MQ = #mqueue{len = Len, queue = Q}) -> + {Result, Q1} = queue:out(Q), + {Result, maybe_clear_alarm(MQ#mqueue{len = Len - 1, queue = Q1})}. -%%------------------------------------------------------------------------------ -%% @doc -%% Clear queue. -%% -%% @end -%%------------------------------------------------------------------------------ --spec clear(mqtt_queue()) -> mqtt_queue(). -clear(Queue) -> Queue#mqtt_queue_wrapper{queue = queue:new()}. +peek(#mqueue{queue = Q}) -> + queue:peek(Q). + +to_list(#mqueue{queue = Q}) -> + queue:to_list(Q). + +maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_watermark = HighWM, alert = false}) + when Len >= HighWM -> + AlarmDescr = io_lib:format("len ~p > high_watermark ~p", [Len, HighWM]), + emqttd_alarm:set_alarm({{queue_high_watermark, Name}, AlarmDescr}), + MQ#mqueue{alert = true}; +maybe_set_alarm(MQ) -> + MQ. + +maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_watermark = LowWM, alert = true}) + when Len =< LowWM -> + emqttd_alarm:clear_alarm({queue_high_watermark, Name}), MQ#mqueue{alert = false}; +maybe_clear_alarm(MQ) -> + MQ. From 051b8604e86a9a93fc1b82e97b7618533899a20c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 00:05:44 +0800 Subject: [PATCH 054/104] g/2, g/3 --- apps/emqttd/src/emqttd_opts.erl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_opts.erl b/apps/emqttd/src/emqttd_opts.erl index a83466a73..dfd5b74b6 100644 --- a/apps/emqttd/src/emqttd_opts.erl +++ b/apps/emqttd/src/emqttd_opts.erl @@ -28,7 +28,7 @@ -author("Feng Lee "). --export([merge/2]). +-export([merge/2, g/2, g/3]). %%------------------------------------------------------------------------------ %% @doc Merge Options @@ -50,3 +50,13 @@ merge(Defaults, Options) -> end end, Defaults, Options). +%%------------------------------------------------------------------------------ +%% @doc Get option +%% @end +%%------------------------------------------------------------------------------ +g(Key, Options) -> + proplists:get_value(Key, Options). + +g(Key, Options, Default) -> + proplists:get_value(Key, Options, Default). + From 53099f253637db9f09d9e8779e6b14822ab5999c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 10:23:13 +0800 Subject: [PATCH 055/104] 0.9.0 --- CHANGELOG.md | 12 ++++++++++++ apps/emqttd/src/emqttd_app.erl | 2 +- apps/emqttd/src/emqttd_mqueue.erl | 13 +++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b61e4a7..88821daf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ emqttd ChangeLog ================== +0.9.0-alpha (2015-06-14) +------------------------- + +Session + +Queue + +Alarm + +Protocol Compliant + + 0.8.5-beta (2015-06-10) ------------------------- diff --git a/apps/emqttd/src/emqttd_app.erl b/apps/emqttd/src/emqttd_app.erl index 138808b08..f9434b385 100644 --- a/apps/emqttd/src/emqttd_app.erl +++ b/apps/emqttd/src/emqttd_app.erl @@ -76,8 +76,8 @@ start_servers(Sup) -> {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, - %{"emqttd router", emqttd_router}, {"emqttd broker", emqttd_broker}, + {"emqttd alarm", emqttd_alarm}, {"emqttd mode supervisor", emqttd_mod_sup}, {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup}}, {"emqttd access control", emqttd_access_control}, diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index a76c5ef97..874aa82d0 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -20,7 +20,7 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% simple message queue. +%%% Simple message queue. %%% %%% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client %%% should be online in most of the time. @@ -45,8 +45,6 @@ peek/1, to_list/1]). -%% in_r/2, out_r/1, - -define(MAX_LEN, 600). -define(HIGH_WM, 0.6). @@ -78,9 +76,12 @@ new(Name, Opts) -> MaxLen = emqttd_opts:g(max_queued_messages, Opts, ?MAX_LEN), HighWM = round(MaxLen * emqttd_opts:g(high_queue_watermark, Opts, ?HIGH_WM)), LowWM = round(MaxLen * emqttd_opts:g(low_queue_watermark, Opts, ?LOW_WM)), - #mqueue{name = Name, max_len = MaxLen, - store_qos0 = emqttd_opts:g(queue_qos0_messages, Opts, false), - high_watermark = HighWM, low_watermark = LowWM}. + StoreQos0 = emqttd_opts:g(queue_qos0_messages, Opts, false), + #mqueue{name = Name, + max_len = MaxLen, + store_qos0 = StoreQos0, + high_watermark = HighWM, + low_watermark = LowWM}. name(#mqueue{name = Name}) -> Name. From 4c906b19aebd2288ca5c22194d7560c5b61fa69c Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 11 Jun 2015 12:07:44 +0800 Subject: [PATCH 056/104] new session --- apps/emqtt/src/emqtt.app.src | 2 +- apps/emqttd/src/emqttd.app.src | 2 +- apps/emqttd/src/emqttd_mqueue.erl | 15 +++++ apps/emqttd/src/emqttd_pubsub.erl | 19 +++++- apps/emqttd/src/emqttd_session.erl | 103 ++++++++++++++++++++++------- apps/zenmq/README | 14 ++++ apps/zenmq/src/zenmq.app.src | 12 ++++ apps/zenmq/src/zenmq.erl | 2 + apps/zenmq/src/zenmq_app.erl | 16 +++++ apps/zenmq/src/zenmq_sup.erl | 27 ++++++++ rel/files/emqttd.config | 4 +- rel/reltool.config | 2 +- 12 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 apps/emqttd/src/emqttd_mqueue.erl create mode 100644 apps/zenmq/README create mode 100644 apps/zenmq/src/zenmq.app.src create mode 100644 apps/zenmq/src/zenmq.erl create mode 100644 apps/zenmq/src/zenmq_app.erl create mode 100644 apps/zenmq/src/zenmq_sup.erl diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src index c444276fa..c84d855f6 100644 --- a/apps/emqtt/src/emqtt.app.src +++ b/apps/emqtt/src/emqtt.app.src @@ -1,7 +1,7 @@ {application, emqtt, [ {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.3"}, + {vsn, git}, {modules, []}, {registered, []}, {applications, [ diff --git a/apps/emqttd/src/emqttd.app.src b/apps/emqttd/src/emqttd.app.src index 5c3347f27..024fcdac9 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/apps/emqttd/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.3"}, + {vsn, git}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl new file mode 100644 index 000000000..58077355f --- /dev/null +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -0,0 +1,15 @@ + +-module(emqttd_mqueue). + +-export([init/1, in/1]). + +-record(queue_state, { + max_queued_messages = 1000 +}). + +init(Opts) -> + {ok, #queue_state{}}. + +in(Msg, Q = #queue_state{}) -> + Q. + diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/apps/emqttd/src/emqttd_pubsub.erl index b52037d29..b89355c76 100644 --- a/apps/emqttd/src/emqttd_pubsub.erl +++ b/apps/emqttd/src/emqttd_pubsub.erl @@ -24,6 +24,7 @@ %%% %%% @end %%%----------------------------------------------------------------------------- + -module(emqttd_pubsub). -author("Feng Lee "). @@ -217,6 +218,7 @@ match(Topic) when is_binary(Topic) -> init([Id, _Opts]) -> process_flag(min_heap_size, 1024*1024), gproc_pool:connect_worker(pubsub, {?MODULE, Id}), + %%TODO: gb_trees to replace maps? {ok, #state{id = Id, submap = maps:new()}}. handle_call({subscribe, SubPid, Topics}, _From, State) -> @@ -384,9 +386,24 @@ add_topic(TopicR = #mqtt_topic{topic = Topic}) -> end end. -add_subscriber({TopicR, Subscriber}) when is_record(TopicR, mqtt_topic) -> +%% Fix issue #53 - Remove Overlapping Subscriptions +add_subscriber({TopicR, Subscriber = #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}}) + when is_record(TopicR, mqtt_topic) -> case add_topic(TopicR) of ok -> + OverlapSubs = [Sub || Sub = #mqtt_subscriber{topic = SubTopic, qos = SubQos} + <- mnesia:index_read(subscriber, SubPid, #mqtt_subscriber.pid), + SubTopic =:= Topic, SubQos =/= Qos], + + %% remove overlapping subscribers + if + length(OverlapSubs) =:= 0 -> ok; + true -> + lager:warning("Remove overlapping subscribers: ~p", [OverlapSubs]), + [mnesia:delete_object(subscriber, OverlapSub, write) || OverlapSub <- OverlapSubs] + end, + + %% insert subscriber mnesia:write(subscriber, Subscriber, write); Error -> Error diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 3052f059a..ed10c37a7 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -43,6 +43,7 @@ unsubscribe/2, destroy/2]). +%% This api looks strange... :( -export([store/2]). %% Start gen_server @@ -52,19 +53,53 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(session_state, { - clientid :: binary(), +-record(session_state, { + %% ClientId: Identifier of Session + clientid :: binary(), + + %% Client Pid linked with session client_pid :: pid(), - message_id = 1, - submap :: map(), - inflight_messages, - queued_messages, - msg_queue, %% do not receive rel - awaiting_ack :: map(), - awaiting_rel :: map(), + + %% Last message id of the session + message_id = 1, + + %% Client’s subscriptions. + subscriptions :: list(), + + %% Inflight window size + inflight_window = 40, + + %% Inflight qos1, qos2 messages sent to the client but unacked, QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged. + %% Client <- Broker + inflight_queue :: list(), + + %% Inflight qos2 messages received from client and waiting for pubrel. QoS 2 messages which have been received from the Client, but have not been completely acknowledged. + %% Client -> Broker + awaiting_queue :: list(), + + %% All qos1, qos2 messages published to when client is disconnected. QoS 1 and QoS 2 messages pending transmission to the Client. + %% Optionally, QoS 0 messages pending transmission to the Client. + pending_queue :: emqttd_mqueue:mqueue(), + + %% Awaiting timers for ack, rel and comp. + awaiting_ack :: map(), + + awaiting_rel :: map(), + awaiting_comp :: map(), - expires, - expire_timer}). + + %% Retries to resend the unacked messages + max_unack_retries = 3, + + %% 4, 8, 16 seconds if 3 retries:) + unack_retry_after = 4, + + %% session expired + sess_expired_after = 48, + + sess_expired_timer, + + timestamp}). -type session() :: #session_state{} | pid(). @@ -235,11 +270,13 @@ store(SessState = #session_state{message_id = MsgId, awaiting_ack = Awaiting}, {Message1, next_msg_id(SessState#session_state{awaiting_ack = Awaiting1})}. initial_state(ClientId) -> - #session_state{clientid = ClientId, - submap = #{}, - awaiting_ack = #{}, - awaiting_rel = #{}, - awaiting_comp = #{}}. + #session_state{clientid = ClientId, + subscriptions = [], + inflight_queue = [], + awaiting_queue = [], + awaiting_ack = #{}, + awaiting_rel = #{}, + awaiting_comp = #{}}. initial_state(ClientId, ClientPid) -> State = initial_state(ClientId), @@ -258,16 +295,36 @@ start_link(ClientId, ClientPid) -> init([ClientId, ClientPid]) -> process_flag(trap_exit, true), - %%TODO: Is this OK? or should monitor... true = link(ClientPid), - SessOpts = emqttd:env(mqtt, session), State = initial_state(ClientId, ClientPid), - Expires = proplists:get_value(expires, SessOpts, 1) * 3600, - MsgQueue = emqttd_queue:new(proplists:get_value(max_queue, SessOpts, 1000), - proplists:get_value(store_qos0, SessOpts, false)), - {ok, State#session_state{expires = Expires, - msg_queue = MsgQueue}, hibernate}. + MQueue = emqttd_mqueue:new(ClientId, emqttd:env(mqtt, queue)), + State1 = State#session_state{pending_queue = MQueue, + timestamp = os:timestamp()}, + {ok, init(emqttd:env(mqtt, session), State1), hibernate}. +init([], State) -> + State; + +%% Session expired after hours +init([{expired_after, Hours} | Opts], State) -> + init(Opts, State#session_state{sess_expired_after = Hours * 3600 * 1000}); + +%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. +init([{max_inflight_messages, MaxInflight} | Opts], State) -> + init(Opts, State#session_state{inflight_window = MaxInflight}); + +%% Max retries for unacknolege Qos1/2 messages +init([{max_unack_retries, Retries} | Opts], State) -> + init(Opts, State#session_state{max_unack_retries = Retries}); + +%% Retry after 4, 8, 16 seconds +init([{unack_retry_after, Secs} | Opts], State) -> + init(Opts, State#session_state{unack_retry_after = Secs * 1000}); + +init([Opt | Opts], State) -> + lager:error("Bad Session Option: ~p", [Opt]), + init(Opts, State). + handle_call({subscribe, Topics}, _From, State) -> {ok, NewState, GrantedQos} = subscribe(State, Topics), {reply, {ok, GrantedQos}, NewState}; diff --git a/apps/zenmq/README b/apps/zenmq/README new file mode 100644 index 000000000..5b60d8891 --- /dev/null +++ b/apps/zenmq/README @@ -0,0 +1,14 @@ +## Overview + +ZenMQ is a general architecture of a distributed messaging queue written in Erlang. + +## Responsibilties + +* Topic Trie Tree +* Message Route +* Queue Management +* Broker Cluster +* Distributed Broker + +**Notice that this is an experimental design** + diff --git a/apps/zenmq/src/zenmq.app.src b/apps/zenmq/src/zenmq.app.src new file mode 100644 index 000000000..2d191d048 --- /dev/null +++ b/apps/zenmq/src/zenmq.app.src @@ -0,0 +1,12 @@ +{application, zenmq, + [ + {description, ""}, + {vsn, "1"}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {mod, { zenmq_app, []}}, + {env, []} + ]}. diff --git a/apps/zenmq/src/zenmq.erl b/apps/zenmq/src/zenmq.erl new file mode 100644 index 000000000..1e96b16f5 --- /dev/null +++ b/apps/zenmq/src/zenmq.erl @@ -0,0 +1,2 @@ +-module(zenmq). + diff --git a/apps/zenmq/src/zenmq_app.erl b/apps/zenmq/src/zenmq_app.erl new file mode 100644 index 000000000..15200771a --- /dev/null +++ b/apps/zenmq/src/zenmq_app.erl @@ -0,0 +1,16 @@ +-module(zenmq_app). + +-behaviour(application). + +%% Application callbacks +-export([start/2, stop/1]). + +%% =================================================================== +%% Application callbacks +%% =================================================================== + +start(_StartType, _StartArgs) -> + zenmq_sup:start_link(). + +stop(_State) -> + ok. diff --git a/apps/zenmq/src/zenmq_sup.erl b/apps/zenmq/src/zenmq_sup.erl new file mode 100644 index 000000000..f626a12b1 --- /dev/null +++ b/apps/zenmq/src/zenmq_sup.erl @@ -0,0 +1,27 @@ +-module(zenmq_sup). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). + +%% Supervisor callbacks +-export([init/1]). + +%% Helper macro for declaring children of supervisor +-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). + +%% =================================================================== +%% API functions +%% =================================================================== + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +init([]) -> + {ok, { {one_for_one, 5, 10}, []} }. + diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 442c801d7..fc15fb0a2 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -93,8 +93,8 @@ {max_inflight_messages, 20}, %% Max retries for unacknolege Qos1/2 messages {max_unack_retries, 3}, - %% Retry after 10 seconds - {unack_retry_after, 10} + %% Retry after 2, 4,8 seconds + {unack_retry_after, 2} ]}, {queue, [ %% Max messages queued when client is disconnected, or inflight messsages is overload diff --git a/rel/reltool.config b/rel/reltool.config index f26654f74..2dbb76ea5 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -4,7 +4,7 @@ {lib_dirs, ["../apps", "../deps", "../plugins"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.8.3", + {rel, "emqttd", git, [ kernel, stdlib, From f69b6270fbd4f157e244be467f595a6dcc97cf23 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 11 Jun 2015 12:08:18 +0800 Subject: [PATCH 057/104] zenmq --- apps/zenmq/README | 2 +- apps/zenmq/src/zenmq_router.erl | 4 ++++ rel/files/emqttd.config | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 apps/zenmq/src/zenmq_router.erl diff --git a/apps/zenmq/README b/apps/zenmq/README index 5b60d8891..e91132732 100644 --- a/apps/zenmq/README +++ b/apps/zenmq/README @@ -1,6 +1,6 @@ ## Overview -ZenMQ is a general architecture of a distributed messaging queue written in Erlang. +ZenMQ is the core architecture of distributed pubsub messaging queue written in Erlang. ## Responsibilties diff --git a/apps/zenmq/src/zenmq_router.erl b/apps/zenmq/src/zenmq_router.erl new file mode 100644 index 000000000..ecd6511e0 --- /dev/null +++ b/apps/zenmq/src/zenmq_router.erl @@ -0,0 +1,4 @@ + +-module(zenmq_router). + + diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index fc15fb0a2..ca11109e4 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -90,15 +90,15 @@ %% Expired after 2 days {expired_after, 48}, %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - {max_inflight_messages, 20}, + {max_inflight_messages, 40}, %% Max retries for unacknolege Qos1/2 messages {max_unack_retries, 3}, - %% Retry after 2, 4,8 seconds - {unack_retry_after, 2} + %% Retry after 4, 8, 16 seconds + {unack_retry_after, 4} ]}, {queue, [ %% Max messages queued when client is disconnected, or inflight messsages is overload - {max_queued_messages, 100}, + {max_queued_messages, 200}, %% High watermark of queued messsages {high_queue_watermark, 0.6}, %% Low watermark of queued messsages From 4313ed0cf37e98545f851a99112b4204d72a2fa2 Mon Sep 17 00:00:00 2001 From: Feng Date: Thu, 11 Jun 2015 13:57:00 +0800 Subject: [PATCH 058/104] comment --- apps/emqttd/src/emqttd_session.erl | 55 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 313b8eb3d..8350b89b8 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -69,15 +69,21 @@ %% Inflight window size inflight_window = 40, - %% Inflight qos1, qos2 messages sent to the client but unacked, QoS 1 and QoS 2 messages which have been sent to the Client, but have not been completely acknowledged. + %% Inflight qos1, qos2 messages sent to the client but unacked, + %% QoS 1 and QoS 2 messages which have been sent to the Client, + %% but have not been completely acknowledged. %% Client <- Broker inflight_queue :: list(), - %% Inflight qos2 messages received from client and waiting for pubrel. QoS 2 messages which have been received from the Client, but have not been completely acknowledged. + %% Inflight qos2 messages received from client and waiting for pubrel. + %% QoS 2 messages which have been received from the Client, + %% but have not been completely acknowledged. %% Client -> Broker awaiting_queue :: list(), - %% All qos1, qos2 messages published to when client is disconnected. QoS 1 and QoS 2 messages pending transmission to the Client. + %% All qos1, qos2 messages published to when client is disconnected. + %% QoS 1 and QoS 2 messages pending transmission to the Client. + %% %% Optionally, QoS 0 messages pending transmission to the Client. pending_queue :: emqttd_mqueue:mqueue(), @@ -209,33 +215,34 @@ puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> %% @end %%------------------------------------------------------------------------------ -spec subscribe(session(), [{binary(), mqtt_qos()}]) -> {ok, session(), [mqtt_qos()]}. -subscribe(SessState = #session_state{clientid = ClientId, submap = SubMap}, Topics) -> +subscribe(SessState = #session_state{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %% subscribe first and don't care if the subscriptions have been existed {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), lager:info([{client, ClientId}], "Client ~s subscribe ~p. Granted QoS: ~p", - [ClientId, Topics, GrantedQos]), + [ClientId, Topics, GrantedQos]), + Subscriptions1 = + lists:foldl(fun({Topic, Qos}, Acc) -> + case lists:keyfind(Topic, 1, Acc) of + {Topic, Qos} -> + lager:warning([{client, ClientId}], "~s resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc; + {Topic, Old} -> + lager:warning([{client, ClientId}], "~s resubscribe ~p: old qos=~p, new qos=~p", + [ClientId, Topic, Old, Qos]), + lists:keyreplace(Topic, 1, Acc, {Topic, Qos}); + false -> + %%TODO: the design is ugly, rewrite later...:( + %% : 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_msg_store:redeliver(Topic, self()), + [{Topic, Qos} | Acc] + end + end, Subscriptions, Topics), - %% : 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. - lists:foreach(fun({Name, _Qos}) -> - case maps:is_key(Name, SubMap) of - true -> - lager:warning("~s resubscribe ~p", [ClientId, Name]); - false -> - %%TODO: this is not right, rewrite later... - emqttd_msg_store:redeliver(Name, self()) - end - end, Topics), - - SubMap1 = lists:foldl(fun({Name, Qos}, Acc) -> - maps:put(Name, Qos, Acc) - end, SubMap, Topics), - - {ok, SessState#session_state{submap = SubMap1}, GrantedQos}; + {ok, SessState#session_state{subscriptions = Subscriptions1}, GrantedQos}; subscribe(SessPid, Topics) when is_pid(SessPid) -> {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), @@ -246,7 +253,7 @@ subscribe(SessPid, Topics) when is_pid(SessPid) -> %% @end %%------------------------------------------------------------------------------ -spec unsubscribe(session(), [binary()]) -> {ok, session()}. -unsubscribe(SessState = #session_state{clientid = ClientId, submap = SubMap}, Topics) -> +unsubscribe(SessState = #session_state{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %%TODO: refactor later. case Topics -- maps:keys(SubMap) of [] -> ok; From 517c7eb7b6b7c5826f980a3e869279a6d44ceb4c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 23:34:53 +0800 Subject: [PATCH 059/104] session upgrade --- apps/emqttd/src/emqttd_protocol.erl | 2 +- apps/emqttd/src/emqttd_session.erl | 205 ++++++++++++++++++---------- rel/files/emqttd.config | 4 +- 3 files changed, 134 insertions(+), 77 deletions(-) diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index d519a182e..37e19f16e 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -272,7 +272,7 @@ send({_From = SessPid, Message}, State = #proto_state{session = SessPid}) when i %% message(qos1, qos2) not from session send({_From, Message = #mqtt_message{qos = Qos}}, State = #proto_state{session = Session}) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - {Message1, NewSession} = emqttd_session:store(Session, Message), + {Message1, NewSession} = emqttd_session:await_ack(Session, Message), send(emqtt_message:to_packet(Message1), State#proto_state{session = NewSession}); send(Packet, State = #proto_state{sendfun = SendFun, peername = Peername}) when is_record(Packet, mqtt_packet) -> diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 8350b89b8..6b6d961a4 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -24,6 +24,7 @@ %%% %%% @end %%%----------------------------------------------------------------------------- + -module(emqttd_session). -author("Feng Lee "). @@ -53,7 +54,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(session_state, { +-record(session, { %% ClientId: Identifier of Session clientid :: binary(), @@ -100,14 +101,17 @@ %% 4, 8, 16 seconds if 3 retries:) unack_retry_after = 4, - %% session expired - sess_expired_after = 48, + %% Awaiting PUBREL timeout + await_rel_timeout = 8, + + %% session expired after 48 hours + sess_expired_after = 172800, sess_expired_timer, - timestamp}). + timestamp }). --type session() :: #session_state{} | pid(). +-type session() :: #session{} | pid(). %%%============================================================================= %%% Session API @@ -132,7 +136,7 @@ start({false = _CleanSess, ClientId, ClientPid}) -> %% @end %%------------------------------------------------------------------------------ -spec resume(session(), binary(), pid()) -> session(). -resume(SessState = #session_state{}, _ClientId, _ClientPid) -> +resume(SessState = #session{}, _ClientId, _ClientPid) -> SessState; resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> gen_server:cast(SessPid, {resume, ClientId, ClientPid}), @@ -149,10 +153,12 @@ publish(Session, ClientId, {?QOS_0, Message}) -> publish(Session, ClientId, {?QOS_1, Message}) -> emqttd_pubsub:publish(ClientId, Message), Session; -publish(SessState = #session_state{awaiting_rel = AwaitingRel}, _ClientId, +publish(SessState = #session{awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}, _ClientId, {?QOS_2, Message = #mqtt_message{msgid = MsgId}}) -> %% store in awaiting_rel - SessState#session_state{awaiting_rel = maps:put(MsgId, Message, AwaitingRel)}; + TRef = erlang:send_after(Timeout * 1000, self(), {timeout, awaiting_rel, MsgId}), + SessState#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}; publish(SessPid, ClientId, {?QOS_2, Message}) when is_pid(SessPid) -> gen_server:cast(SessPid, {publish, ClientId, {?QOS_2, Message}}), @@ -163,59 +169,72 @@ publish(SessPid, ClientId, {?QOS_2, Message}) when is_pid(SessPid) -> %% @end %%------------------------------------------------------------------------------ -spec puback(session(), {mqtt_packet_type(), mqtt_packet_id()}) -> session(). -puback(SessState = #session_state{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> +puback(SessState = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> case maps:is_key(PacketId, Awaiting) of true -> ok; false -> lager:warning("Session ~s: PUBACK PacketId '~p' not found!", [ClientId, PacketId]) end, - SessState#session_state{awaiting_ack = maps:remove(PacketId, Awaiting)}; + SessState#session{awaiting_ack = maps:remove(PacketId, Awaiting)}; puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> gen_server:cast(SessPid, {puback, PacketId}), SessPid; %% PUBREC -puback(SessState = #session_state{clientid = ClientId, +puback(SessState = #session{clientid = ClientId, awaiting_ack = AwaitingAck, awaiting_comp = AwaitingComp}, {?PUBREC, PacketId}) -> case maps:is_key(PacketId, AwaitingAck) of true -> ok; false -> lager:warning("Session ~s: PUBREC PacketId '~p' not found!", [ClientId, PacketId]) end, - SessState#session_state{awaiting_ack = maps:remove(PacketId, AwaitingAck), - awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; + SessState#session{awaiting_ack = maps:remove(PacketId, AwaitingAck), + awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> gen_server:cast(SessPid, {pubrec, PacketId}), SessPid; %% PUBREL -puback(SessState = #session_state{clientid = ClientId, - awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> +puback(SessState = #session{clientid = ClientId, + awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> case maps:find(PacketId, Awaiting) of - {ok, Msg} -> emqttd_pubsub:publish(ClientId, Msg); - error -> lager:warning("Session ~s: PUBREL PacketId '~p' not found!", [ClientId, PacketId]) + {ok, {Msg, TRef}} -> + catch erlang:cancel_timer(TRef), + emqttd_pubsub:publish(ClientId, Msg); + error -> + lager:error("Session ~s PUBREL PacketId '~p' not found!", [ClientId, PacketId]) end, - SessState#session_state{awaiting_rel = maps:remove(PacketId, Awaiting)}; + SessState#session{awaiting_rel = maps:remove(PacketId, Awaiting)}; puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> gen_server:cast(SessPid, {pubrel, PacketId}), SessPid; %% PUBCOMP -puback(SessState = #session_state{clientid = ClientId, +puback(SessState = #session{clientid = ClientId, awaiting_comp = AwaitingComp}, {?PUBCOMP, PacketId}) -> case maps:is_key(PacketId, AwaitingComp) of true -> ok; false -> lager:warning("Session ~s: PUBREC PacketId '~p' not exist", [ClientId, PacketId]) end, - SessState#session_state{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; + SessState#session{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> gen_server:cast(SessPid, {pubcomp, PacketId}), SessPid. +timeout(awaiting_rel, MsgId, SessState = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, {Msg, _TRef}} -> + lager:error([{client, ClientId}], "Session ~s Awaiting Rel Timout!~nDrop Message:~p", [ClientId, Msg]), + SessState#session{awaiting_rel = maps:remove(MsgId, Awaiting)}; + error -> + lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting Rel: MsgId=~p", [ClientId, MsgId]), + SessState + end. + %%------------------------------------------------------------------------------ %% @doc Subscribe Topics %% @end %%------------------------------------------------------------------------------ -spec subscribe(session(), [{binary(), mqtt_qos()}]) -> {ok, session(), [mqtt_qos()]}. -subscribe(SessState = #session_state{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> +subscribe(SessState = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %% subscribe first and don't care if the subscriptions have been existed {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), @@ -242,7 +261,7 @@ subscribe(SessState = #session_state{clientid = ClientId, subscriptions = Subscr end end, Subscriptions, Topics), - {ok, SessState#session_state{subscriptions = Subscriptions1}, GrantedQos}; + {ok, SessState#session{subscriptions = Subscriptions1}, GrantedQos}; subscribe(SessPid, Topics) when is_pid(SessPid) -> {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), @@ -253,17 +272,23 @@ subscribe(SessPid, Topics) when is_pid(SessPid) -> %% @end %%------------------------------------------------------------------------------ -spec unsubscribe(session(), [binary()]) -> {ok, session()}. -unsubscribe(SessState = #session_state{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> - %%TODO: refactor later. - case Topics -- maps:keys(SubMap) of - [] -> ok; - BadUnsubs -> lager:warning("~s should not unsubscribe ~p", [ClientId, BadUnsubs]) - end, +unsubscribe(SessState = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> + %%unsubscribe from topic tree ok = emqttd_pubsub:unsubscribe(Topics), lager:info([{client, ClientId}], "Client ~s unsubscribe ~p.", [ClientId, Topics]), - SubMap1 = lists:foldl(fun(Topic, Acc) -> maps:remove(Topic, Acc) end, SubMap, Topics), - {ok, SessState#session_state{submap = SubMap1}}; + + Subscriptions1 = + lists:foldl(fun(Topic, Acc) -> + case lists:keyfind(Topic, 1, Acc) of + {Topic, _Qos} -> + lists:keydelete(Topic, 1, Acc); + false -> + lager:warning([{client, ClientId}], "~s not subscribe ~s", [ClientId, Topic]), Acc + end + end, Subscriptions, Topics), + + {ok, SessState#session{subscriptions = Subscriptions1}}; unsubscribe(SessPid, Topics) when is_pid(SessPid) -> gen_server:call(SessPid, {unsubscribe, Topics}), @@ -277,31 +302,45 @@ unsubscribe(SessPid, Topics) when is_pid(SessPid) -> destroy(SessPid, ClientId) when is_pid(SessPid) -> gen_server:cast(SessPid, {destroy, ClientId}). -%store message(qos1) that sent to client -store(SessState = #session_state{message_id = MsgId, awaiting_ack = Awaiting}, - Message = #mqtt_message{qos = Qos}) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> +% message(qos1) is awaiting ack +await_ack(Msg = #mqtt_message{qos = ?QOS_1}, SessState = #session{message_id = MsgId, + inflight_queue = InflightQ, + awaiting_ack = Awaiting, + unack_retry_after = Time, + max_unack_retries = Retries}) -> + %% assign msgid before send + Msg1 = Msg#mqtt_message{msgid = MsgId}, + TRef = erlang:send_after(Time * 1000, self(), {retry, MsgId}), + Awaiting1 = maps:put(MsgId, {TRef, Retries, Time}, Awaiting), + {Msg1, next_msgid(SessState#session{inflight_queue = [{MsgId, Msg1} | InflightQ], + awaiting_ack = Awaiting1})}. + +% message(qos2) is awaiting ack +await_ack(Message = #mqtt_message{qos = Qos}, SessState = #session{message_id = MsgId, awaiting_ack = Awaiting},) + when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> %%assign msgid before send - Message1 = Message#mqtt_message{msgid = MsgId}, + Message1 = Message#mqtt_message{msgid = MsgId, dup = false}, Message2 = if Qos =:= ?QOS_2 -> Message1#mqtt_message{dup = false}; true -> Message1 end, Awaiting1 = maps:put(MsgId, Message2, Awaiting), - {Message1, next_msg_id(SessState#session_state{awaiting_ack = Awaiting1})}. + {Message1, next_msgid(SessState#session{awaiting_ack = Awaiting1})}. initial_state(ClientId) -> - #session_state{clientid = ClientId, - subscriptions = [], - inflight_queue = [], - awaiting_queue = [], - awaiting_ack = #{}, - awaiting_rel = #{}, - awaiting_comp = #{}}. + %%TODO: init session options. + #session{clientid = ClientId, + subscriptions = [], + inflight_queue = [], + awaiting_queue = [], + awaiting_ack = #{}, + awaiting_rel = #{}, + awaiting_comp = #{}}. initial_state(ClientId, ClientPid) -> State = initial_state(ClientId), - State#session_state{client_pid = ClientPid}. + State#session{client_pid = ClientPid}. %%------------------------------------------------------------------------------ %% @doc Start a session process. @@ -319,7 +358,7 @@ init([ClientId, ClientPid]) -> true = link(ClientPid), State = initial_state(ClientId, ClientPid), MQueue = emqttd_mqueue:new(ClientId, emqttd:env(mqtt, queue)), - State1 = State#session_state{pending_queue = MQueue, + State1 = State#session{pending_queue = MQueue, timestamp = os:timestamp()}, {ok, init(emqttd:env(mqtt, session), State1), hibernate}. @@ -328,19 +367,23 @@ init([], State) -> %% Session expired after hours init([{expired_after, Hours} | Opts], State) -> - init(Opts, State#session_state{sess_expired_after = Hours * 3600 * 1000}); + init(Opts, State#session{sess_expired_after = Hours * 3600}); %% Max number of QoS 1 and 2 messages that can be “inflight” at one time. init([{max_inflight_messages, MaxInflight} | Opts], State) -> - init(Opts, State#session_state{inflight_window = MaxInflight}); + init(Opts, State#session{inflight_window = MaxInflight}); %% Max retries for unacknolege Qos1/2 messages init([{max_unack_retries, Retries} | Opts], State) -> - init(Opts, State#session_state{max_unack_retries = Retries}); + init(Opts, State#session{max_unack_retries = Retries}); %% Retry after 4, 8, 16 seconds init([{unack_retry_after, Secs} | Opts], State) -> - init(Opts, State#session_state{unack_retry_after = Secs * 1000}); + init(Opts, State#session{unack_retry_after = Secs}); + +%% Awaiting PUBREL timeout +init([{await_rel_timeout, Secs} | Opts], State) -> + init(Opts, State#session{await_rel_timeout = Secs}); init([Opt | Opts], State) -> lager:error("Bad Session Option: ~p", [Opt]), @@ -358,7 +401,7 @@ handle_call(Req, _From, State) -> lager:error("Unexpected request: ~p", [Req]), {reply, error, State}. -handle_cast({resume, ClientId, ClientPid}, State = #session_state{ +handle_cast({resume, ClientId, ClientPid}, State = #session{ clientid = ClientId, client_pid = OldClientPid, msg_queue = Queue, @@ -399,7 +442,7 @@ handle_cast({resume, ClientId, ClientPid}, State = #session_state{ ClientPid ! {dispatch, {self(), Msg}} end, emqttd_queue:all(Queue)), - {noreply, State#session_state{client_pid = ClientPid, + {noreply, State#session{client_pid = ClientPid, msg_queue = emqttd_queue:clear(Queue), expire_timer = undefined}, hibernate}; @@ -423,7 +466,7 @@ handle_cast({pubcomp, PacketId}, State) -> NewState = puback(State, {?PUBCOMP, PacketId}), {noreply, NewState}; -handle_cast({destroy, ClientId}, State = #session_state{clientid = ClientId}) -> +handle_cast({destroy, ClientId}, State = #session{clientid = ClientId}) -> lager:warning("Session ~s destroyed", [ClientId]), {stop, normal, State}; @@ -438,19 +481,23 @@ handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> handle_info({dispatch, {_From, Message}}, State) -> {noreply, dispatch(Message, State)}; -handle_info({'EXIT', ClientPid, Reason}, State = #session_state{clientid = ClientId, +handle_info({'EXIT', ClientPid, Reason}, State = #session{clientid = ClientId, client_pid = ClientPid}) -> lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), - {noreply, start_expire_timer(State#session_state{client_pid = undefined})}; + {noreply, start_expire_timer(State#session{client_pid = undefined})}; -handle_info({'EXIT', ClientPid0, _Reason}, State = #session_state{client_pid = ClientPid}) -> +handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), {noreply, State}; -handle_info(session_expired, State = #session_state{clientid = ClientId}) -> +handle_info(session_expired, State = #session{clientid = ClientId}) -> lager:warning("Session ~s expired!", [ClientId]), {stop, {shutdown, expired}, State}; +handle_info({timeout, awaiting_rel, MsgId}, SessState) -> + NewState = timeout(awaiting_rel, MsgId, SessState), + {noreply, NewState}; + handle_info(Info, State) -> lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), {noreply, State}. @@ -465,32 +512,40 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= -dispatch(Message, State = #session_state{clientid = ClientId, - client_pid = undefined}) -> - queue(ClientId, Message, State); +%% client is offline +dispatch(Msg, SessState = #session{client_pid = undefined}) -> + queue(Msg, SessState); -dispatch(Message = #mqtt_message{qos = ?QOS_0}, State = #session_state{client_pid = ClientPid}) -> - ClientPid ! {dispatch, {self(), Message}}, - State; +%% dispatch qos0 directly +dispatch(Msg = #mqtt_message{qos = ?QOS_0}, SessState = #session{client_pid = ClientPid}) -> + ClientPid ! {dispatch, {self(), Msg}}, SessState; -dispatch(Message = #mqtt_message{qos = Qos}, State = #session_state{client_pid = ClientPid}) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - {Message1, NewState} = store(State, Message), - ClientPid ! {dispatch, {self(), Message1}}, - NewState. +%% queue if inflight_queue is full +dispatch(Msg = #mqtt_message{qos = Qos}, SessState = #session{inflight_window = InflightWin, + inflight_queue = InflightQ}) + when (Qos > ?QOS_0) andalso (length(InflightQ) >= InflightWin) -> + %%TODO: set alarms + lager:error([{clientid, ClientId}], "Session ~s inflight_queue is full!", [ClientId]), + queue(Msg, SessState); -queue(ClientId, Message, State = #session_state{msg_queue = Queue}) -> - State#session_state{msg_queue = emqttd_queue:in(ClientId, Message, Queue)}. +%% dispatch and await ack +dispatch(Msg = #mqtt_message{qos = Qos}, SessState = #session{client_pid = ClientPid}) + when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> + %% assign msgid and await + {NewMsg, NewState} = await_ack(Msg, SessState), + ClientPid ! {dispatch, {self(), NewMsg}}, -next_msg_id(State = #session_state{message_id = 16#ffff}) -> - State#session_state{message_id = 1}; +queue(Msg, SessState = #session{pending_queue = Queue}) -> + SessState#session{pending_queue = emqttd_mqueue:in(Msg, Queue)}. -next_msg_id(State = #session_state{message_id = MsgId}) -> - State#session_state{message_id = MsgId + 1}. +next_msgid(State = #session{message_id = 16#ffff}) -> + State#session{message_id = 1}; -start_expire_timer(State = #session_state{expires = Expires, - expire_timer = OldTimer}) -> +next_msgid(State = #session{message_id = MsgId}) -> + State#session{message_id = MsgId + 1}. + +start_expire_timer(State = #session{expires = Expires, expire_timer = OldTimer}) -> emqttd_util:cancel_timer(OldTimer), Timer = erlang:send_after(Expires * 1000, self(), session_expired), - State#session_state{expire_timer = Timer}. + State#session{expire_timer = Timer}. diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 5de150b4b..da32fbfc9 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -94,7 +94,9 @@ %% Max retries for unacknolege Qos1/2 messages {max_unack_retries, 3}, %% Retry after 4, 8, 16 seconds - {unack_retry_after, 4} + {unack_retry_after, 4}, + %% Awaiting PUBREL timeout + {await_rel_timeout, 8} ]}, {queue, [ %% Max messages queued when client is disconnected, or inflight messsage window is overload From 04c2772859f96aa3c6bbe2534900f28671551504 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Thu, 11 Jun 2015 23:40:57 +0800 Subject: [PATCH 060/104] seperate session api and process --- apps/emqttd/src/emqttd_session_proc.erl | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/emqttd/src/emqttd_session_proc.erl diff --git a/apps/emqttd/src/emqttd_session_proc.erl b/apps/emqttd/src/emqttd_session_proc.erl new file mode 100644 index 000000000..5f08b68af --- /dev/null +++ b/apps/emqttd/src/emqttd_session_proc.erl @@ -0,0 +1,29 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% emqttd session process. +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +-module(emqttd_session_proc). + From 882fbb83ca33c4cc41f6d71868454688eb073b1b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 12 Jun 2015 10:37:30 +0800 Subject: [PATCH 061/104] out --- apps/emqttd/src/emqttd_queue.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_queue.erl b/apps/emqttd/src/emqttd_queue.erl index d9897a3f8..1ca068a13 100644 --- a/apps/emqttd/src/emqttd_queue.erl +++ b/apps/emqttd/src/emqttd_queue.erl @@ -72,7 +72,7 @@ in(ClientId, Message = #mqtt_message{qos = Qos}, lager:error("Queue ~s drop qos0 message: ~p", [ClientId, Message]), Wrapper; true -> - {{value, Msg}, Queue1} = queue:drop(Queue), + {{value, Msg}, Queue1} = queue:out(Queue), lager:error("Queue ~s drop message: ~p", [ClientId, Msg]), Wrapper#mqtt_queue_wrapper{queue = queue:in(Message, Queue1)} end From ddf831f36156b95ffa8726bda160e9110d4d935f Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 12 Jun 2015 17:24:08 +0800 Subject: [PATCH 062/104] session --- apps/emqttd/src/emqttd_mqueue.erl | 143 ++++++++-------- apps/emqttd/src/emqttd_mqwin.erl | 66 ++++++++ apps/emqttd/src/emqttd_session.erl | 215 +++--------------------- apps/emqttd/src/emqttd_session_proc.erl | 183 ++++++++++++++++++++ rel/files/emqttd.config | 22 ++- 5 files changed, 359 insertions(+), 270 deletions(-) create mode 100644 apps/emqttd/src/emqttd_mqwin.erl diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index 874aa82d0..84381f036 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -20,15 +20,30 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% Simple message queue. +%%% +%%% A Simple in-memory message queue. %%% %%% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client %%% should be online in most of the time. %%% -%%% This module wraps an erlang queue to store offline messages temporarily for MQTT -%%% persistent session. +%%% This module implements a simple in-memory queue for MQTT persistent session. %%% -%%% If the broker restarted or crashed, all the messages stored will be gone. +%%% If the broker restarted or crashed, all the messages queued will be gone. +%%% +%%% Desgin of The Queue: +%%% |<----------------- Max Len ----------------->| +%%% ----------------------------------------------- +%%% IN -> | Pending Messages | Inflight Window | -> Out +%%% ----------------------------------------------- +%%% |<--- Win Size --->| +%%% +%%% +%%% 1. Inflight Window to store the messages awaiting for ack. +%%% +%%% 2. Suspend IN messages when the queue is deactive, or inflight windows is full. +%%% +%%% 3. If the queue is full, dropped qos0 messages if store_qos0 is true, +%%% otherwise dropped the oldest pending one. %%% %%% @end %%%----------------------------------------------------------------------------- @@ -40,104 +55,98 @@ -include_lib("emqtt/include/emqtt.hrl"). -export([new/2, name/1, - is_empty/1, len/1, - in/2, out/1, - peek/1, - to_list/1]). - --define(MAX_LEN, 600). - --define(HIGH_WM, 0.6). + is_empty/1, is_full/1, + len/1, in/2, out/2]). -define(LOW_WM, 0.2). +-define(HIGH_WM, 0.6). + +-define(MAX_LEN, 1000). + -record(mqueue, {name, - len = 0, - max_len = ?MAX_LEN, - queue = queue:new(), - store_qos0 = false, - high_watermark = ?HIGH_WM, - low_watermark = ?LOW_WM, - alert = false}). + q = queue:new(), %% pending queue + len = 0, %% current queue len + low_wm = ?LOW_WM, + high_wm = ?HIGH_WM, + max_len = ?MAX_LEN, + qos0 = false, + alarm = false}). -type mqueue() :: #mqueue{}. --type queue_option() :: {max_queued_messages, pos_integer()} %% Max messages queued - | {high_queue_watermark, float()} %% High watermark - | {low_queue_watermark, float()} %% Low watermark - | {queue_qos0_messages, boolean()}. %% Queue Qos0 messages? +-type mqueue_option() :: {max_length, pos_integer()} %% Max queue length + | {inflight_window, pos_integer()} %% Inflight Window + | {low_watermark, float()} %% Low watermark + | {high_watermark, float()} %% High watermark + | {queue_qos0, boolean()}. %% Queue Qos0 + +-export_type([mqueue/0]). %%------------------------------------------------------------------------------ %% @doc New Queue. %% @end %%------------------------------------------------------------------------------ --spec new(binary() | string(), list(queue_option())) -> mqueue(). +-spec new(binary(), list(mqueue_option())) -> mqueue(). new(Name, Opts) -> - MaxLen = emqttd_opts:g(max_queued_messages, Opts, ?MAX_LEN), - HighWM = round(MaxLen * emqttd_opts:g(high_queue_watermark, Opts, ?HIGH_WM)), - LowWM = round(MaxLen * emqttd_opts:g(low_queue_watermark, Opts, ?LOW_WM)), - StoreQos0 = emqttd_opts:g(queue_qos0_messages, Opts, false), - #mqueue{name = Name, - max_len = MaxLen, - store_qos0 = StoreQos0, - high_watermark = HighWM, - low_watermark = LowWM}. + MaxLen = emqttd_opts:g(max_length, Opts, ?MAX_LEN), + #mqueue{name = Name, + max_len = MaxLen, + low_wm = round(MaxLen * emqttd_opts:g(low_watermark, Opts, ?LOW_WM)), + high_wm = round(MaxLen * emqttd_opts:g(high_watermark, Opts, ?HIGH_WM)), + qos0 = emqttd_opts:g(queue_qos0, Opts, true)}. name(#mqueue{name = Name}) -> Name. -len(#mqueue{len = Len}) -> - Len. - is_empty(#mqueue{len = 0}) -> true; -is_empty(_Q) -> false. +is_empty(_MQ) -> false. + +is_full(#mqueue{len = Len, max_len = MaxLen}) + when Len =:= MaxLen -> true; +is_full(_MQ) -> false. + +len(#mqueue{len = Len}) -> Len. %%------------------------------------------------------------------------------ -%% @doc -%% Queue one message. -%% +%% @doc Queue one message. %% @end %%------------------------------------------------------------------------------ -spec in(mqtt_message(), mqueue()) -> mqueue(). -in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{store_qos0 = false}) -> + +%% drop qos0 +in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> MQ; -%% queue is full, drop the oldest -in(Msg, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen, queue = Q}) when Len =:= MaxLen -> - Q2 = case queue:out(Q) of - {{value, OldMsg}, Q1} -> - %%TODO: publish the dropped message to $SYS? - lager:error("Queue(~s) drop message: ~p", [Name, OldMsg]), - Q1; - {empty, Q1} -> %% maybe max_len is 1 - Q1 - end, - MQ#mqueue{queue = queue:in(Msg, Q2)}; -in(Msg, MQ = #mqueue{len = Len, queue = Q}) -> - maybe_set_alarm(MQ#mqueue{len = Len+1, queue = queue:in(Msg, Q)}). -out(MQ = #mqueue{len = 0, queue = _Q}) -> +%% simply drop the oldest one if queue is full, improve later +in(Msg, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen}) + when Len =:= MaxLen -> + {{value, OldMsg}, Q2} = queue:out(Q), + lager:error("queue(~s) drop message: ~p", [Name, OldMsg]), + MQ#mqueue{q = queue:in(Msg, Q2)}; + +in(Msg, MQ = #mqueue{q = Q, len = Len}) -> + maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); + +out(MQ = #mqueue{len = 0}) -> {empty, MQ}; -out(MQ = #mqueue{len = Len, queue = Q}) -> - {Result, Q1} = queue:out(Q), - {Result, maybe_clear_alarm(MQ#mqueue{len = Len - 1, queue = Q1})}. -peek(#mqueue{queue = Q}) -> - queue:peek(Q). +out(MQ = #mqueue{q = Q, len = Len}) -> + {Result, Q2} = queue:out(Q), + {Result, maybe_clear_alarm(MQ#mqueue{q = Q2, len = Len - 1})}. -to_list(#mqueue{queue = Q}) -> - queue:to_list(Q). - -maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_watermark = HighWM, alert = false}) +maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm = false}) when Len >= HighWM -> AlarmDescr = io_lib:format("len ~p > high_watermark ~p", [Len, HighWM]), emqttd_alarm:set_alarm({{queue_high_watermark, Name}, AlarmDescr}), - MQ#mqueue{alert = true}; + MQ#mqueue{alarm = true}; maybe_set_alarm(MQ) -> MQ. -maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_watermark = LowWM, alert = true}) +maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_watermark = LowWM, alarm = true}) when Len =< LowWM -> - emqttd_alarm:clear_alarm({queue_high_watermark, Name}), MQ#mqueue{alert = false}; + emqttd_alarm:clear_alarm({queue_high_watermark, Name}), + MQ#mqueue{alarm = false}; maybe_clear_alarm(MQ) -> MQ. diff --git a/apps/emqttd/src/emqttd_mqwin.erl b/apps/emqttd/src/emqttd_mqwin.erl new file mode 100644 index 000000000..acc90bf41 --- /dev/null +++ b/apps/emqttd/src/emqttd_mqwin.erl @@ -0,0 +1,66 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- +%%% @doc +%%% Inflight window of message queue. Wrap a list with len. +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +-module(emqttd_mqwin). + +-author("Feng Lee "). + +-export([new/2, len/1, in/2, ack/2]). + +-define(WIN_SIZE, 100). + +-record(mqwin, {name, + w = [], %% window list + len = 0, %% current window len + size = ?WIN_SIZE}). + +-type mqwin() :: #mqwin{}. + +-export_type([mqwin/0]). + +new(Name, Opts) -> + WinSize = emqttd_opts:g(inflight_window, Opts, ?WIN_SIZE), + #mqwin{name = Name, size = WinSize}. + +len(#mqwin{len = Len}) -> + Len. + +in(_Msg, #mqwin{len = Len, size = Size}) + when Len =:= Size -> {error, full}; + +in(Msg, Win = #mqwin{w = W, len = Len}) -> + {ok, Win#mqwin{w = [Msg|W], len = Len +1}}. + +ack(MsgId, QWin = #mqwin{w = W, len = Len}) -> + case lists:keyfind(MsgId, 2, W) of + false -> + lager:error("qwin(~s) cannot find msgid: ~p", [MsgId]), QWin; + _Msg -> + QWin#mqwin{w = lists:keydelete(MsgId, 2, W), len = Len - 1} + end. + + diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 6b6d961a4..44bc76bb4 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -35,24 +35,20 @@ -include_lib("emqtt/include/emqtt_packet.hrl"). -%% API Function Exports +-define(SessProc, emqttd_session_proc). + +%% Session Managenent APIs -export([start/1, resume/3, - publish/3, + destroy/2]). + +%% PubSub APIs +-export([publish/3, puback/2, subscribe/2, unsubscribe/2, - destroy/2]). - -%% This api looks strange... :( --export([store/2]). - -%% Start gen_server --export([start_link/2]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). + await/2, + dispatch/2]). -record(session, { %% ClientId: Identifier of Session @@ -67,20 +63,11 @@ %% Client’s subscriptions. subscriptions :: list(), - %% Inflight window size - inflight_window = 40, - %% Inflight qos1, qos2 messages sent to the client but unacked, %% QoS 1 and QoS 2 messages which have been sent to the Client, %% but have not been completely acknowledged. %% Client <- Broker - inflight_queue :: list(), - - %% Inflight qos2 messages received from client and waiting for pubrel. - %% QoS 2 messages which have been received from the Client, - %% but have not been completely acknowledged. - %% Client -> Broker - awaiting_queue :: list(), + inflight_window :: emqttd_mqwin:mqwin(), %% All qos1, qos2 messages published to when client is disconnected. %% QoS 1 and QoS 2 messages pending transmission to the Client. @@ -88,18 +75,22 @@ %% Optionally, QoS 0 messages pending transmission to the Client. pending_queue :: emqttd_mqueue:mqueue(), + %% Inflight qos2 messages received from client and waiting for pubrel. + %% QoS 2 messages which have been received from the Client, + %% but have not been completely acknowledged. + %% Client -> Broker + awaiting_rel :: map(), + %% Awaiting timers for ack, rel and comp. awaiting_ack :: map(), - awaiting_rel :: map(), - awaiting_comp :: map(), %% Retries to resend the unacked messages - max_unack_retries = 3, + unack_retries = 3, %% 4, 8, 16 seconds if 3 retries:) - unack_retry_after = 4, + unack_timeout = 4, %% Awaiting PUBREL timeout await_rel_timeout = 8, @@ -109,9 +100,9 @@ sess_expired_timer, - timestamp }). + timestamp}). --type session() :: #session{} | pid(). +-type session() :: #session{}. %%%============================================================================= %%% Session API @@ -139,6 +130,7 @@ start({false = _CleanSess, ClientId, ClientPid}) -> resume(SessState = #session{}, _ClientId, _ClientPid) -> SessState; resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> + ?SessProc: gen_server:cast(SessPid, {resume, ClientId, ClientPid}), SessPid. @@ -342,171 +334,6 @@ initial_state(ClientId, ClientPid) -> State = initial_state(ClientId), State#session{client_pid = ClientPid}. -%%------------------------------------------------------------------------------ -%% @doc Start a session process. -%% @end -%%------------------------------------------------------------------------------ -start_link(ClientId, ClientPid) -> - gen_server:start_link(?MODULE, [ClientId, ClientPid], []). - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -init([ClientId, ClientPid]) -> - process_flag(trap_exit, true), - true = link(ClientPid), - State = initial_state(ClientId, ClientPid), - MQueue = emqttd_mqueue:new(ClientId, emqttd:env(mqtt, queue)), - State1 = State#session{pending_queue = MQueue, - timestamp = os:timestamp()}, - {ok, init(emqttd:env(mqtt, session), State1), hibernate}. - -init([], State) -> - State; - -%% Session expired after hours -init([{expired_after, Hours} | Opts], State) -> - init(Opts, State#session{sess_expired_after = Hours * 3600}); - -%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. -init([{max_inflight_messages, MaxInflight} | Opts], State) -> - init(Opts, State#session{inflight_window = MaxInflight}); - -%% Max retries for unacknolege Qos1/2 messages -init([{max_unack_retries, Retries} | Opts], State) -> - init(Opts, State#session{max_unack_retries = Retries}); - -%% Retry after 4, 8, 16 seconds -init([{unack_retry_after, Secs} | Opts], State) -> - init(Opts, State#session{unack_retry_after = Secs}); - -%% Awaiting PUBREL timeout -init([{await_rel_timeout, Secs} | Opts], State) -> - init(Opts, State#session{await_rel_timeout = Secs}); - -init([Opt | Opts], State) -> - lager:error("Bad Session Option: ~p", [Opt]), - init(Opts, State). - -handle_call({subscribe, Topics}, _From, State) -> - {ok, NewState, GrantedQos} = subscribe(State, Topics), - {reply, {ok, GrantedQos}, NewState}; - -handle_call({unsubscribe, Topics}, _From, State) -> - {ok, NewState} = unsubscribe(State, Topics), - {reply, ok, NewState}; - -handle_call(Req, _From, State) -> - lager:error("Unexpected request: ~p", [Req]), - {reply, error, State}. - -handle_cast({resume, ClientId, ClientPid}, State = #session{ - clientid = ClientId, - client_pid = OldClientPid, - msg_queue = Queue, - awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp, - expire_timer = ETimer}) -> - - lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), - - %% kick old client... - if - OldClientPid =:= undefined -> - ok; - OldClientPid =:= ClientPid -> - ok; - true -> - lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), - unlink(OldClientPid), - OldClientPid ! {stop, duplicate_id, ClientPid} - end, - - %% cancel timeout timer - emqttd_util:cancel_timer(ETimer), - - %% redelivery PUBREL - lists:foreach(fun(PacketId) -> - ClientPid ! {redeliver, {?PUBREL, PacketId}} - end, maps:keys(AwaitingComp)), - - %% redelivery messages that awaiting PUBACK or PUBREC - Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end, - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Dup(Msg)}} - end, maps:values(AwaitingAck)), - - %% send offline messages - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Msg}} - end, emqttd_queue:all(Queue)), - - {noreply, State#session{client_pid = ClientPid, - msg_queue = emqttd_queue:clear(Queue), - expire_timer = undefined}, hibernate}; - -handle_cast({publish, ClientId, {?QOS_2, Message}}, State) -> - NewState = publish(State, ClientId, {?QOS_2, Message}), - {noreply, NewState}; - -handle_cast({puback, PacketId}, State) -> - NewState = puback(State, {?PUBACK, PacketId}), - {noreply, NewState}; - -handle_cast({pubrec, PacketId}, State) -> - NewState = puback(State, {?PUBREC, PacketId}), - {noreply, NewState}; - -handle_cast({pubrel, PacketId}, State) -> - NewState = puback(State, {?PUBREL, PacketId}), - {noreply, NewState}; - -handle_cast({pubcomp, PacketId}, State) -> - NewState = puback(State, {?PUBCOMP, PacketId}), - {noreply, NewState}; - -handle_cast({destroy, ClientId}, State = #session{clientid = ClientId}) -> - lager:warning("Session ~s destroyed", [ClientId]), - {stop, normal, State}; - -handle_cast(Msg, State) -> - lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), - {noreply, State}. - -handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> - F = fun(Message, S) -> dispatch(Message, S) end, - {noreply, lists:foldl(F, State, Messages)}; - -handle_info({dispatch, {_From, Message}}, State) -> - {noreply, dispatch(Message, State)}; - -handle_info({'EXIT', ClientPid, Reason}, State = #session{clientid = ClientId, - client_pid = ClientPid}) -> - lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), - {noreply, start_expire_timer(State#session{client_pid = undefined})}; - -handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> - lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), - {noreply, State}; - -handle_info(session_expired, State = #session{clientid = ClientId}) -> - lager:warning("Session ~s expired!", [ClientId]), - {stop, {shutdown, expired}, State}; - -handle_info({timeout, awaiting_rel, MsgId}, SessState) -> - NewState = timeout(awaiting_rel, MsgId, SessState), - {noreply, NewState}; - -handle_info(Info, State) -> - lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. %%%============================================================================= %%% Internal functions diff --git a/apps/emqttd/src/emqttd_session_proc.erl b/apps/emqttd/src/emqttd_session_proc.erl index 5f08b68af..43b60b75e 100644 --- a/apps/emqttd/src/emqttd_session_proc.erl +++ b/apps/emqttd/src/emqttd_session_proc.erl @@ -27,3 +27,186 @@ -module(emqttd_session_proc). +-author("Feng Lee "). + +-include("emqttd.hrl"). + +-include_lib("emqtt/include/emqtt.hrl"). + +-include_lib("emqtt/include/emqtt_packet.hrl"). + +%% Start gen_server +-export([start_link/2]). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%%------------------------------------------------------------------------------ +%% @doc Start a session process. +%% @end +%%------------------------------------------------------------------------------ +start_link(ClientId, ClientPid) -> + gen_server:start_link(?MODULE, [ClientId, ClientPid], []). + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= + +init([ClientId, ClientPid]) -> + process_flag(trap_exit, true), + true = link(ClientPid), + State = initial_state(ClientId, ClientPid), + MQueue = emqttd_mqueue:new(ClientId, emqttd:env(mqtt, queue)), + State1 = State#session{pending_queue = MQueue, + timestamp = os:timestamp()}, + {ok, init(emqttd:env(mqtt, session), State1), hibernate}. + +init([], State) -> + State; + +%% Session expired after hours +init([{expired_after, Hours} | Opts], State) -> + init(Opts, State#session{sess_expired_after = Hours * 3600}); + +%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. +init([{max_inflight_messages, MaxInflight} | Opts], State) -> + init(Opts, State#session{inflight_window = MaxInflight}); + +%% Max retries for unacknolege Qos1/2 messages +init([{max_unack_retries, Retries} | Opts], State) -> + init(Opts, State#session{max_unack_retries = Retries}); + +%% Retry after 4, 8, 16 seconds +init([{unack_retry_after, Secs} | Opts], State) -> + init(Opts, State#session{unack_retry_after = Secs}); + +%% Awaiting PUBREL timeout +init([{await_rel_timeout, Secs} | Opts], State) -> + init(Opts, State#session{await_rel_timeout = Secs}); + +init([Opt | Opts], State) -> + lager:error("Bad Session Option: ~p", [Opt]), + init(Opts, State). + +handle_call({subscribe, Topics}, _From, State) -> + {ok, NewState, GrantedQos} = subscribe(State, Topics), + {reply, {ok, GrantedQos}, NewState}; + +handle_call({unsubscribe, Topics}, _From, State) -> + {ok, NewState} = unsubscribe(State, Topics), + {reply, ok, NewState}; + +handle_call(Req, _From, State) -> + lager:error("Unexpected request: ~p", [Req]), + {reply, error, State}. + +handle_cast({resume, ClientId, ClientPid}, State = #session{ + clientid = ClientId, + client_pid = OldClientPid, + msg_queue = Queue, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + expire_timer = ETimer}) -> + + lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), + + %% kick old client... + if + OldClientPid =:= undefined -> + ok; + OldClientPid =:= ClientPid -> + ok; + true -> + lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), + unlink(OldClientPid), + OldClientPid ! {stop, duplicate_id, ClientPid} + end, + + %% cancel timeout timer + emqttd_util:cancel_timer(ETimer), + + %% redelivery PUBREL + lists:foreach(fun(PacketId) -> + ClientPid ! {redeliver, {?PUBREL, PacketId}} + end, maps:keys(AwaitingComp)), + + %% redelivery messages that awaiting PUBACK or PUBREC + Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end, + lists:foreach(fun(Msg) -> + ClientPid ! {dispatch, {self(), Dup(Msg)}} + end, maps:values(AwaitingAck)), + + %% send offline messages + lists:foreach(fun(Msg) -> + ClientPid ! {dispatch, {self(), Msg}} + end, emqttd_queue:all(Queue)), + + {noreply, State#session{client_pid = ClientPid, + msg_queue = emqttd_queue:clear(Queue), + expire_timer = undefined}, hibernate}; + + +handle_cast({publish, ClientId, {?QOS_2, Message}}, State) -> + NewState = publish(State, ClientId, {?QOS_2, Message}), + {noreply, NewState}; + +handle_cast({puback, PacketId}, State) -> + NewState = puback(State, {?PUBACK, PacketId}), + {noreply, NewState}; + +handle_cast({pubrec, PacketId}, State) -> + NewState = puback(State, {?PUBREC, PacketId}), + {noreply, NewState}; + +handle_cast({pubrel, PacketId}, State) -> + NewState = puback(State, {?PUBREL, PacketId}), + {noreply, NewState}; + +handle_cast({pubcomp, PacketId}, State) -> + NewState = puback(State, {?PUBCOMP, PacketId}), + {noreply, NewState}; + +handle_cast({destroy, ClientId}, State = #session{clientid = ClientId}) -> + lager:warning("Session ~s destroyed", [ClientId]), + {stop, normal, State}; + +handle_cast(Msg, State) -> + lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), + {noreply, State}. + +handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> + F = fun(Message, S) -> dispatch(Message, S) end, + {noreply, lists:foldl(F, State, Messages)}; + +handle_info({dispatch, {_From, Message}}, State) -> + {noreply, dispatch(Message, State)}; + +handle_info({'EXIT', ClientPid, Reason}, State = #session{clientid = ClientId, + client_pid = ClientPid}) -> + lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), + {noreply, start_expire_timer(State#session{client_pid = undefined})}; + +handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> + lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), + {noreply, State}; + +handle_info(session_expired, State = #session{clientid = ClientId}) -> + lager:warning("Session ~s expired!", [ClientId]), + {stop, {shutdown, expired}, State}; + +handle_info({timeout, awaiting_rel, MsgId}, SessState) -> + NewState = timeout(awaiting_rel, MsgId, SessState), + {noreply, NewState}; + +handle_info(Info, State) -> + lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index da32fbfc9..2df163bae 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -89,8 +89,6 @@ {session, [ %% Expired after 2 days {expired_after, 48}, - %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - {max_inflight_messages, 40}, %% Max retries for unacknolege Qos1/2 messages {max_unack_retries, 3}, %% Retry after 4, 8, 16 seconds @@ -99,14 +97,20 @@ {await_rel_timeout, 8} ]}, {queue, [ - %% Max messages queued when client is disconnected, or inflight messsage window is overload - {max_queued_messages, 200}, - %% High watermark of queued messsages - {high_queue_watermark, 0.8}, + %% Max queue length + {max_length, 1000}, + + %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. + {inflight_window, 100}, + %% Low watermark of queued messsages - {low_queue_watermark, 0.2}, - %% Queue Qos0 offline messages? - {queue_qos0_messages, true} + {low_watermark, 0.2}, + + %% High watermark of queued messsages + {high_watermark, 0.6}, + + %% Queue Qos0 messages? + {queue_qos0, true} ]} ]}, %% Broker Options From c293ccab133d982ae0ff2ca0731b42c6ac9b2c8c Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Fri, 12 Jun 2015 18:38:26 +0800 Subject: [PATCH 063/104] sync --- apps/emqttd/src/emqttd_session.erl | 129 +++++++++++------------- apps/emqttd/src/emqttd_session_proc.erl | 45 ++++++++- 2 files changed, 101 insertions(+), 73 deletions(-) diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 44bc76bb4..4a1f44309 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -20,8 +20,26 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc +%%% %%% emqttd session. %%% +%%% Session State in the broker consists of: +%%% +%%% 1. The Client’s subscriptions. +%%% +%%% 2. inflight qos1, qos2 messages sent to the client but unacked, QoS 1 and QoS 2 +%%% messages which have been sent to the Client, but have not been completely +%%% acknowledged. +%%% +%%% 3. inflight qos2 messages received from client and waiting for pubrel. QoS 2 +%%% messages which have been received from the Client, but have not been +%%% completely acknowledged. +%%% +%%% 4. all qos1, qos2 messages published to when client is disconnected. +%%% QoS 1 and QoS 2 messages pending transmission to the Client. +%%% +%%% 5. Optionally, QoS 0 messages pending transmission to the Client. +%%% %%% @end %%%----------------------------------------------------------------------------- @@ -35,8 +53,6 @@ -include_lib("emqtt/include/emqtt_packet.hrl"). --define(SessProc, emqttd_session_proc). - %% Session Managenent APIs -export([start/1, resume/3, @@ -104,6 +120,10 @@ -type session() :: #session{}. +-export_type([session/0]). + +-define(SESSION(Sess), is_record(Sess, session)). + %%%============================================================================= %%% Session API %%%============================================================================= @@ -116,23 +136,15 @@ start({true = _CleanSess, ClientId, _ClientPid}) -> %%Destroy old session if CleanSess is true before. ok = emqttd_sm:destroy_session(ClientId), - {ok, initial_state(ClientId)}; - -start({false = _CleanSess, ClientId, ClientPid}) -> - {ok, SessPid} = emqttd_sm:start_session(ClientId, ClientPid), - {ok, SessPid}. + {ok, initial_state(ClientId)}. %%------------------------------------------------------------------------------ %% @doc Resume Session %% @end %%------------------------------------------------------------------------------ -spec resume(session(), binary(), pid()) -> session(). -resume(SessState = #session{}, _ClientId, _ClientPid) -> - SessState; -resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> - ?SessProc: - gen_server:cast(SessPid, {resume, ClientId, ClientPid}), - SessPid. +resume(Session = #session{}, _ClientId, _ClientPid) -> + Session. %%------------------------------------------------------------------------------ %% @doc Publish message @@ -145,47 +157,38 @@ publish(Session, ClientId, {?QOS_0, Message}) -> publish(Session, ClientId, {?QOS_1, Message}) -> emqttd_pubsub:publish(ClientId, Message), Session; -publish(SessState = #session{awaiting_rel = AwaitingRel, - await_rel_timeout = Timeout}, _ClientId, +publish(Session = #session{awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}, _ClientId, {?QOS_2, Message = #mqtt_message{msgid = MsgId}}) -> %% store in awaiting_rel TRef = erlang:send_after(Timeout * 1000, self(), {timeout, awaiting_rel, MsgId}), - SessState#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}; - -publish(SessPid, ClientId, {?QOS_2, Message}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {publish, ClientId, {?QOS_2, Message}}), - SessPid. + Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}. %%------------------------------------------------------------------------------ %% @doc PubAck message %% @end %%------------------------------------------------------------------------------ -spec puback(session(), {mqtt_packet_type(), mqtt_packet_id()}) -> session(). -puback(SessState = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> +puback(Session = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> case maps:is_key(PacketId, Awaiting) of true -> ok; false -> lager:warning("Session ~s: PUBACK PacketId '~p' not found!", [ClientId, PacketId]) end, - SessState#session{awaiting_ack = maps:remove(PacketId, Awaiting)}; -puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, PacketId}), SessPid; + Session#session{awaiting_ack = maps:remove(PacketId, Awaiting)}; %% PUBREC -puback(SessState = #session{clientid = ClientId, +puback(Session = #session{clientid = ClientId, awaiting_ack = AwaitingAck, awaiting_comp = AwaitingComp}, {?PUBREC, PacketId}) -> case maps:is_key(PacketId, AwaitingAck) of true -> ok; false -> lager:warning("Session ~s: PUBREC PacketId '~p' not found!", [ClientId, PacketId]) end, - SessState#session{awaiting_ack = maps:remove(PacketId, AwaitingAck), + Session#session{awaiting_ack = maps:remove(PacketId, AwaitingAck), awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; -puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubrec, PacketId}), SessPid; - %% PUBREL -puback(SessState = #session{clientid = ClientId, +puback(Session = #session{clientid = ClientId, awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> case maps:find(PacketId, Awaiting) of {ok, {Msg, TRef}} -> @@ -194,31 +197,25 @@ puback(SessState = #session{clientid = ClientId, error -> lager:error("Session ~s PUBREL PacketId '~p' not found!", [ClientId, PacketId]) end, - SessState#session{awaiting_rel = maps:remove(PacketId, Awaiting)}; - -puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubrel, PacketId}), SessPid; + Session#session{awaiting_rel = maps:remove(PacketId, Awaiting)}; %% PUBCOMP -puback(SessState = #session{clientid = ClientId, +puback(Session = #session{clientid = ClientId, awaiting_comp = AwaitingComp}, {?PUBCOMP, PacketId}) -> case maps:is_key(PacketId, AwaitingComp) of true -> ok; false -> lager:warning("Session ~s: PUBREC PacketId '~p' not exist", [ClientId, PacketId]) end, - SessState#session{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; + Session#session{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; -puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubcomp, PacketId}), SessPid. - -timeout(awaiting_rel, MsgId, SessState = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> +timeout(awaiting_rel, MsgId, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> case maps:find(MsgId, Awaiting) of {ok, {Msg, _TRef}} -> lager:error([{client, ClientId}], "Session ~s Awaiting Rel Timout!~nDrop Message:~p", [ClientId, Msg]), - SessState#session{awaiting_rel = maps:remove(MsgId, Awaiting)}; + Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}; error -> lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting Rel: MsgId=~p", [ClientId, MsgId]), - SessState + Session end. %%------------------------------------------------------------------------------ @@ -226,7 +223,7 @@ timeout(awaiting_rel, MsgId, SessState = #session{clientid = ClientId, awaiting_ %% @end %%------------------------------------------------------------------------------ -spec subscribe(session(), [{binary(), mqtt_qos()}]) -> {ok, session(), [mqtt_qos()]}. -subscribe(SessState = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> +subscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %% subscribe first and don't care if the subscriptions have been existed {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), @@ -253,18 +250,14 @@ subscribe(SessState = #session{clientid = ClientId, subscriptions = Subscription end end, Subscriptions, Topics), - {ok, SessState#session{subscriptions = Subscriptions1}, GrantedQos}; - -subscribe(SessPid, Topics) when is_pid(SessPid) -> - {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), - {ok, SessPid, GrantedQos}. + {ok, Session#session{subscriptions = Subscriptions1}, GrantedQos}; %%------------------------------------------------------------------------------ %% @doc Unsubscribe Topics %% @end %%------------------------------------------------------------------------------ -spec unsubscribe(session(), [binary()]) -> {ok, session()}. -unsubscribe(SessState = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> +unsubscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %%unsubscribe from topic tree ok = emqttd_pubsub:unsubscribe(Topics), @@ -280,22 +273,15 @@ unsubscribe(SessState = #session{clientid = ClientId, subscriptions = Subscripti end end, Subscriptions, Topics), - {ok, SessState#session{subscriptions = Subscriptions1}}; - -unsubscribe(SessPid, Topics) when is_pid(SessPid) -> - gen_server:call(SessPid, {unsubscribe, Topics}), - {ok, SessPid}. + {ok, Session#session{subscriptions = Subscriptions1}}; %%------------------------------------------------------------------------------ %% @doc Destroy Session %% @end %%------------------------------------------------------------------------------ --spec destroy(SessPid :: pid(), ClientId :: binary()) -> ok. -destroy(SessPid, ClientId) when is_pid(SessPid) -> - gen_server:cast(SessPid, {destroy, ClientId}). % message(qos1) is awaiting ack -await_ack(Msg = #mqtt_message{qos = ?QOS_1}, SessState = #session{message_id = MsgId, +await_ack(Msg = #mqtt_message{qos = ?QOS_1}, Session = #session{message_id = MsgId, inflight_queue = InflightQ, awaiting_ack = Awaiting, unack_retry_after = Time, @@ -304,11 +290,11 @@ await_ack(Msg = #mqtt_message{qos = ?QOS_1}, SessState = #session{message_id = M Msg1 = Msg#mqtt_message{msgid = MsgId}, TRef = erlang:send_after(Time * 1000, self(), {retry, MsgId}), Awaiting1 = maps:put(MsgId, {TRef, Retries, Time}, Awaiting), - {Msg1, next_msgid(SessState#session{inflight_queue = [{MsgId, Msg1} | InflightQ], + {Msg1, next_msgid(Session#session{inflight_queue = [{MsgId, Msg1} | InflightQ], awaiting_ack = Awaiting1})}. % message(qos2) is awaiting ack -await_ack(Message = #mqtt_message{qos = Qos}, SessState = #session{message_id = MsgId, awaiting_ack = Awaiting},) +await_ack(Message = #mqtt_message{qos = Qos}, Session = #session{message_id = MsgId, awaiting_ack = Awaiting},) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> %%assign msgid before send Message1 = Message#mqtt_message{msgid = MsgId, dup = false}, @@ -318,7 +304,7 @@ await_ack(Message = #mqtt_message{qos = Qos}, SessState = #session{message_id = true -> Message1 end, Awaiting1 = maps:put(MsgId, Message2, Awaiting), - {Message1, next_msgid(SessState#session{awaiting_ack = Awaiting1})}. + {Message1, next_msgid(Session#session{awaiting_ack = Awaiting1})}. initial_state(ClientId) -> %%TODO: init session options. @@ -334,36 +320,35 @@ initial_state(ClientId, ClientPid) -> State = initial_state(ClientId), State#session{client_pid = ClientPid}. - %%%============================================================================= %%% Internal functions %%%============================================================================= %% client is offline -dispatch(Msg, SessState = #session{client_pid = undefined}) -> - queue(Msg, SessState); +dispatch(Msg, Session = #session{client_pid = undefined}) -> + queue(Msg, Session); %% dispatch qos0 directly -dispatch(Msg = #mqtt_message{qos = ?QOS_0}, SessState = #session{client_pid = ClientPid}) -> - ClientPid ! {dispatch, {self(), Msg}}, SessState; +dispatch(Msg = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> + ClientPid ! {dispatch, {self(), Msg}}, Session; %% queue if inflight_queue is full -dispatch(Msg = #mqtt_message{qos = Qos}, SessState = #session{inflight_window = InflightWin, +dispatch(Msg = #mqtt_message{qos = Qos}, Session = #session{inflight_window = InflightWin, inflight_queue = InflightQ}) when (Qos > ?QOS_0) andalso (length(InflightQ) >= InflightWin) -> %%TODO: set alarms lager:error([{clientid, ClientId}], "Session ~s inflight_queue is full!", [ClientId]), - queue(Msg, SessState); + queue(Msg, Session); %% dispatch and await ack -dispatch(Msg = #mqtt_message{qos = Qos}, SessState = #session{client_pid = ClientPid}) +dispatch(Msg = #mqtt_message{qos = Qos}, Session = #session{client_pid = ClientPid}) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> %% assign msgid and await - {NewMsg, NewState} = await_ack(Msg, SessState), + {NewMsg, NewState} = await_ack(Msg, Session), ClientPid ! {dispatch, {self(), NewMsg}}, -queue(Msg, SessState = #session{pending_queue = Queue}) -> - SessState#session{pending_queue = emqttd_mqueue:in(Msg, Queue)}. +queue(Msg, Session = #session{pending_queue = Queue}) -> + Session#session{pending_queue = emqttd_mqueue:in(Msg, Queue)}. next_msgid(State = #session{message_id = 16#ffff}) -> State#session{message_id = 1}; diff --git a/apps/emqttd/src/emqttd_session_proc.erl b/apps/emqttd/src/emqttd_session_proc.erl index 43b60b75e..04d65563b 100644 --- a/apps/emqttd/src/emqttd_session_proc.erl +++ b/apps/emqttd/src/emqttd_session_proc.erl @@ -20,7 +20,7 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqttd session process. +%%% emqttd session process of persistent client. %%% %%% @end %%%----------------------------------------------------------------------------- @@ -42,6 +42,11 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +%% Refactor this API. +start({false = _CleanSess, ClientId, ClientPid}) -> + {ok, SessPid} = emqttd_sm:start_session(ClientId, ClientPid), + {ok, SessPid}. + %%------------------------------------------------------------------------------ %% @doc Start a session process. %% @end @@ -49,6 +54,44 @@ start_link(ClientId, ClientPid) -> gen_server:start_link(?MODULE, [ClientId, ClientPid], []). +resume(SessProc, ClientId, ClientPid) when is_pid(SessProc) -> + cast(SessProc, {resume, ClientId, ClientPid}). + +-spec publish(pid(), mqtt_clientid(), {mqtt_qos(), mqtt_message()}) -> pid(). +publish(SessProc, ClientId, {?QOS_0, Message}) when is_pid(SessProc) -> + emqttd_pubsub:publish(ClientId, Message), Session; + +publish(SessProc, ClientId, {?QOS_1, Message}) when is_pid(SessProc) -> + emqttd_pubsub:publish(ClientId, Message), Session; + +publish(SessProc, ClientId, {?QOS_2, Message}) when is_pid(SessProc) -> + cast(SessProc, {publish, ClientId, {?QOS_2, Message}}). + +puback(SessProc, {?PUBACK, PacketId}) when is_pid(SessProc) -> + cast(SessProc, {puback, PacketId}). + +puback(SessProc, {?PUBREL, PacketId}) when is_pid(SessProc) -> + cast(SessPid, {pubrel, PacketId}). + +puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> + cast(SessPid, {pubcomp, PacketId}). + +subscribe(SessPid, Topics) when is_pid(SessPid) -> + {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), + {ok, SessPid, GrantedQos}. + +unsubscribe(SessPid, Topics) when is_pid(SessPid) -> + gen_server:call(SessPid, {unsubscribe, Topics}), + {ok, SessPid}. + +-spec destroy(SessPid :: pid(), ClientId :: binary()) -> ok. +destroy(SessPid, ClientId) when is_pid(SessPid) -> + gen_server:cast(SessPid, {destroy, ClientId}). + +cast(SessProc, Msg) -> + gen_server:cast(SessProc, Msg), SessProc. + + %%%============================================================================= %%% gen_server callbacks %%%============================================================================= From 5e0bf3d831f7e6145d308da0731d1d2d846de328 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 12 Jun 2015 21:59:37 +0800 Subject: [PATCH 064/104] sync with imac --- apps/emqttd/src/emqttd_protocol.erl | 1 + apps/emqttd/src/emqttd_sm.erl | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 37e19f16e..c6b3448a5 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -51,6 +51,7 @@ username, clientid, clean_sess, + sessmod, session, %% session state or session pid will_msg, max_clientid_len = ?MAX_CLIENTID_LEN, diff --git a/apps/emqttd/src/emqttd_sm.erl b/apps/emqttd/src/emqttd_sm.erl index 05d9e62b0..a454d6fd9 100644 --- a/apps/emqttd/src/emqttd_sm.erl +++ b/apps/emqttd/src/emqttd_sm.erl @@ -47,7 +47,9 @@ %% API Function Exports -export([start_link/2, pool/0, table/0]). --export([lookup_session/1, start_session/2, destroy_session/1]). +-export([lookup_session/1, + start_session/2, + destroy_session/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -85,6 +87,15 @@ pool() -> ?SM_POOL. %%------------------------------------------------------------------------------ table() -> ?SESSION_TAB. +%%------------------------------------------------------------------------------ +%% @doc Start a session +%% @end +%%------------------------------------------------------------------------------ +-spec start_session(binary(), pid()) -> {ok, pid()} | {error, any()}. +start_session(ClientId, ClientPid) -> + SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), + gen_server:call(SmPid, {start_session, ClientId, ClientPid}). + %%------------------------------------------------------------------------------ %% @doc Lookup Session Pid %% @end @@ -96,15 +107,6 @@ lookup_session(ClientId) -> [] -> undefined end. -%%------------------------------------------------------------------------------ -%% @doc Start a session -%% @end -%%------------------------------------------------------------------------------ --spec start_session(binary(), pid()) -> {ok, pid()} | {error, any()}. -start_session(ClientId, ClientPid) -> - SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - gen_server:call(SmPid, {start_session, ClientId, ClientPid}). - %%------------------------------------------------------------------------------ %% @doc Destroy a session %% @end From 30ff3b3f4e15a4e0fd91ad2a80413fd14e4b3ea8 Mon Sep 17 00:00:00 2001 From: Feng Date: Fri, 12 Jun 2015 22:47:16 +0800 Subject: [PATCH 065/104] SESSION_TAB --- apps/emqttd/src/emqttd_sm.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/emqttd/src/emqttd_sm.erl b/apps/emqttd/src/emqttd_sm.erl index 05d9e62b0..d5ecf0a8e 100644 --- a/apps/emqttd/src/emqttd_sm.erl +++ b/apps/emqttd/src/emqttd_sm.erl @@ -53,7 +53,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {id, tabid, statsfun}). +-record(state, {id, statsfun}). -define(SM_POOL, sm_pool). @@ -91,7 +91,7 @@ table() -> ?SESSION_TAB. %%------------------------------------------------------------------------------ -spec lookup_session(binary()) -> pid() | undefined. lookup_session(ClientId) -> - case ets:lookup(emqttd_sm_sup:table(), ClientId) of + case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, _}] -> SessPid; [] -> undefined end. @@ -156,8 +156,8 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({'DOWN', MRef, process, DownPid, _Reason}, State = #state{tabid = Tab}) -> - ets:match_delete(Tab, {'_', DownPid, MRef}), +handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> + ets:match_delete(?SESSION_TAB, {'_', DownPid, MRef}), {noreply, setstats(State)}; handle_info(_Info, State) -> From 9c666cef7081fbe6d93141b2c783106d09008715 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sat, 13 Jun 2015 12:09:08 +0800 Subject: [PATCH 066/104] merge session --- apps/emqttd/src/emqttd_protocol.erl | 4 +- apps/emqttd/src/emqttd_session.erl | 278 ++++++++++++++++++++---- apps/emqttd/src/emqttd_session_proc.erl | 255 ---------------------- apps/emqttd/src/emqttd_session_sup.erl | 4 +- apps/emqttd/src/emqttd_sm.erl | 35 ++- rel/files/emqttd.config | 16 +- 6 files changed, 271 insertions(+), 321 deletions(-) delete mode 100644 apps/emqttd/src/emqttd_session_proc.erl diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index c6b3448a5..bd500ab26 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -152,13 +152,13 @@ handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername} emqttd_cm:register(client(State2)), %%Starting session - {ok, Session} = emqttd_session:start({CleanSess, clientid(State2), self()}), + {ok, SessMod, Session} = emqttd_sm:start_session(CleanSess, clientid(State2)), %% Start keepalive start_keepalive(KeepAlive), %% ACCEPT - {?CONNACK_ACCEPT, State2#proto_state{session = Session, will_msg = willmsg(Var)}}; + {?CONNACK_ACCEPT, State2#proto_state{sessmod = SessMod, session = Session, will_msg = willmsg(Var)}}; {error, Reason}-> lager:error("~s@~s: username '~s', login failed - ~s", [ClientId, emqttd_net:format(Peername), Username, Reason]), diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 4a1f44309..fce1c766b 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -21,7 +21,7 @@ %%%----------------------------------------------------------------------------- %%% @doc %%% -%%% emqttd session. +%%% emqttd session for persistent client. %%% %%% Session State in the broker consists of: %%% @@ -53,10 +53,11 @@ -include_lib("emqtt/include/emqtt_packet.hrl"). -%% Session Managenent APIs --export([start/1, - resume/3, - destroy/2]). +%% Start gen_server +-export([start_link/2, resume/3, destroy/2]). + +%% Init Session State +-export([new/1]). %% PubSub APIs -export([publish/3, @@ -66,10 +67,17 @@ await/2, dispatch/2]). +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + -record(session, { %% ClientId: Identifier of Session clientid :: binary(), + %% Clean Session Flag + clean_sess = true, + %% Client Pid linked with session client_pid :: pid(), @@ -111,63 +119,103 @@ %% Awaiting PUBREL timeout await_rel_timeout = 8, - %% session expired after 48 hours - sess_expired_after = 172800, + %% Max Packets that Awaiting PUBREL + max_awaiting_rel = 100, - sess_expired_timer, + %% session expired after 48 hours + expired_after = 172800, + + expired_timer, timestamp}). -type session() :: #session{}. --export_type([session/0]). - --define(SESSION(Sess), is_record(Sess, session)). - %%%============================================================================= %%% Session API %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Start Session +%% @doc Start a session process. %% @end %%------------------------------------------------------------------------------ --spec start({boolean(), binary(), pid()}) -> {ok, session()}. -start({true = _CleanSess, ClientId, _ClientPid}) -> - %%Destroy old session if CleanSess is true before. - ok = emqttd_sm:destroy_session(ClientId), - {ok, initial_state(ClientId)}. +start_link(ClientId, ClientPid) -> + gen_server:start_link(?MODULE, [ClientId, ClientPid], []). %%------------------------------------------------------------------------------ -%% @doc Resume Session +%% @doc Resume a session. %% @end %%------------------------------------------------------------------------------ --spec resume(session(), binary(), pid()) -> session(). -resume(Session = #session{}, _ClientId, _ClientPid) -> - Session. +resume(Session, _ClientId, _ClientPid) when is_record(Session, session) -> + Session; +resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> + gen_server:cast(SessPid, {resume, ClientId, ClientPid}), SessPid. + +%%------------------------------------------------------------------------------ +%% @doc Destroy a session. +%% @end +%%------------------------------------------------------------------------------ +-spec destroy(SessPid :: pid(), ClientId :: binary()) -> ok. +destroy(SessPid, ClientId) when is_pid(SessPid) -> + gen_server:cast(SessPid, {destroy, ClientId}), SessPid. + +%%------------------------------------------------------------------------------ +%% @doc Init Session State. +%% @end +%%------------------------------------------------------------------------------ +-spec new(binary()) -> session(). +new(ClientId) -> + QEnv = emqttd:env(mqtt, queue), + SessEnv = emqttd:env(mqtt, session), + #session{ + clientid = ClientId, + clean_sess = true, + subscriptions = [], + inflight_window = emqttd_mqwin:new(ClientId, QEnv), + pending_queue = emqttd_mqueue:new(ClientId, QEnv), + awaiting_rel = #{}, + awaiting_ack = #{}, + awaiting_comp = #{}, + unack_retries = emqttd_opts:g(unack_retries, SessEnv), + unack_timeout = emqttd_opts:g(unack_timeout, SessEnv), + await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv), + max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv), + expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600 + }. %%------------------------------------------------------------------------------ %% @doc Publish message %% @end %%------------------------------------------------------------------------------ --spec publish(session(), mqtt_clientid(), {mqtt_qos(), mqtt_message()}) -> session(). +-spec publish(session() | pid(), mqtt_clientid(), {mqtt_qos(), mqtt_message()}) -> session() | pid(). publish(Session, ClientId, {?QOS_0, Message}) -> + %% publish qos0 directly emqttd_pubsub:publish(ClientId, Message), Session; publish(Session, ClientId, {?QOS_1, Message}) -> + %% publish qos1 directly, and client will puback emqttd_pubsub:publish(ClientId, Message), Session; -publish(Session = #session{awaiting_rel = AwaitingRel, - await_rel_timeout = Timeout}, _ClientId, +publish(Session = #session{awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout, + max_awaiting_rel = MaxLen}, ClientId, {?QOS_2, Message = #mqtt_message{msgid = MsgId}}) -> - %% store in awaiting_rel - TRef = erlang:send_after(Timeout * 1000, self(), {timeout, awaiting_rel, MsgId}), - Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}. + case maps:size(AwaitingRel) >= MaxLen of + true -> lager:error([{clientid, ClientId}], "Session ~s " + " dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]); + false -> + %% store in awaiting_rel + TRef = erlang:send_after(Timeout * 1000, self(), {timeout, awaiting_rel, MsgId}), + Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}; + end; +publish(SessPid, ClientId, {?QOS_2, Message}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {publish, ClientId, {?QOS_2, Message}}), SessPid. %%------------------------------------------------------------------------------ %% @doc PubAck message %% @end %%------------------------------------------------------------------------------ + -spec puback(session(), {mqtt_packet_type(), mqtt_packet_id()}) -> session(). puback(Session = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> case maps:is_key(PacketId, Awaiting) of @@ -176,6 +224,9 @@ puback(Session = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBAC end, Session#session{awaiting_ack = maps:remove(PacketId, Awaiting)}; +puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {puback, PacketId}); + %% PUBREC puback(Session = #session{clientid = ClientId, awaiting_ack = AwaitingAck, @@ -187,18 +238,23 @@ puback(Session = #session{clientid = ClientId, Session#session{awaiting_ack = maps:remove(PacketId, AwaitingAck), awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; +puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> + gen_server:cast(SessPid, {pubrec, PacketId}), SessPid; + %% PUBREL -puback(Session = #session{clientid = ClientId, - awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> +puback(Session = #session{clientid = ClientId, awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> case maps:find(PacketId, Awaiting) of {ok, {Msg, TRef}} -> catch erlang:cancel_timer(TRef), emqttd_pubsub:publish(ClientId, Msg); error -> - lager:error("Session ~s PUBREL PacketId '~p' not found!", [ClientId, PacketId]) + lager:error("Session ~s cannot find PUBREL PacketId '~p'!", [ClientId, PacketId]) end, Session#session{awaiting_rel = maps:remove(PacketId, Awaiting)}; +puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> + cast(SessPid, {pubrel, PacketId}); + %% PUBCOMP puback(Session = #session{clientid = ClientId, awaiting_comp = AwaitingComp}, {?PUBCOMP, PacketId}) -> @@ -208,6 +264,9 @@ puback(Session = #session{clientid = ClientId, end, Session#session{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; +puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> + cast(SessPid, {pubcomp, PacketId}). + timeout(awaiting_rel, MsgId, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> case maps:find(MsgId, Awaiting) of {ok, {Msg, _TRef}} -> @@ -222,7 +281,7 @@ timeout(awaiting_rel, MsgId, Session = #session{clientid = ClientId, awaiting_re %% @doc Subscribe Topics %% @end %%------------------------------------------------------------------------------ --spec subscribe(session(), [{binary(), mqtt_qos()}]) -> {ok, session(), [mqtt_qos()]}. +-spec subscribe(session() | pid(), [{binary(), mqtt_qos()}]) -> {ok, session() | pid(), [mqtt_qos()]}. subscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %% subscribe first and don't care if the subscriptions have been existed @@ -252,11 +311,15 @@ subscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions} {ok, Session#session{subscriptions = Subscriptions1}, GrantedQos}; +subscribe(SessPid, Topics) when is_pid(SessPid) -> + {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), + {ok, SessPid, GrantedQos}. + %%------------------------------------------------------------------------------ %% @doc Unsubscribe Topics %% @end %%------------------------------------------------------------------------------ --spec unsubscribe(session(), [binary()]) -> {ok, session()}. +-spec unsubscribe(session() | pid(), [binary()]) -> {ok, session() | pid()}. unsubscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> %%unsubscribe from topic tree @@ -275,6 +338,10 @@ unsubscribe(Session = #session{clientid = ClientId, subscriptions = Subscription {ok, Session#session{subscriptions = Subscriptions1}}; +unsubscribe(SessPid, Topics) when is_pid(SessPid) -> + gen_server:call(SessPid, {unsubscribe, Topics}), + {ok, SessPid}. + %%------------------------------------------------------------------------------ %% @doc Destroy Session %% @end @@ -306,19 +373,140 @@ await_ack(Message = #mqtt_message{qos = Qos}, Session = #session{message_id = Ms Awaiting1 = maps:put(MsgId, Message2, Awaiting), {Message1, next_msgid(Session#session{awaiting_ack = Awaiting1})}. -initial_state(ClientId) -> - %%TODO: init session options. - #session{clientid = ClientId, - subscriptions = [], - inflight_queue = [], - awaiting_queue = [], - awaiting_ack = #{}, - awaiting_rel = #{}, - awaiting_comp = #{}}. -initial_state(ClientId, ClientPid) -> - State = initial_state(ClientId), - State#session{client_pid = ClientPid}. +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= +init([ClientId, ClientPid]) -> + process_flag(trap_exit, true), + true = link(ClientPid), + Session = emqttd_session:new(ClientId), + {ok, Session#session{clean_sess = false, + client_pid = ClientPid, + timestamp = os:timestamp()}, hibernate}. + + +handle_call({subscribe, Topics}, _From, Session) -> + {ok, NewSession, GrantedQos} = subscribe(Session, Topics), + {reply, {ok, GrantedQos}, NewSession}; + +handle_call({unsubscribe, Topics}, _From, Session) -> + {ok, NewSession} = unsubscribe(Session, Topics), + {reply, ok, NewSession}; + +handle_call(Req, _From, State) -> + lager:error("Unexpected Request: ~p", [Req]), + {reply, {error, badreq}, State}. + +handle_cast({resume, ClientId, ClientPid}, State = #session{ + clientid = ClientId, + client_pid = OldClientPid, + msg_queue = Queue, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + expire_timer = ETimer}) -> + + lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), + + %% kick old client... + if + OldClientPid =:= undefined -> + ok; + OldClientPid =:= ClientPid -> + ok; + true -> + lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), + unlink(OldClientPid), + OldClientPid ! {stop, duplicate_id, ClientPid} + end, + + %% cancel timeout timer + emqttd_util:cancel_timer(ETimer), + + %% redelivery PUBREL + lists:foreach(fun(PacketId) -> + ClientPid ! {redeliver, {?PUBREL, PacketId}} + end, maps:keys(AwaitingComp)), + + %% redelivery messages that awaiting PUBACK or PUBREC + Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end, + lists:foreach(fun(Msg) -> + ClientPid ! {dispatch, {self(), Dup(Msg)}} + end, maps:values(AwaitingAck)), + + %% send offline messages + lists:foreach(fun(Msg) -> + ClientPid ! {dispatch, {self(), Msg}} + end, emqttd_queue:all(Queue)), + + {noreply, State#session{client_pid = ClientPid, + msg_queue = emqttd_queue:clear(Queue), + expire_timer = undefined}, hibernate}; + +handle_cast({publish, ClientId, {?QOS_2, Message}}, State) -> + NewState = publish(State, ClientId, {?QOS_2, Message}), + {noreply, NewState}; + +handle_cast({puback, PacketId}, State) -> + NewState = puback(State, {?PUBACK, PacketId}), + {noreply, NewState}; + +handle_cast({pubrec, PacketId}, State) -> + NewState = puback(State, {?PUBREC, PacketId}), + {noreply, NewState}; + +handle_cast({pubrel, PacketId}, State) -> + NewState = puback(State, {?PUBREL, PacketId}), + {noreply, NewState}; + +handle_cast({pubcomp, PacketId}, State) -> + NewState = puback(State, {?PUBCOMP, PacketId}), + {noreply, NewState}; + +handle_cast({destroy, ClientId}, State = #session{clientid = ClientId}) -> + lager:warning("Session ~s destroyed", [ClientId]), + {stop, normal, State}; + +handle_cast(Msg, State) -> + lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), + {noreply, State}. + +handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> + F = fun(Message, S) -> dispatch(Message, S) end, + {noreply, lists:foldl(F, State, Messages)}; + +handle_info({dispatch, {_From, Message}}, State) -> + {noreply, dispatch(Message, State)}; + +handle_info({'EXIT', ClientPid, Reason}, State = #session{clientid = ClientId, + client_pid = ClientPid}) -> + lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), + {noreply, start_expire_timer(State#session{client_pid = undefined})}; + +handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> + lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), + {noreply, State}; + +handle_info(session_expired, State = #session{clientid = ClientId}) -> + lager:warning("Session ~s expired!", [ClientId]), + {stop, {shutdown, expired}, State}; + +handle_info({timeout, awaiting_rel, MsgId}, SessState) -> + NewState = timeout(awaiting_rel, MsgId, SessState), + {noreply, NewState}; + +handle_info(Info, State) -> + lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + + %%%============================================================================= %%% Internal functions diff --git a/apps/emqttd/src/emqttd_session_proc.erl b/apps/emqttd/src/emqttd_session_proc.erl deleted file mode 100644 index 04d65563b..000000000 --- a/apps/emqttd/src/emqttd_session_proc.erl +++ /dev/null @@ -1,255 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd session process of persistent client. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttd_session_proc). - --author("Feng Lee "). - --include("emqttd.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). - -%% Start gen_server --export([start_link/2]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - -%% Refactor this API. -start({false = _CleanSess, ClientId, ClientPid}) -> - {ok, SessPid} = emqttd_sm:start_session(ClientId, ClientPid), - {ok, SessPid}. - -%%------------------------------------------------------------------------------ -%% @doc Start a session process. -%% @end -%%------------------------------------------------------------------------------ -start_link(ClientId, ClientPid) -> - gen_server:start_link(?MODULE, [ClientId, ClientPid], []). - -resume(SessProc, ClientId, ClientPid) when is_pid(SessProc) -> - cast(SessProc, {resume, ClientId, ClientPid}). - --spec publish(pid(), mqtt_clientid(), {mqtt_qos(), mqtt_message()}) -> pid(). -publish(SessProc, ClientId, {?QOS_0, Message}) when is_pid(SessProc) -> - emqttd_pubsub:publish(ClientId, Message), Session; - -publish(SessProc, ClientId, {?QOS_1, Message}) when is_pid(SessProc) -> - emqttd_pubsub:publish(ClientId, Message), Session; - -publish(SessProc, ClientId, {?QOS_2, Message}) when is_pid(SessProc) -> - cast(SessProc, {publish, ClientId, {?QOS_2, Message}}). - -puback(SessProc, {?PUBACK, PacketId}) when is_pid(SessProc) -> - cast(SessProc, {puback, PacketId}). - -puback(SessProc, {?PUBREL, PacketId}) when is_pid(SessProc) -> - cast(SessPid, {pubrel, PacketId}). - -puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> - cast(SessPid, {pubcomp, PacketId}). - -subscribe(SessPid, Topics) when is_pid(SessPid) -> - {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), - {ok, SessPid, GrantedQos}. - -unsubscribe(SessPid, Topics) when is_pid(SessPid) -> - gen_server:call(SessPid, {unsubscribe, Topics}), - {ok, SessPid}. - --spec destroy(SessPid :: pid(), ClientId :: binary()) -> ok. -destroy(SessPid, ClientId) when is_pid(SessPid) -> - gen_server:cast(SessPid, {destroy, ClientId}). - -cast(SessProc, Msg) -> - gen_server:cast(SessProc, Msg), SessProc. - - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -init([ClientId, ClientPid]) -> - process_flag(trap_exit, true), - true = link(ClientPid), - State = initial_state(ClientId, ClientPid), - MQueue = emqttd_mqueue:new(ClientId, emqttd:env(mqtt, queue)), - State1 = State#session{pending_queue = MQueue, - timestamp = os:timestamp()}, - {ok, init(emqttd:env(mqtt, session), State1), hibernate}. - -init([], State) -> - State; - -%% Session expired after hours -init([{expired_after, Hours} | Opts], State) -> - init(Opts, State#session{sess_expired_after = Hours * 3600}); - -%% Max number of QoS 1 and 2 messages that can be “inflight” at one time. -init([{max_inflight_messages, MaxInflight} | Opts], State) -> - init(Opts, State#session{inflight_window = MaxInflight}); - -%% Max retries for unacknolege Qos1/2 messages -init([{max_unack_retries, Retries} | Opts], State) -> - init(Opts, State#session{max_unack_retries = Retries}); - -%% Retry after 4, 8, 16 seconds -init([{unack_retry_after, Secs} | Opts], State) -> - init(Opts, State#session{unack_retry_after = Secs}); - -%% Awaiting PUBREL timeout -init([{await_rel_timeout, Secs} | Opts], State) -> - init(Opts, State#session{await_rel_timeout = Secs}); - -init([Opt | Opts], State) -> - lager:error("Bad Session Option: ~p", [Opt]), - init(Opts, State). - -handle_call({subscribe, Topics}, _From, State) -> - {ok, NewState, GrantedQos} = subscribe(State, Topics), - {reply, {ok, GrantedQos}, NewState}; - -handle_call({unsubscribe, Topics}, _From, State) -> - {ok, NewState} = unsubscribe(State, Topics), - {reply, ok, NewState}; - -handle_call(Req, _From, State) -> - lager:error("Unexpected request: ~p", [Req]), - {reply, error, State}. - -handle_cast({resume, ClientId, ClientPid}, State = #session{ - clientid = ClientId, - client_pid = OldClientPid, - msg_queue = Queue, - awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp, - expire_timer = ETimer}) -> - - lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), - - %% kick old client... - if - OldClientPid =:= undefined -> - ok; - OldClientPid =:= ClientPid -> - ok; - true -> - lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), - unlink(OldClientPid), - OldClientPid ! {stop, duplicate_id, ClientPid} - end, - - %% cancel timeout timer - emqttd_util:cancel_timer(ETimer), - - %% redelivery PUBREL - lists:foreach(fun(PacketId) -> - ClientPid ! {redeliver, {?PUBREL, PacketId}} - end, maps:keys(AwaitingComp)), - - %% redelivery messages that awaiting PUBACK or PUBREC - Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end, - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Dup(Msg)}} - end, maps:values(AwaitingAck)), - - %% send offline messages - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Msg}} - end, emqttd_queue:all(Queue)), - - {noreply, State#session{client_pid = ClientPid, - msg_queue = emqttd_queue:clear(Queue), - expire_timer = undefined}, hibernate}; - - -handle_cast({publish, ClientId, {?QOS_2, Message}}, State) -> - NewState = publish(State, ClientId, {?QOS_2, Message}), - {noreply, NewState}; - -handle_cast({puback, PacketId}, State) -> - NewState = puback(State, {?PUBACK, PacketId}), - {noreply, NewState}; - -handle_cast({pubrec, PacketId}, State) -> - NewState = puback(State, {?PUBREC, PacketId}), - {noreply, NewState}; - -handle_cast({pubrel, PacketId}, State) -> - NewState = puback(State, {?PUBREL, PacketId}), - {noreply, NewState}; - -handle_cast({pubcomp, PacketId}, State) -> - NewState = puback(State, {?PUBCOMP, PacketId}), - {noreply, NewState}; - -handle_cast({destroy, ClientId}, State = #session{clientid = ClientId}) -> - lager:warning("Session ~s destroyed", [ClientId]), - {stop, normal, State}; - -handle_cast(Msg, State) -> - lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), - {noreply, State}. - -handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> - F = fun(Message, S) -> dispatch(Message, S) end, - {noreply, lists:foldl(F, State, Messages)}; - -handle_info({dispatch, {_From, Message}}, State) -> - {noreply, dispatch(Message, State)}; - -handle_info({'EXIT', ClientPid, Reason}, State = #session{clientid = ClientId, - client_pid = ClientPid}) -> - lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), - {noreply, start_expire_timer(State#session{client_pid = undefined})}; - -handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> - lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), - {noreply, State}; - -handle_info(session_expired, State = #session{clientid = ClientId}) -> - lager:warning("Session ~s expired!", [ClientId]), - {stop, {shutdown, expired}, State}; - -handle_info({timeout, awaiting_rel, MsgId}, SessState) -> - NewState = timeout(awaiting_rel, MsgId, SessState), - {noreply, NewState}; - -handle_info(Info, State) -> - lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - - diff --git a/apps/emqttd/src/emqttd_session_sup.erl b/apps/emqttd/src/emqttd_session_sup.erl index 7ab0cc18a..d4f790847 100644 --- a/apps/emqttd/src/emqttd_session_sup.erl +++ b/apps/emqttd/src/emqttd_session_sup.erl @@ -56,6 +56,6 @@ start_session(ClientId, ClientPid) -> init([]) -> {ok, {{simple_one_for_one, 10, 10}, - [{session, {emqttd_session, start_link, []}, - transient, 10000, worker, [emqttd_session]}]}}. + [{session, {emqttd_session_proc, start_link, []}, + transient, 10000, worker, [emqttd_session_proc]}]}}. diff --git a/apps/emqttd/src/emqttd_sm.erl b/apps/emqttd/src/emqttd_sm.erl index a454d6fd9..56c0ea9f9 100644 --- a/apps/emqttd/src/emqttd_sm.erl +++ b/apps/emqttd/src/emqttd_sm.erl @@ -38,8 +38,6 @@ -author("Feng Lee "). -%%cleanSess: true | false - -include("emqttd.hrl"). -behaviour(gen_server). @@ -55,7 +53,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {id, tabid, statsfun}). +-record(state, {id, statsfun}). -define(SM_POOL, sm_pool). @@ -91,10 +89,21 @@ table() -> ?SESSION_TAB. %% @doc Start a session %% @end %%------------------------------------------------------------------------------ --spec start_session(binary(), pid()) -> {ok, pid()} | {error, any()}. -start_session(ClientId, ClientPid) -> + +-spec start_session(CleanSess :: boolean(), binary()) -> {ok, module(), record() | pid()}. +start_session(true, ClientId) -> + %% destroy old session if existed + ok = destroy_session(ClientId), + {ok, emqttd_session, emqttd_session:new(ClientId)}; + +start_session(false, ClientId) -> SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - gen_server:call(SmPid, {start_session, ClientId, ClientPid}). + case call(SmPid, {start_session, ClientId, self()}) of + {ok, SessPid} -> + {ok, emqttd_session_proc, SessPid}; + {error, Error} -> + {error, Error} + end. %%------------------------------------------------------------------------------ %% @doc Lookup Session Pid @@ -102,7 +111,7 @@ start_session(ClientId, ClientPid) -> %%------------------------------------------------------------------------------ -spec lookup_session(binary()) -> pid() | undefined. lookup_session(ClientId) -> - case ets:lookup(emqttd_sm_sup:table(), ClientId) of + case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, _}] -> SessPid; [] -> undefined end. @@ -114,7 +123,9 @@ lookup_session(ClientId) -> -spec destroy_session(binary()) -> ok. destroy_session(ClientId) -> SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - gen_server:call(SmPid, {destroy_session, ClientId}). + call(SmPid, {destroy_session, ClientId}). + +call(SmPid, Req) -> gen_server:call(SmPid, Req). %%%============================================================================= %%% gen_server callbacks @@ -128,11 +139,11 @@ handle_call({start_session, ClientId, ClientPid}, _From, State) -> Reply = case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, _MRef}] -> - emqttd_session:resume(SessPid, ClientId, ClientPid), + emqttd_session_proc:resume(SessPid, ClientId, ClientPid), {ok, SessPid}; [] -> case emqttd_session_sup:start_session(ClientId, ClientPid) of - {ok, SessPid} -> + {ok, SessPid} -> ets:insert(?SESSION_TAB, {ClientId, SessPid, erlang:monitor(process, SessPid)}), {ok, SessPid}; {error, Error} -> @@ -158,8 +169,8 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({'DOWN', MRef, process, DownPid, _Reason}, State = #state{tabid = Tab}) -> - ets:match_delete(Tab, {'_', DownPid, MRef}), +handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> + ets:match_delete(?SESSION_TAB, {'_', DownPid, MRef}), {noreply, setstats(State)}; handle_info(_Info, State) -> diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 2df163bae..9995ddea1 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -89,12 +89,18 @@ {session, [ %% Expired after 2 days {expired_after, 48}, - %% Max retries for unacknolege Qos1/2 messages - {max_unack_retries, 3}, + + %% Max retries for unack Qos1/2 messages + {unack_retries, 3}, + %% Retry after 4, 8, 16 seconds - {unack_retry_after, 4}, - %% Awaiting PUBREL timeout - {await_rel_timeout, 8} + {unack_timeout, 4}, + + %% Awaiting PUBREL Timeout + {await_rel_timeout, 8}, + + %% Max Packets that Awaiting PUBREL + {max_awaiting_rel, 100} ]}, {queue, [ %% Max queue length From a1d778b08188baf227b195ae697bde83f99fa3af Mon Sep 17 00:00:00 2001 From: Feng Date: Sun, 14 Jun 2015 07:13:08 +0800 Subject: [PATCH 067/104] sync --- apps/emqtt/include/emqtt.hrl | 4 + apps/emqttd/src/emqttd_session.erl | 117 ++++++++++++++--------------- 2 files changed, 59 insertions(+), 62 deletions(-) diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl index bbd1f95c4..bb17eef64 100644 --- a/apps/emqtt/include/emqtt.hrl +++ b/apps/emqtt/include/emqtt.hrl @@ -58,6 +58,10 @@ -record(mqtt_message, { %% topic is first for message may be retained topic :: binary(), + %% clientid from + from :: binary() | atom(), + %% sender pid ?? + sender :: pid(), qos = ?QOS_0 :: mqtt_qos(), retain = false :: boolean(), dup = false :: boolean(), diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index fce1c766b..01a910e3c 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -225,7 +225,7 @@ puback(Session = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBAC Session#session{awaiting_ack = maps:remove(PacketId, Awaiting)}; puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, PacketId}); + gen_server:cast(SessPid, {puback, {?PUBACK, PacketId}); %% PUBREC puback(Session = #session{clientid = ClientId, @@ -239,7 +239,7 @@ puback(Session = #session{clientid = ClientId, awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubrec, PacketId}), SessPid; + gen_server:cast(SessPid, {puback, {?PUBREC, PacketId}); %% PUBREL puback(Session = #session{clientid = ClientId, awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> @@ -253,7 +253,7 @@ puback(Session = #session{clientid = ClientId, awaiting_rel = Awaiting}, {?PUBRE Session#session{awaiting_rel = maps:remove(PacketId, Awaiting)}; puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> - cast(SessPid, {pubrel, PacketId}); + gen_server:cast(SessPid, {puback, {?PUBREL, PacketId}); %% PUBCOMP puback(Session = #session{clientid = ClientId, @@ -265,7 +265,9 @@ puback(Session = #session{clientid = ClientId, Session#session{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> - cast(SessPid, {pubcomp, PacketId}). + gen_server:cast(SessPid, {puback, {?PUBCOMP, PacketId}); + +wait_ack timeout(awaiting_rel, MsgId, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> case maps:find(MsgId, Awaiting) of @@ -440,48 +442,34 @@ handle_cast({resume, ClientId, ClientPid}, State = #session{ end, emqttd_queue:all(Queue)), {noreply, State#session{client_pid = ClientPid, - msg_queue = emqttd_queue:clear(Queue), - expire_timer = undefined}, hibernate}; + msg_queue = emqttd_queue:clear(Queue), + expire_timer = undefined}, hibernate}; -handle_cast({publish, ClientId, {?QOS_2, Message}}, State) -> - NewState = publish(State, ClientId, {?QOS_2, Message}), - {noreply, NewState}; +handle_cast({publish, ClientId, {?QOS_2, Message}}, Session) -> + {noreply, publish(Session, ClientId, {?QOS_2, Message})}; -handle_cast({puback, PacketId}, State) -> - NewState = puback(State, {?PUBACK, PacketId}), - {noreply, NewState}; +handle_cast({puback, {PubAck, PacketId}, Session) -> + {noreply, puback(Session, {PubAck, PacketId})}; -handle_cast({pubrec, PacketId}, State) -> - NewState = puback(State, {?PUBREC, PacketId}), - {noreply, NewState}; - -handle_cast({pubrel, PacketId}, State) -> - NewState = puback(State, {?PUBREL, PacketId}), - {noreply, NewState}; - -handle_cast({pubcomp, PacketId}, State) -> - NewState = puback(State, {?PUBCOMP, PacketId}), - {noreply, NewState}; - -handle_cast({destroy, ClientId}, State = #session{clientid = ClientId}) -> +handle_cast({destroy, ClientId}, Session = #session{clientid = ClientId}) -> lager:warning("Session ~s destroyed", [ClientId]), - {stop, normal, State}; + {stop, normal, Session}; handle_cast(Msg, State) -> lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), {noreply, State}. -handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> +handle_info({dispatch, {_From, Messages}}, Session) when is_list(Messages) -> F = fun(Message, S) -> dispatch(Message, S) end, - {noreply, lists:foldl(F, State, Messages)}; + {noreply, lists:foldl(F, Session, Messages)}; handle_info({dispatch, {_From, Message}}, State) -> {noreply, dispatch(Message, State)}; -handle_info({'EXIT', ClientPid, Reason}, State = #session{clientid = ClientId, - client_pid = ClientPid}) -> +handle_info({'EXIT', ClientPid, Reason}, Session = #session{clientid = ClientId, + client_pid = ClientPid}) -> lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), - {noreply, start_expire_timer(State#session{client_pid = undefined})}; + {noreply, start_expire_timer(Session#session{client_pid = undefined})}; handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), @@ -491,51 +479,55 @@ handle_info(session_expired, State = #session{clientid = ClientId}) -> lager:warning("Session ~s expired!", [ClientId]), {stop, {shutdown, expired}, State}; -handle_info({timeout, awaiting_rel, MsgId}, SessState) -> - NewState = timeout(awaiting_rel, MsgId, SessState), - {noreply, NewState}; +handle_info({timeout, awaiting_rel, MsgId}, Session) -> + {noreply, timeout(awaiting_rel, MsgId, Session)}; -handle_info(Info, State) -> - lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), - {noreply, State}. +handle_info(Info, Session) -> + lager:critical("Unexpected Info: ~p, Session: ~p", [Info, Session]), + {noreply, Session}. -terminate(_Reason, _State) -> +terminate(_Reason, _Session) -> ok. -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - - - +code_change(_OldVsn, Session, _Extra) -> + {ok, Session}. %%%============================================================================= -%%% Internal functions +%%% Dispatch message from broker -> client. %%%============================================================================= -%% client is offline +%% queued the message if client is offline dispatch(Msg, Session = #session{client_pid = undefined}) -> queue(Msg, Session); -%% dispatch qos0 directly +%% dispatch qos0 directly to client process dispatch(Msg = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> ClientPid ! {dispatch, {self(), Msg}}, Session; -%% queue if inflight_queue is full -dispatch(Msg = #mqtt_message{qos = Qos}, Session = #session{inflight_window = InflightWin, - inflight_queue = InflightQ}) - when (Qos > ?QOS_0) andalso (length(InflightQ) >= InflightWin) -> - %%TODO: set alarms - lager:error([{clientid, ClientId}], "Session ~s inflight_queue is full!", [ClientId]), - queue(Msg, Session); - -%% dispatch and await ack -dispatch(Msg = #mqtt_message{qos = Qos}, Session = #session{client_pid = ClientPid}) +%% dispatch qos1/2 messages and wait for puback +dispatch(Msg = #mqtt_message{qos = Qos}, Session = #session{clientid = ClientId, + message_id = MsgId, + pending_queue = Q, + inflight_window = Win}) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - %% assign msgid and await - {NewMsg, NewState} = await_ack(Msg, Session), - ClientPid ! {dispatch, {self(), NewMsg}}, -queue(Msg, Session = #session{pending_queue = Queue}) -> + case emqttd_mqwin:is_full(InflightWin) of + true -> + lager:error("Session ~s inflight window is full!", [ClientId]), + Session#session{pending_queue = emqttd_mqueue:in(Msg, Q)}; + false -> + Msg1 = Msg#mqtt_message{msgid = MsgId}, + Msg2 = + if + Qos =:= ?QOS_2 -> Msg1#mqtt_message{dup = false}; + true -> Msg1 + end, + ClientPid ! {dispatch, {self(), Msg2}}, + NewWin = emqttd_mqwin:in(Msg2, Win), + await_ack(Msg2, next_msgid(Session#session{inflight_window = NewWin})) + end. + +queue(Msg, Session = #session{pending_queue= Queue}) -> Session#session{pending_queue = emqttd_mqueue:in(Msg, Queue)}. next_msgid(State = #session{message_id = 16#ffff}) -> @@ -544,8 +536,9 @@ next_msgid(State = #session{message_id = 16#ffff}) -> next_msgid(State = #session{message_id = MsgId}) -> State#session{message_id = MsgId + 1}. -start_expire_timer(State = #session{expires = Expires, expire_timer = OldTimer}) -> +start_expire_timer(Session = #session{expired_after = Expires, + expired_timer = OldTimer}) -> emqttd_util:cancel_timer(OldTimer), Timer = erlang:send_after(Expires * 1000, self(), session_expired), - State#session{expire_timer = Timer}. + Session#session{expired_timer = Timer}. From b152d6ba2d7976727825207f49dde69fa5435134 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 14 Jun 2015 08:10:47 +0800 Subject: [PATCH 068/104] Seq.png --- doc/design/Seq.graphml | 234 +++++++++++++++++++++++++++++++++++++++++ doc/design/Seq.png | Bin 0 -> 10980 bytes 2 files changed, 234 insertions(+) create mode 100644 doc/design/Seq.graphml create mode 100644 doc/design/Seq.png diff --git a/doc/design/Seq.graphml b/doc/design/Seq.graphml new file mode 100644 index 000000000..8ddc9353a --- /dev/null +++ b/doc/design/Seq.graphml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + T + + + + + + + + + + + + + + + + + + C1 + + + + + + + + + + + + + + + + + + S1 + + + + + + + + + + + + + + + + + + C2 + + + + + + + + + + + + + + + + + + S2 + + + + + + + + + + + + + + + + + + Queue + + + + + + + + + + + + + + + + + + Queue + + + + + + + + + + + + + + + + + + Dispatch + + + + + + + + + + + + + Deliver + + + + + + + + + + + + + Publish QoS1/2 + + + + + + + + + + + + + + + + + + + + + + + + + + Publish Qos0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/design/Seq.png b/doc/design/Seq.png new file mode 100644 index 0000000000000000000000000000000000000000..b77f682a172be1ef7ae508151b175908ebb0bc66 GIT binary patch literal 10980 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 xA6AYCPyw*r14N Date: Sun, 14 Jun 2015 08:24:17 +0800 Subject: [PATCH 069/104] fix issue #174 - add 'from' field to mqtt_message record --- apps/emqtt/include/emqtt.hrl | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl index bb17eef64..49a19cf62 100644 --- a/apps/emqtt/include/emqtt.hrl +++ b/apps/emqtt/include/emqtt.hrl @@ -56,17 +56,12 @@ -type mqtt_msgid() :: undefined | 1..16#ffff. -record(mqtt_message, { - %% topic is first for message may be retained - topic :: binary(), - %% clientid from - from :: binary() | atom(), - %% sender pid ?? - sender :: pid(), + topic :: binary(), %% topic published to + from :: mqtt_clientid() | atom(), %% from clientid qos = ?QOS_0 :: mqtt_qos(), retain = false :: boolean(), dup = false :: boolean(), - %% $SYS message flag - sys = false :: boolean(), + sys = false :: boolean(), %% $SYS flag msgid :: mqtt_msgid(), payload :: binary() }). From a0f90b3ac632a839e7f2503bfa17f76a932667c7 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 14 Jun 2015 19:24:03 +0800 Subject: [PATCH 070/104] upgrade session --- apps/emqtt/include/emqtt.hrl | 2 +- apps/emqtt/include/emqtt_packet.hrl | 2 +- apps/emqtt/src/emqtt_message.erl | 5 +- apps/emqttd/src/emqttd_bridge.erl | 4 +- apps/emqttd/src/emqttd_client.erl | 12 +- .../{emqttd_mqwin.erl => emqttd_inflight.erl} | 47 +- apps/emqttd/src/emqttd_mqueue.erl | 20 +- apps/emqttd/src/emqttd_msg_store.erl | 4 +- apps/emqttd/src/emqttd_protocol.erl | 85 ++- apps/emqttd/src/emqttd_pubsub.erl | 4 +- apps/emqttd/src/emqttd_session.erl | 557 +++++++++--------- apps/emqttd/src/emqttd_session_sup.erl | 15 +- apps/emqttd/src/emqttd_sm.erl | 56 +- apps/emqttd/src/emqttd_ws_client.erl | 11 +- rel/files/emqttd.config | 12 +- 15 files changed, 422 insertions(+), 414 deletions(-) rename apps/emqttd/src/{emqttd_mqwin.erl => emqttd_inflight.erl} (62%) diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl index 49a19cf62..e0a2aab63 100644 --- a/apps/emqtt/include/emqtt.hrl +++ b/apps/emqtt/include/emqtt.hrl @@ -57,7 +57,7 @@ -record(mqtt_message, { topic :: binary(), %% topic published to - from :: mqtt_clientid() | atom(), %% from clientid + from :: binary() | atom(), %% from clientid qos = ?QOS_0 :: mqtt_qos(), retain = false :: boolean(), dup = false :: boolean(), diff --git a/apps/emqtt/include/emqtt_packet.hrl b/apps/emqtt/include/emqtt_packet.hrl index 105a939cf..5a965e98c 100644 --- a/apps/emqtt/include/emqtt_packet.hrl +++ b/apps/emqtt/include/emqtt_packet.hrl @@ -162,7 +162,7 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, variable = Var}). -define(CONNACK_PACKET(ReturnCode), - #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{return_code = ReturnCode}}). -define(PUBLISH_PACKET(Qos, Topic, PacketId, Payload), diff --git a/apps/emqtt/src/emqtt_message.erl b/apps/emqtt/src/emqtt_message.erl index 54a46526c..61da1011e 100644 --- a/apps/emqtt/src/emqtt_message.erl +++ b/apps/emqtt/src/emqtt_message.erl @@ -32,7 +32,7 @@ -include("emqtt_packet.hrl"). --export([from_packet/1, to_packet/1]). +-export([from_packet/1, from_packet/2, to_packet/1]). -export([set_flag/1, set_flag/2, unset_flag/1, unset_flag/2]). @@ -70,6 +70,9 @@ from_packet(#mqtt_packet_connect{will_retain = Retain, dup = false, payload = Msg}. +from_packet(ClientId, Packet) -> + Msg = from_packet(Packet), Msg#mqtt_message{from = ClientId}. + %%------------------------------------------------------------------------------ %% @doc Message to packet %% @end diff --git a/apps/emqttd/src/emqttd_bridge.erl b/apps/emqttd/src/emqttd_bridge.erl index 26919b47f..7f2d8345d 100644 --- a/apps/emqttd/src/emqttd_bridge.erl +++ b/apps/emqttd/src/emqttd_bridge.erl @@ -106,11 +106,11 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({dispatch, {_From, Msg}}, State = #state{node = Node, status = down}) -> +handle_info({dispatch, Msg}, State = #state{node = Node, status = down}) -> lager:warning("Bridge Dropped Msg for ~p Down:~n~p", [Node, Msg]), {noreply, State}; -handle_info({dispatch, {_From, Msg}}, State = #state{node = Node, status = up}) -> +handle_info({dispatch, Msg}, State = #state{node = Node, status = up}) -> rpc:cast(Node, emqttd_pubsub, publish, [transform(Msg, State)]), {noreply, State}; diff --git a/apps/emqttd/src/emqttd_client.erl b/apps/emqttd/src/emqttd_client.erl index de5d65d45..a61b3373b 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/apps/emqttd/src/emqttd_client.erl @@ -106,16 +106,8 @@ handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState [emqttd_protocol:clientid(ProtoState), ConnName]), stop({shutdown, duplicate_id}, State); -%%TODO: ok?? -handle_info({dispatch, {From, Messages}}, #state{proto_state = ProtoState} = State) when is_list(Messages) -> - ProtoState1 = - lists:foldl(fun(Message, PState) -> - {ok, PState1} = emqttd_protocol:send({From, Message}, PState), PState1 - end, ProtoState, Messages), - {noreply, State#state{proto_state = ProtoState1}}; - -handle_info({dispatch, {From, Message}}, #state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:send({From, Message}, ProtoState), +handle_info({deliver, Message}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:send(Message, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = State) -> diff --git a/apps/emqttd/src/emqttd_mqwin.erl b/apps/emqttd/src/emqttd_inflight.erl similarity index 62% rename from apps/emqttd/src/emqttd_mqwin.erl rename to apps/emqttd/src/emqttd_inflight.erl index acc90bf41..1eb69de8f 100644 --- a/apps/emqttd/src/emqttd_mqwin.erl +++ b/apps/emqttd/src/emqttd_inflight.erl @@ -25,42 +25,47 @@ %%% @end %%%----------------------------------------------------------------------------- --module(emqttd_mqwin). +-module(emqttd_inflight). -author("Feng Lee "). --export([new/2, len/1, in/2, ack/2]). +-include_lib("emqtt/include/emqtt.hrl"). --define(WIN_SIZE, 100). +-export([new/2, is_full/1, len/1, in/2, ack/2]). --record(mqwin, {name, - w = [], %% window list - len = 0, %% current window len - size = ?WIN_SIZE}). +-define(MAX_SIZE, 100). --type mqwin() :: #mqwin{}. +-record(inflight, {name, q = [], len = 0, size = ?MAX_SIZE}). --export_type([mqwin/0]). +-type inflight() :: #inflight{}. -new(Name, Opts) -> - WinSize = emqttd_opts:g(inflight_window, Opts, ?WIN_SIZE), - #mqwin{name = Name, size = WinSize}. +-export_type([inflight/0]). -len(#mqwin{len = Len}) -> +new(Name, Max) -> + #inflight{name = Name, size = Max}. + +is_full(#inflight{size = 0}) -> + false; +is_full(#inflight{len = Len, size = Size}) when Len < Size -> + false; +is_full(_Inflight) -> + true. + +len(#inflight{len = Len}) -> Len. -in(_Msg, #mqwin{len = Len, size = Size}) +in(_Msg, #inflight{len = Len, size = Size}) when Len =:= Size -> {error, full}; -in(Msg, Win = #mqwin{w = W, len = Len}) -> - {ok, Win#mqwin{w = [Msg|W], len = Len +1}}. +in(Msg = #mqtt_message{msgid = MsgId}, Inflight = #inflight{q = Q, len = Len}) -> + {ok, Inflight#inflight{q = [{MsgId, Msg}|Q], len = Len +1}}. -ack(MsgId, QWin = #mqwin{w = W, len = Len}) -> - case lists:keyfind(MsgId, 2, W) of +ack(MsgId, Inflight = #inflight{q = Q, len = Len}) -> + case lists:keyfind(MsgId, 1, Q) of false -> - lager:error("qwin(~s) cannot find msgid: ~p", [MsgId]), QWin; + lager:error("Inflight(~s) cannot find msgid: ~p", [MsgId]), + Inflight; _Msg -> - QWin#mqwin{w = lists:keydelete(MsgId, 2, W), len = Len - 1} + Inflight#inflight{q = lists:keydelete(MsgId, 1, Q), len = Len - 1} end. - diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index 84381f036..6a37a707e 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -56,7 +56,7 @@ -export([new/2, name/1, is_empty/1, is_full/1, - len/1, in/2, out/2]). + len/1, in/2, out/1]). -define(LOW_WM, 0.2). @@ -112,22 +112,30 @@ len(#mqueue{len = Len}) -> Len. %% @doc Queue one message. %% @end %%------------------------------------------------------------------------------ --spec in(mqtt_message(), mqueue()) -> mqueue(). + +-spec in({new | old, mqtt_message()}, mqueue()) -> mqueue(). %% drop qos0 -in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> +in({_, #mqtt_message{qos = ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; %% simply drop the oldest one if queue is full, improve later -in(Msg, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen}) +in({new, Msg}, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen}) when Len =:= MaxLen -> {{value, OldMsg}, Q2} = queue:out(Q), lager:error("queue(~s) drop message: ~p", [Name, OldMsg]), MQ#mqueue{q = queue:in(Msg, Q2)}; -in(Msg, MQ = #mqueue{q = Q, len = Len}) -> +in({old, Msg}, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen}) + when Len =:= MaxLen -> + lager:error("queue(~s) drop message: ~p", [Name, Msg]), MQ; + +in({new, Msg}, MQ = #mqueue{q = Q, len = Len}) -> maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); +in({old, Msg}, MQ = #mqueue{q = Q, len = Len}) -> + MQ#mqueue{q = queue:in_r(Msg, Q), len = Len + 1}. + out(MQ = #mqueue{len = 0}) -> {empty, MQ}; @@ -143,7 +151,7 @@ maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm = f maybe_set_alarm(MQ) -> MQ. -maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_watermark = LowWM, alarm = true}) +maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm = true}) when Len =< LowWM -> emqttd_alarm:clear_alarm({queue_high_watermark, Name}), MQ#mqueue{alarm = false}; diff --git a/apps/emqttd/src/emqttd_msg_store.erl b/apps/emqttd/src/emqttd_msg_store.erl index 511b7cb22..c0b37cbc8 100644 --- a/apps/emqttd/src/emqttd_msg_store.erl +++ b/apps/emqttd/src/emqttd_msg_store.erl @@ -123,7 +123,7 @@ redeliver(Topic, CPid) when is_binary(Topic) andalso is_pid(CPid) -> dispatch(_CPid, []) -> ignore; dispatch(CPid, Msgs) when is_list(Msgs) -> - CPid ! {dispatch, {self(), [Msg || Msg <- Msgs]}}; + CPid ! {dispatch, [Msg || Msg <- Msgs]}; dispatch(CPid, Msg) when is_record(Msg, mqtt_message) -> - CPid ! {dispatch, {self(), Msg}}. + CPid ! {dispatch, Msg}. diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index bd500ab26..3626c6203 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -51,8 +51,7 @@ username, clientid, clean_sess, - sessmod, - session, %% session state or session pid + session, will_msg, max_clientid_len = ?MAX_CLIENTID_LEN, client_pid @@ -152,13 +151,13 @@ handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername} emqttd_cm:register(client(State2)), %%Starting session - {ok, SessMod, Session} = emqttd_sm:start_session(CleanSess, clientid(State2)), + {ok, Session} = emqttd_sm:start_session(CleanSess, clientid(State2)), %% Start keepalive start_keepalive(KeepAlive), %% ACCEPT - {?CONNACK_ACCEPT, State2#proto_state{sessmod = SessMod, session = Session, will_msg = willmsg(Var)}}; + {?CONNACK_ACCEPT, State2#proto_state{session = Session, will_msg = willmsg(Var)}}; {error, Reason}-> lager:error("~s@~s: username '~s', login failed - ~s", [ClientId, emqttd_net:format(Peername), Username, Reason]), @@ -177,7 +176,7 @@ handle(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), State = #proto_state{clientid = ClientId, session = Session}) -> case check_acl(publish, Topic, State) of allow -> - do_publish(Session, ClientId, ?QOS_0, Packet); + do_publish(Session, ClientId, Packet); deny -> lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]) end, @@ -187,7 +186,7 @@ handle(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), State = #proto_state{clientid = ClientId, session = Session}) -> case check_acl(publish, Topic, State) of allow -> - do_publish(Session, ClientId, ?QOS_1, Packet), + do_publish(Session, ClientId, Packet), send(?PUBACK_PACKET(?PUBACK, PacketId), State); deny -> lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]), @@ -198,26 +197,28 @@ handle(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), State = #proto_state{clientid = ClientId, session = Session}) -> case check_acl(publish, Topic, State) of allow -> - NewSession = do_publish(Session, ClientId, ?QOS_2, Packet), - send(?PUBACK_PACKET(?PUBREC, PacketId), State#proto_state{session = NewSession}); + do_publish(Session, ClientId, Packet), + send(?PUBACK_PACKET(?PUBREC, PacketId), State); deny -> lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]), {ok, State} end; -handle(?PUBACK_PACKET(Type, PacketId), State = #proto_state{session = Session}) - when Type >= ?PUBACK andalso Type =< ?PUBCOMP -> - NewSession = emqttd_session:puback(Session, {Type, PacketId}), - NewState = State#proto_state{session = NewSession}, - if - Type =:= ?PUBREC -> - send(?PUBREL_PACKET(PacketId), NewState); - Type =:= ?PUBREL -> - send(?PUBACK_PACKET(?PUBCOMP, PacketId), NewState); - true -> - ok - end, - {ok, NewState}; +handle(?PUBACK_PACKET(?PUBACK, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:puback(Session, PacketId), + {ok, State}; + +handle(?PUBACK_PACKET(?PUBREC, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:pubrec(Session, PacketId), + send(?PUBREL_PACKET(PacketId), State); + +handle(?PUBACK_PACKET(?PUBREL, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:pubrel(Session, PacketId), + send(?PUBACK_PACKET(?PUBCOMP, PacketId), State); + +handle(?PUBACK_PACKET(?PUBCOMP, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:pubcomp(Session, PacketId), + {ok, State}; %% protect from empty topic list handle(?SUBSCRIBE_PACKET(PacketId, []), State) -> @@ -233,13 +234,13 @@ handle(?SUBSCRIBE_PACKET(PacketId, TopicTable), State = #proto_state{clientid = false -> TopicTable1 = emqttd_broker:foldl_hooks(client_subscribe, [], TopicTable), %%TODO: GrantedQos should be renamed. - {ok, NewSession, GrantedQos} = emqttd_session:subscribe(Session, TopicTable1), - send(?SUBACK_PACKET(PacketId, GrantedQos), State#proto_state{session = NewSession}) + {ok, GrantedQos} = emqttd_session:subscribe(Session, TopicTable1), + send(?SUBACK_PACKET(PacketId, GrantedQos), State) end; -handle({subscribe, Topic, Qos}, State = #proto_state{session = Session}) -> - {ok, NewSession, _GrantedQos} = emqttd_session:subscribe(Session, [{Topic, Qos}]), - {ok, State#proto_state{session = NewSession}}; +handle({subscribe, TopicTable}, State = #proto_state{session = Session}) -> + {ok, _GrantedQos} = emqttd_session:subscribe(Session, TopicTable), + {ok, State}; %% protect from empty topic list handle(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> @@ -247,34 +248,24 @@ handle(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> handle(?UNSUBSCRIBE_PACKET(PacketId, Topics), State = #proto_state{session = Session}) -> Topics1 = emqttd_broker:foldl_hooks(client_unsubscribe, [], Topics), - {ok, NewSession} = emqttd_session:unsubscribe(Session, Topics1), - send(?UNSUBACK_PACKET(PacketId), State#proto_state{session = NewSession}); + ok = emqttd_session:unsubscribe(Session, Topics1), + send(?UNSUBACK_PACKET(PacketId), State); handle(?PACKET(?PINGREQ), State) -> send(?PACKET(?PINGRESP), State); handle(?PACKET(?DISCONNECT), State) -> - %%TODO: how to handle session? % clean willmsg {stop, normal, State#proto_state{will_msg = undefined}}. -do_publish(Session, ClientId, Qos, Packet) -> - Message = emqttd_broker:foldl_hooks(client_publish, [], emqtt_message:from_packet(Packet)), - emqttd_session:publish(Session, ClientId, {Qos, Message}). +do_publish(Session, ClientId, Packet) -> + Msg = emqtt_message:from_packet(ClientId, Packet), + Msg1 = emqttd_broker:foldl_hooks(client_publish, [], Msg), + emqttd_session:publish(Session, Msg1). --spec send({pid() | tuple(), mqtt_message()} | mqtt_packet(), proto_state()) -> {ok, proto_state()}. -%% qos0 message -send({_From, Message = #mqtt_message{qos = ?QOS_0}}, State) -> - send(emqtt_message:to_packet(Message), State); - -%% message from session -send({_From = SessPid, Message}, State = #proto_state{session = SessPid}) when is_pid(SessPid) -> - send(emqtt_message:to_packet(Message), State); -%% message(qos1, qos2) not from session -send({_From, Message = #mqtt_message{qos = Qos}}, State = #proto_state{session = Session}) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - {Message1, NewSession} = emqttd_session:await_ack(Session, Message), - send(emqtt_message:to_packet(Message1), State#proto_state{session = NewSession}); +-spec send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}. +send(Msg, State) when is_record(Msg, mqtt_message) -> + send(emqtt_message:to_packet(Msg), State); send(Packet, State = #proto_state{sendfun = SendFun, peername = Peername}) when is_record(Packet, mqtt_packet) -> trace(send, Packet, State), @@ -331,8 +322,8 @@ clientid(ClientId, _State) -> ClientId. send_willmsg(_ClientId, undefined) -> ignore; %%TODO:should call session... -send_willmsg(ClientId, WillMsg) -> - emqttd_pubsub:publish(ClientId, WillMsg). +send_willmsg(ClientId, WillMsg) -> + emqttd_pubsub:publish(WillMsg#mqtt_message{from = ClientId}). start_keepalive(0) -> ignore; diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/apps/emqttd/src/emqttd_pubsub.erl index b89355c76..51fcc3c1e 100644 --- a/apps/emqttd/src/emqttd_pubsub.erl +++ b/apps/emqttd/src/emqttd_pubsub.erl @@ -177,7 +177,7 @@ publish(From, <<"$Q/", _/binary>> = Queue, #mqtt_message{qos = Qos} = Msg) -> Qos > SubQos -> Msg#mqtt_message{qos = SubQos}; true -> Msg end, - SubPid ! {dispatch, {self(), Msg1}} + SubPid ! {dispatch, Msg1} end, mnesia:dirty_read(queue, Queue)); publish(_From, Topic, Msg) when is_binary(Topic) -> @@ -202,7 +202,7 @@ dispatch(Topic, #mqtt_message{qos = Qos} = Msg ) when is_binary(Topic) -> Qos > SubQos -> Msg#mqtt_message{qos = SubQos}; true -> Msg end, - SubPid ! {dispatch, {self(), Msg1}} + SubPid ! {dispatch, Msg1} end, Subscribers), length(Subscribers). diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 01a910e3c..4cd480165 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -53,31 +53,26 @@ -include_lib("emqtt/include/emqtt_packet.hrl"). -%% Start gen_server --export([start_link/2, resume/3, destroy/2]). - -%% Init Session State --export([new/1]). +%% Session API +-export([start_link/3, resume/3, destroy/2]). %% PubSub APIs --export([publish/3, - puback/2, - subscribe/2, - unsubscribe/2, - await/2, - dispatch/2]). +-export([publish/2, + puback/2, pubrec/2, pubrel/2, pubcomp/2, + subscribe/2, unsubscribe/2]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(session, { - %% ClientId: Identifier of Session - clientid :: binary(), %% Clean Session Flag clean_sess = true, + %% ClientId: Identifier of Session + clientid :: binary(), + %% Client Pid linked with session client_pid :: pid(), @@ -91,7 +86,7 @@ %% QoS 1 and QoS 2 messages which have been sent to the Client, %% but have not been completely acknowledged. %% Client <- Broker - inflight_window :: emqttd_mqwin:mqwin(), + inflight_queue :: emqttd_inflight:inflight(), %% All qos1, qos2 messages published to when client is disconnected. %% QoS 1 and QoS 2 messages pending transmission to the Client. @@ -129,167 +124,121 @@ timestamp}). --type session() :: #session{}. - -%%%============================================================================= -%%% Session API -%%%============================================================================= - %%------------------------------------------------------------------------------ -%% @doc Start a session process. +%% @doc Start a session. %% @end %%------------------------------------------------------------------------------ -start_link(ClientId, ClientPid) -> - gen_server:start_link(?MODULE, [ClientId, ClientPid], []). +start_link(CleanSess, ClientId, ClientPid) -> + gen_server:start_link(?MODULE, [CleanSess, ClientId, ClientPid], []). %%------------------------------------------------------------------------------ %% @doc Resume a session. %% @end %%------------------------------------------------------------------------------ -resume(Session, _ClientId, _ClientPid) when is_record(Session, session) -> - Session; -resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> - gen_server:cast(SessPid, {resume, ClientId, ClientPid}), SessPid. +resume(Session, ClientId, ClientPid) when is_pid(Session) -> + gen_server:cast(Session, {resume, ClientId, ClientPid}). %%------------------------------------------------------------------------------ %% @doc Destroy a session. %% @end %%------------------------------------------------------------------------------ --spec destroy(SessPid :: pid(), ClientId :: binary()) -> ok. -destroy(SessPid, ClientId) when is_pid(SessPid) -> - gen_server:cast(SessPid, {destroy, ClientId}), SessPid. +-spec destroy(Session:: pid(), ClientId :: binary()) -> ok. +destroy(Session, ClientId) when is_pid(Session) -> + gen_server:call(Session, {destroy, ClientId}). %%------------------------------------------------------------------------------ -%% @doc Init Session State. +%% @doc Publish message %% @end %%------------------------------------------------------------------------------ --spec new(binary()) -> session(). -new(ClientId) -> +-spec publish(Session :: pid(), {mqtt_qos(), mqtt_message()}) -> ok. +publish(Session, Msg = #mqtt_message{qos = ?QOS_0}) when is_pid(Session) -> + %% publish qos0 directly + emqttd_pubsub:publish(Msg); + +publish(Session, Msg = #mqtt_message{qos = ?QOS_1}) when is_pid(Session) -> + %% publish qos1 directly, and client will puback + emqttd_pubsub:publish(Msg); + +publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) when is_pid(Session) -> + %% publish qos2 by session + gen_server:cast(Session, {publish, Msg}). + +%%------------------------------------------------------------------------------ +%% @doc PubAck message +%% @end +%%------------------------------------------------------------------------------ +-spec puback(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. +puback(Session, MsgId) when is_pid(Session) -> + gen_server:cast(Session, {puback, MsgId}). + +-spec pubrec(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. +pubrec(Session, MsgId) when is_pid(Session) -> + gen_server:cast(Session, {pubrec, MsgId}). + +-spec pubrel(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. +pubrel(Session, MsgId) when is_pid(Session) -> + gen_server:cast(Session, {pubrel, MsgId}). + +-spec pubcomp(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. +pubcomp(Session, MsgId) when is_pid(Session) -> + gen_server:cast(Session, {pubcomp, MsgId}). + +%%------------------------------------------------------------------------------ +%% @doc Subscribe Topics +%% @end +%%------------------------------------------------------------------------------ +-spec subscribe(Session :: pid(), [{binary(), mqtt_qos()}]) -> {ok, [mqtt_qos()]}. +subscribe(Session, Topics) when is_pid(Session) -> + gen_server:call(Session, {subscribe, Topics}). + +%%------------------------------------------------------------------------------ +%% @doc Unsubscribe Topics +%% @end +%%------------------------------------------------------------------------------ +-spec unsubscribe(Session :: pid(), [Topic :: binary()]) -> ok. +unsubscribe(Session, Topics) when is_pid(Session) -> + gen_server:call(Session, {unsubscribe, Topics}). + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= +init([CleanSess, ClientId, ClientPid]) -> + if + CleanSess =:= false -> + process_flag(trap_exit, true), + true = link(ClientPid); + CleanSess =:= true -> + ok + end, QEnv = emqttd:env(mqtt, queue), SessEnv = emqttd:env(mqtt, session), - #session{ + PendingQ = emqttd_mqueue:new(ClientId, QEnv), + InflightQ = emqttd_inflight:new(ClientId, emqttd_opts:g(max_inflight, SessEnv)), + Session = #session{ + clean_sess = CleanSess, clientid = ClientId, - clean_sess = true, + client_pid = ClientPid, subscriptions = [], - inflight_window = emqttd_mqwin:new(ClientId, QEnv), - pending_queue = emqttd_mqueue:new(ClientId, QEnv), - awaiting_rel = #{}, + inflight_queue = InflightQ, + pending_queue = PendingQ, + awaiting_rel = #{}, awaiting_ack = #{}, awaiting_comp = #{}, unack_retries = emqttd_opts:g(unack_retries, SessEnv), unack_timeout = emqttd_opts:g(unack_timeout, SessEnv), await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv), max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv), - expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600 - }. + expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600, + timestamp = os:timestamp() + }, + {ok, Session, hibernate}. -%%------------------------------------------------------------------------------ -%% @doc Publish message -%% @end -%%------------------------------------------------------------------------------ --spec publish(session() | pid(), mqtt_clientid(), {mqtt_qos(), mqtt_message()}) -> session() | pid(). -publish(Session, ClientId, {?QOS_0, Message}) -> - %% publish qos0 directly - emqttd_pubsub:publish(ClientId, Message), Session; - -publish(Session, ClientId, {?QOS_1, Message}) -> - %% publish qos1 directly, and client will puback - emqttd_pubsub:publish(ClientId, Message), Session; - -publish(Session = #session{awaiting_rel = AwaitingRel, - await_rel_timeout = Timeout, - max_awaiting_rel = MaxLen}, ClientId, - {?QOS_2, Message = #mqtt_message{msgid = MsgId}}) -> - case maps:size(AwaitingRel) >= MaxLen of - true -> lager:error([{clientid, ClientId}], "Session ~s " - " dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]); - false -> - %% store in awaiting_rel - TRef = erlang:send_after(Timeout * 1000, self(), {timeout, awaiting_rel, MsgId}), - Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}; - end; -publish(SessPid, ClientId, {?QOS_2, Message}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {publish, ClientId, {?QOS_2, Message}}), SessPid. - -%%------------------------------------------------------------------------------ -%% @doc PubAck message -%% @end -%%------------------------------------------------------------------------------ - --spec puback(session(), {mqtt_packet_type(), mqtt_packet_id()}) -> session(). -puback(Session = #session{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> - case maps:is_key(PacketId, Awaiting) of - true -> ok; - false -> lager:warning("Session ~s: PUBACK PacketId '~p' not found!", [ClientId, PacketId]) - end, - Session#session{awaiting_ack = maps:remove(PacketId, Awaiting)}; - -puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, {?PUBACK, PacketId}); - -%% PUBREC -puback(Session = #session{clientid = ClientId, - awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp}, {?PUBREC, PacketId}) -> - case maps:is_key(PacketId, AwaitingAck) of - true -> ok; - false -> lager:warning("Session ~s: PUBREC PacketId '~p' not found!", [ClientId, PacketId]) - end, - Session#session{awaiting_ack = maps:remove(PacketId, AwaitingAck), - awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; - -puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, {?PUBREC, PacketId}); - -%% PUBREL -puback(Session = #session{clientid = ClientId, awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> - case maps:find(PacketId, Awaiting) of - {ok, {Msg, TRef}} -> - catch erlang:cancel_timer(TRef), - emqttd_pubsub:publish(ClientId, Msg); - error -> - lager:error("Session ~s cannot find PUBREL PacketId '~p'!", [ClientId, PacketId]) - end, - Session#session{awaiting_rel = maps:remove(PacketId, Awaiting)}; - -puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, {?PUBREL, PacketId}); - -%% PUBCOMP -puback(Session = #session{clientid = ClientId, - awaiting_comp = AwaitingComp}, {?PUBCOMP, PacketId}) -> - case maps:is_key(PacketId, AwaitingComp) of - true -> ok; - false -> lager:warning("Session ~s: PUBREC PacketId '~p' not exist", [ClientId, PacketId]) - end, - Session#session{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; - -puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, {?PUBCOMP, PacketId}); - -wait_ack - -timeout(awaiting_rel, MsgId, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> - case maps:find(MsgId, Awaiting) of - {ok, {Msg, _TRef}} -> - lager:error([{client, ClientId}], "Session ~s Awaiting Rel Timout!~nDrop Message:~p", [ClientId, Msg]), - Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}; - error -> - lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting Rel: MsgId=~p", [ClientId, MsgId]), - Session - end. - -%%------------------------------------------------------------------------------ -%% @doc Subscribe Topics -%% @end -%%------------------------------------------------------------------------------ --spec subscribe(session() | pid(), [{binary(), mqtt_qos()}]) -> {ok, session() | pid(), [mqtt_qos()]}. -subscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> +handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId, subscriptions = Subscriptions}) -> %% subscribe first and don't care if the subscriptions have been existed {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), - lager:info([{client, ClientId}], "Client ~s subscribe ~p. Granted QoS: ~p", + lager:info([{client, ClientId}], "Session ~s subscribe ~p. Granted QoS: ~p", [ClientId, Topics, GrantedQos]), Subscriptions1 = @@ -310,19 +259,9 @@ subscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions} [{Topic, Qos} | Acc] end end, Subscriptions, Topics), + {reply, {ok, GrantedQos}, Session#session{subscriptions = Subscriptions1}}; - {ok, Session#session{subscriptions = Subscriptions1}, GrantedQos}; - -subscribe(SessPid, Topics) when is_pid(SessPid) -> - {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), - {ok, SessPid, GrantedQos}. - -%%------------------------------------------------------------------------------ -%% @doc Unsubscribe Topics -%% @end -%%------------------------------------------------------------------------------ --spec unsubscribe(session() | pid(), [binary()]) -> {ok, session() | pid()}. -unsubscribe(Session = #session{clientid = ClientId, subscriptions = Subscriptions}, Topics) -> +handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId, subscriptions = Subscriptions}) -> %%unsubscribe from topic tree ok = emqttd_pubsub:unsubscribe(Topics), @@ -338,63 +277,11 @@ unsubscribe(Session = #session{clientid = ClientId, subscriptions = Subscription end end, Subscriptions, Topics), - {ok, Session#session{subscriptions = Subscriptions1}}; + {reply, ok, Session#session{subscriptions = Subscriptions1}}; -unsubscribe(SessPid, Topics) when is_pid(SessPid) -> - gen_server:call(SessPid, {unsubscribe, Topics}), - {ok, SessPid}. - -%%------------------------------------------------------------------------------ -%% @doc Destroy Session -%% @end -%%------------------------------------------------------------------------------ - -% message(qos1) is awaiting ack -await_ack(Msg = #mqtt_message{qos = ?QOS_1}, Session = #session{message_id = MsgId, - inflight_queue = InflightQ, - awaiting_ack = Awaiting, - unack_retry_after = Time, - max_unack_retries = Retries}) -> - %% assign msgid before send - Msg1 = Msg#mqtt_message{msgid = MsgId}, - TRef = erlang:send_after(Time * 1000, self(), {retry, MsgId}), - Awaiting1 = maps:put(MsgId, {TRef, Retries, Time}, Awaiting), - {Msg1, next_msgid(Session#session{inflight_queue = [{MsgId, Msg1} | InflightQ], - awaiting_ack = Awaiting1})}. - -% message(qos2) is awaiting ack -await_ack(Message = #mqtt_message{qos = Qos}, Session = #session{message_id = MsgId, awaiting_ack = Awaiting},) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - %%assign msgid before send - Message1 = Message#mqtt_message{msgid = MsgId, dup = false}, - Message2 = - if - Qos =:= ?QOS_2 -> Message1#mqtt_message{dup = false}; - true -> Message1 - end, - Awaiting1 = maps:put(MsgId, Message2, Awaiting), - {Message1, next_msgid(Session#session{awaiting_ack = Awaiting1})}. - - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= -init([ClientId, ClientPid]) -> - process_flag(trap_exit, true), - true = link(ClientPid), - Session = emqttd_session:new(ClientId), - {ok, Session#session{clean_sess = false, - client_pid = ClientPid, - timestamp = os:timestamp()}, hibernate}. - - -handle_call({subscribe, Topics}, _From, Session) -> - {ok, NewSession, GrantedQos} = subscribe(Session, Topics), - {reply, {ok, GrantedQos}, NewSession}; - -handle_call({unsubscribe, Topics}, _From, Session) -> - {ok, NewSession} = unsubscribe(Session, Topics), - {reply, ok, NewSession}; +handle_call({destroy, ClientId}, _From, Session = #session{clientid = ClientId}) -> + lager:warning("Session ~s destroyed", [ClientId]), + {stop, {shutdown, destroy}, ok, Session}; handle_call(Req, _From, State) -> lager:error("Unexpected Request: ~p", [Req]), @@ -403,10 +290,10 @@ handle_call(Req, _From, State) -> handle_cast({resume, ClientId, ClientPid}, State = #session{ clientid = ClientId, client_pid = OldClientPid, - msg_queue = Queue, + pending_queue = Queue, awaiting_ack = AwaitingAck, awaiting_comp = AwaitingComp, - expire_timer = ETimer}) -> + expired_timer = ETimer}) -> lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), @@ -426,8 +313,8 @@ handle_cast({resume, ClientId, ClientPid}, State = #session{ emqttd_util:cancel_timer(ETimer), %% redelivery PUBREL - lists:foreach(fun(PacketId) -> - ClientPid ! {redeliver, {?PUBREL, PacketId}} + lists:foreach(fun(MsgId) -> + ClientPid ! {redeliver, {?PUBREL, MsgId}} end, maps:keys(AwaitingComp)), %% redelivery messages that awaiting PUBACK or PUBREC @@ -442,45 +329,114 @@ handle_cast({resume, ClientId, ClientPid}, State = #session{ end, emqttd_queue:all(Queue)), {noreply, State#session{client_pid = ClientPid, - msg_queue = emqttd_queue:clear(Queue), - expire_timer = undefined}, hibernate}; + %%TODO: + pending_queue = emqttd_queue:clear(Queue), + expired_timer = undefined}, hibernate}; -handle_cast({publish, ClientId, {?QOS_2, Message}}, Session) -> - {noreply, publish(Session, ClientId, {?QOS_2, Message})}; +handle_cast({publish, Message = #mqtt_message{qos = ?QOS_2}}, Session) -> + {noreply, publish_qos2(Message, Session)}; -handle_cast({puback, {PubAck, PacketId}, Session) -> - {noreply, puback(Session, {PubAck, PacketId})}; -handle_cast({destroy, ClientId}, Session = #session{clientid = ClientId}) -> - lager:warning("Session ~s destroyed", [ClientId]), - {stop, normal, Session}; +handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, inflight_queue = Q, awaiting_ack = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, {_, TRef}} -> + catch erlang:cancel_timer(TRef), + {noreply, dispatch(Session#session{inflight_queue = emqttd_inflight:ack(MsgId, Q), + awaiting_ack = maps:remove(MsgId, Awaiting)})}; + error -> + lager:error("Session ~s cannot find PUBACK '~p'!", [ClientId, MsgId]), + {noreply, Session} + end; + +%% PUBREC +handle_cast({pubrec, MsgId}, Session = #session{clientid = ClientId, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + await_rel_timeout = Timeout}) -> + case maps:find(MsgId, AwaitingAck) of + {ok, {_, TRef}} -> + catch erlang:cancel_timer(TRef), + TRef1 = timer(Timeout, {timeout, awaiting_comp, MsgId}), + {noreply, dispatch(Session#session{awaiting_ack = maps:remove(MsgId, AwaitingAck), + awaiting_comp = maps:put(MsgId, TRef1, AwaitingComp)})}; + error -> + lager:error("Session ~s cannot find PUBREC '~p'!", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, {Msg, TRef}} -> + catch erlang:cancel_timer(TRef), + emqttd_pubsub:publish(Msg), + {noreply, Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}}; + error -> + lager:error("Session ~s cannot find PUBREL'~p'!", [ClientId, MsgId]), + {noreply, Session} + end; + +%% PUBCOMP +handle_cast({pubcomp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = AwaitingComp}) -> + case maps:is_key(MsgId, AwaitingComp) of + true -> + {noreply, Session#session{awaiting_comp = maps:remove(MsgId, AwaitingComp)}}; + false -> + lager:error("Session ~s cannot find PUBREC MsgId '~p'", [ClientId, MsgId]), + {noreply, Session} + end; handle_cast(Msg, State) -> - lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), + lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), {noreply, State}. -handle_info({dispatch, {_From, Messages}}, Session) when is_list(Messages) -> - F = fun(Message, S) -> dispatch(Message, S) end, - {noreply, lists:foldl(F, Session, Messages)}; +handle_info({dispatch, MsgList}, Session) when is_list(MsgList) -> + NewSession = lists:foldl(fun(Msg, S) -> + dispatch({new, Msg}, S) + end, Session, MsgList), + {noreply, NewSession}; -handle_info({dispatch, {_From, Message}}, State) -> - {noreply, dispatch(Message, State)}; +handle_info({dispatch, {old, Msg}}, Session) when is_record(Msg, mqtt_message) -> + {noreply, dispatch({old, Msg}, Session)}; -handle_info({'EXIT', ClientPid, Reason}, Session = #session{clientid = ClientId, - client_pid = ClientPid}) -> - lager:info("Session: client ~s@~p exited for ~p", [ClientId, ClientPid, Reason]), - {noreply, start_expire_timer(Session#session{client_pid = undefined})}; +handle_info({dispatch, Msg}, Session) when is_record(Msg, mqtt_message) -> + {noreply, dispatch({new, Msg}, Session)}; + +handle_info({'EXIT', ClientPid, Reason}, Session = #session{clean_sess = false, + clientid = ClientId, + client_pid = ClientPid, + expired_after = Expires}) -> + %%TODO: Clean puback, pubrel, pubcomp timers + lager:info("Session ~s: client ~p exited for ~p", [ClientId, ClientPid, Reason]), + TRef = timer(Expires * 1000, session_expired), + {noreply, Session#session{expired_timer = TRef}}; handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> - lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), + lager:critical("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), {noreply, State}; handle_info(session_expired, State = #session{clientid = ClientId}) -> - lager:warning("Session ~s expired!", [ClientId]), + lager:error("Session ~s expired, shutdown now!", [ClientId]), {stop, {shutdown, expired}, State}; -handle_info({timeout, awaiting_rel, MsgId}, Session) -> - {noreply, timeout(awaiting_rel, MsgId, Session)}; +handle_info({timeout, awaiting_rel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, {Msg, _TRef}} -> + lager:error([{client, ClientId}], "Session ~s Awaiting Rel Timout!~nDrop Message:~p", [ClientId, Msg]), + {noreply, Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}}; + error -> + lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting Rel: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({timeout, awaiting_comp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, _TRef} -> + lager:error([{client, ClientId}], "Session ~s Awaiting PUBCOMP Timout: MsgId=~p!", [ClientId, MsgId]), + {noreply, Session#session{awaiting_comp = maps:remove(MsgId, Awaiting)}}; + error -> + lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting PUBCOMP: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; handle_info(Info, Session) -> lager:critical("Unexpected Info: ~p, Session: ~p", [Info, Session]), @@ -492,53 +448,106 @@ terminate(_Reason, _Session) -> code_change(_OldVsn, Session, _Extra) -> {ok, Session}. +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Plubish Qos2 message from client -> broker, and then wait for pubrel. +%% @end +%%------------------------------------------------------------------------------ + +publish_qos2(Message = #mqtt_message{qos = ?QOS_2,msgid = MsgId}, Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}) -> + + case check_awaiting_rel(Session) of + true -> + TRef = timer(Timeout, {timeout, awaiting_rel, MsgId}), + Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}; + false -> + lager:error([{clientid, ClientId}], "Session ~s " + " dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]), + Session + end. + +check_awaiting_rel(#session{max_awaiting_rel = 0}) -> + true; +check_awaiting_rel(#session{awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxLen}) -> + maps:size(AwaitingRel) < MaxLen. + %%%============================================================================= %%% Dispatch message from broker -> client. %%%============================================================================= +dispatch(Session = #session{client_pid = undefined}) -> + %% do nothing + Session; + +dispatch(Session = #session{pending_queue = PendingQ}) -> + case emqttd_mqueue:out(PendingQ) of + {empty, _Q} -> + Session; + {{value, Msg}, Q1} -> + self() ! {dispatch, {old, Msg}}, + Session#session{pending_queue = Q1} + end. + %% queued the message if client is offline -dispatch(Msg, Session = #session{client_pid = undefined}) -> - queue(Msg, Session); +dispatch({Type, Msg}, Session = #session{client_pid = undefined, + pending_queue= PendingQ}) -> + Session#session{pending_queue = emqttd_mqueue:in({Type, Msg}, PendingQ)}; %% dispatch qos0 directly to client process -dispatch(Msg = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> - ClientPid ! {dispatch, {self(), Msg}}, Session; +dispatch({_Type, Msg} = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> + ClientPid ! {deliver, Msg}, Session; -%% dispatch qos1/2 messages and wait for puback -dispatch(Msg = #mqtt_message{qos = Qos}, Session = #session{clientid = ClientId, - message_id = MsgId, - pending_queue = Q, - inflight_window = Win}) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - - case emqttd_mqwin:is_full(InflightWin) of - true -> - lager:error("Session ~s inflight window is full!", [ClientId]), - Session#session{pending_queue = emqttd_mqueue:in(Msg, Q)}; - false -> - Msg1 = Msg#mqtt_message{msgid = MsgId}, - Msg2 = - if - Qos =:= ?QOS_2 -> Msg1#mqtt_message{dup = false}; - true -> Msg1 - end, - ClientPid ! {dispatch, {self(), Msg2}}, - NewWin = emqttd_mqwin:in(Msg2, Win), - await_ack(Msg2, next_msgid(Session#session{inflight_window = NewWin})) +%% dispatch qos1/2 message and wait for puback +dispatch({Type, Msg = #mqtt_message{qos = Qos}}, Session = #session{clientid = ClientId, + client_pid = ClientPid, + message_id = MsgId, + pending_queue = PendingQ, + inflight_queue= InflightQ}) + when Qos =:= ?QOS_1 orelse Qos =:= ?QOS_2 -> + %% assign id first + Msg1 = Msg#mqtt_message{msgid = MsgId}, + Msg2 = + if + Qos =:= ?QOS_1 -> Msg1; + Qos =:= ?QOS_2 -> Msg1#mqtt_message{dup = false} + end, + case emqttd_inflight:in(Msg1, InflightQ) of + {error, full} -> + lager:error("Session ~s inflight queue is full!", [ClientId]), + Session#session{pending_queue = emqttd_mqueue:in({Type, Msg}, PendingQ)}; + {ok, InflightQ1} -> + ClientPid ! {deliver, Msg1}, + await_ack(Msg1, next_msgid(Session#session{inflight_queue = InflightQ1})) end. -queue(Msg, Session = #session{pending_queue= Queue}) -> - Session#session{pending_queue = emqttd_mqueue:in(Msg, Queue)}. +deliver(Msg, Session) -> + ok. -next_msgid(State = #session{message_id = 16#ffff}) -> - State#session{message_id = 1}; +await(Msg, Session) -> + ok. -next_msgid(State = #session{message_id = MsgId}) -> - State#session{message_id = MsgId + 1}. +% message(qos1/2) is awaiting ack +await_ack(Msg = #mqtt_message{msgid = MsgId}, Session = #session{awaiting_ack = Awaiting, + unack_retries = Retries, + unack_timeout = Timeout}) -> + + TRef = timer(Timeout * 1000, {retry, MsgId}), + Awaiting1 = maps:put(MsgId, {{Retries, Timeout}, TRef}, Awaiting), + Session#session{awaiting_ack = Awaiting1}. -start_expire_timer(Session = #session{expired_after = Expires, - expired_timer = OldTimer}) -> - emqttd_util:cancel_timer(OldTimer), - Timer = erlang:send_after(Expires * 1000, self(), session_expired), - Session#session{expired_timer = Timer}. +timer(Timeout, TimeoutMsg) -> + erlang:send_after(Timeout * 1000, self(), TimeoutMsg). + +next_msgid(Session = #session{message_id = 16#ffff}) -> + Session#session{message_id = 1}; + +next_msgid(Session = #session{message_id = MsgId}) -> + Session#session{message_id = MsgId + 1}. diff --git a/apps/emqttd/src/emqttd_session_sup.erl b/apps/emqttd/src/emqttd_session_sup.erl index d4f790847..9fe7f8ca3 100644 --- a/apps/emqttd/src/emqttd_session_sup.erl +++ b/apps/emqttd/src/emqttd_session_sup.erl @@ -30,7 +30,7 @@ -behavior(supervisor). --export([start_link/0, start_session/2]). +-export([start_link/0, start_session/3]). -export([init/1]). @@ -46,16 +46,17 @@ start_link() -> %% @doc Start a session %% @end %%------------------------------------------------------------------------------ --spec start_session(binary(), pid()) -> {ok, pid()}. -start_session(ClientId, ClientPid) -> - supervisor:start_child(?MODULE, [ClientId, ClientPid]). +-spec start_session(boolean(), binary(), pid()) -> {ok, pid()}. +start_session(CleanSess, ClientId, ClientPid) -> + supervisor:start_child(?MODULE, [CleanSess, ClientId, ClientPid]). %%%============================================================================= %%% Supervisor callbacks %%%============================================================================= init([]) -> - {ok, {{simple_one_for_one, 10, 10}, - [{session, {emqttd_session_proc, start_link, []}, - transient, 10000, worker, [emqttd_session_proc]}]}}. + {ok, {{simple_one_for_one, 0, 1}, + [{session, {emqttd_session, start_link, []}, + transient, 10000, worker, [emqttd_session]}]}}. + diff --git a/apps/emqttd/src/emqttd_sm.erl b/apps/emqttd/src/emqttd_sm.erl index 56c0ea9f9..f1e7041f2 100644 --- a/apps/emqttd/src/emqttd_sm.erl +++ b/apps/emqttd/src/emqttd_sm.erl @@ -90,20 +90,10 @@ table() -> ?SESSION_TAB. %% @end %%------------------------------------------------------------------------------ --spec start_session(CleanSess :: boolean(), binary()) -> {ok, module(), record() | pid()}. -start_session(true, ClientId) -> - %% destroy old session if existed - ok = destroy_session(ClientId), - {ok, emqttd_session, emqttd_session:new(ClientId)}; - -start_session(false, ClientId) -> - SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - case call(SmPid, {start_session, ClientId, self()}) of - {ok, SessPid} -> - {ok, emqttd_session_proc, SessPid}; - {error, Error} -> - {error, Error} - end. +-spec start_session(CleanSess :: boolean(), binary()) -> {ok, pid()} | {error, any()}. +start_session(CleanSess, ClientId) -> + SM = gproc_pool:pick_worker(?SM_POOL, ClientId), + call(SM, {start_session, {CleanSess, ClientId, self()}}). %%------------------------------------------------------------------------------ %% @doc Lookup Session Pid @@ -122,10 +112,10 @@ lookup_session(ClientId) -> %%------------------------------------------------------------------------------ -spec destroy_session(binary()) -> ok. destroy_session(ClientId) -> - SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - call(SmPid, {destroy_session, ClientId}). + SM = gproc_pool:pick_worker(?SM_POOL, ClientId), + call(SM, {destroy_session, ClientId}). -call(SmPid, Req) -> gen_server:call(SmPid, Req). +call(SM, Req) -> gen_server:call(SM, Req). %%%============================================================================= %%% gen_server callbacks @@ -135,23 +125,27 @@ init([Id, StatsFun]) -> gproc_pool:connect_worker(?SM_POOL, {?MODULE, Id}), {ok, #state{id = Id, statsfun = StatsFun}}. -handle_call({start_session, ClientId, ClientPid}, _From, State) -> +handle_call({start_session, {false, ClientId, ClientPid}}, _From, State) -> Reply = case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, _MRef}] -> - emqttd_session_proc:resume(SessPid, ClientId, ClientPid), + emqttd_session:resume(SessPid, ClientId, ClientPid), {ok, SessPid}; [] -> - case emqttd_session_sup:start_session(ClientId, ClientPid) of - {ok, SessPid} -> - ets:insert(?SESSION_TAB, {ClientId, SessPid, erlang:monitor(process, SessPid)}), - {ok, SessPid}; - {error, Error} -> - {error, Error} - end + new_session(false, ClientId, ClientPid) end, {reply, Reply, setstats(State)}; +handle_call({start_session, {true, ClientId, ClientPid}}, _From, State) -> + case ets:lookup(?SESSION_TAB, ClientId) of + [{_, SessPid, MRef}] -> + erlang:demonitor(MRef, [flush]), + emqttd_session:destroy_session(SessPid, ClientId); + [] -> + ok + end, + {reply, new_session(true, ClientId, ClientPid), setstats(State)}; + handle_call({destroy_session, ClientId}, _From, State) -> case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, MRef}] -> @@ -186,6 +180,16 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= +new_session(CleanSess, ClientId, ClientPid) -> + case emqttd_session_sup:start_session(CleanSess, ClientId, ClientPid) of + {ok, SessPid} -> + ets:insert(?SESSION_TAB, {ClientId, SessPid, erlang:monitor(process, SessPid)}), + {ok, SessPid}; + {error, Error} -> + {error, Error} + end. + setstats(State = #state{statsfun = StatsFun}) -> StatsFun(ets:info(?SESSION_TAB, size)), State. + diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/apps/emqttd/src/emqttd_ws_client.erl index 52a7ba7d9..ef7a3ad41 100644 --- a/apps/emqttd/src/emqttd_ws_client.erl +++ b/apps/emqttd/src/emqttd_ws_client.erl @@ -130,15 +130,8 @@ handle_cast({received, Packet}, State = #state{proto_state = ProtoState}) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({dispatch, {From, Messages}}, #state{proto_state = ProtoState} = State) when is_list(Messages) -> - ProtoState1 = - lists:foldl(fun(Message, PState) -> - {ok, PState1} = emqttd_protocol:send({From, Message}, PState), PState1 - end, ProtoState, Messages), - {noreply, State#state{proto_state = ProtoState1}}; - -handle_info({dispatch, {From, Message}}, #state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:send({From, Message}, ProtoState), +handle_info({deliver, Message}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:send(Message, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = State) -> diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 9995ddea1..e72a06862 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -90,6 +90,10 @@ %% Expired after 2 days {expired_after, 48}, + %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. + %% 0 means no limit + {max_inflight, 100}, + %% Max retries for unack Qos1/2 messages {unack_retries, 3}, @@ -99,16 +103,14 @@ %% Awaiting PUBREL Timeout {await_rel_timeout, 8}, - %% Max Packets that Awaiting PUBREL - {max_awaiting_rel, 100} + %% Max Packets that Awaiting PUBREL, 0 means no limit + {max_awaiting_rel, 0} + ]}, {queue, [ %% Max queue length {max_length, 1000}, - %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. - {inflight_window, 100}, - %% Low watermark of queued messsages {low_watermark, 0.2}, From 7bfc673c28abc56f18839ce6861925cf43c0d706 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 14 Jun 2015 23:51:07 +0800 Subject: [PATCH 071/104] publish willmsg when normal exit --- apps/emqttd/src/emqttd_protocol.erl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index d519a182e..2c07abbee 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -302,10 +302,16 @@ shutdown(duplicate_id, _State) -> shutdown(_, #proto_state{clientid = undefined}) -> ignore; -shutdown(normal, #proto_state{peername = Peername, clientid = ClientId}) -> +shutdown(normal, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> lager:info([{client, ClientId}], "Client ~s@~s: normal shutdown", [ClientId, emqttd_net:format(Peername)]), try_unregister(ClientId), + if + WillMsg =/= undefined -> + send_willmsg(ClientId, WillMsg); + true -> + ok + end, emqttd_broker:foreach_hooks(client_disconnected, [normal, ClientId]); shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> @@ -331,6 +337,7 @@ send_willmsg(_ClientId, undefined) -> ignore; %%TODO:should call session... send_willmsg(ClientId, WillMsg) -> + lager:info("Client ~s send willmsg: ~p", [ClientId, WillMsg]), emqttd_pubsub:publish(ClientId, WillMsg). start_keepalive(0) -> ignore; From afa0c1819b9f66c51f712bfbdf497619d8d98474 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Sun, 14 Jun 2015 23:56:19 +0800 Subject: [PATCH 072/104] fix issue #175 --- apps/emqttd/src/emqttd_ws_client.erl | 58 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/apps/emqttd/src/emqttd_ws_client.erl index 52a7ba7d9..4c2da9f8b 100644 --- a/apps/emqttd/src/emqttd_ws_client.erl +++ b/apps/emqttd/src/emqttd_ws_client.erl @@ -49,7 +49,10 @@ parser_state}). %% Client state --record(state, {ws_pid, request, proto_state, keepalive}). +-record(client_state, {ws_pid, + request, + proto_state, + keepalive}). %%------------------------------------------------------------------------------ %% @doc Start WebSocket client. @@ -109,80 +112,85 @@ init([WsPid, Req, ReplyChannel, PktOpts]) -> {ok, Peername} = emqttd_net:peername(Socket), SendFun = fun(Payload) -> ReplyChannel({binary, Payload}) end, ProtoState = emqttd_protocol:init(Peername, SendFun, PktOpts), - {ok, #state{ws_pid = WsPid, request = Req, proto_state = ProtoState}}. + {ok, #client_state{ws_pid = WsPid, request = Req, proto_state = ProtoState}}. handle_call(_Req, _From, State) -> {reply, error, State}. -handle_cast({received, Packet}, State = #state{proto_state = ProtoState}) -> +handle_cast({received, Packet}, State = #client_state{proto_state = ProtoState}) -> case emqttd_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; {error, Error} -> lager:error("MQTT protocol error ~p", [Error]), stop({shutdown, Error}, State); {error, Error, ProtoState1} -> - stop({shutdown, Error}, State#state{proto_state = ProtoState1}); + stop({shutdown, Error}, State#client_state{proto_state = ProtoState1}); {stop, Reason, ProtoState1} -> - stop(Reason, State#state{proto_state = ProtoState1}) + stop(Reason, State#client_state{proto_state = ProtoState1}) end; handle_cast(_Msg, State) -> {noreply, State}. -handle_info({dispatch, {From, Messages}}, #state{proto_state = ProtoState} = State) when is_list(Messages) -> +handle_info({dispatch, {From, Messages}}, #client_state{proto_state = ProtoState} = State) + when is_list(Messages) -> ProtoState1 = lists:foldl(fun(Message, PState) -> {ok, PState1} = emqttd_protocol:send({From, Message}, PState), PState1 end, ProtoState, Messages), - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({dispatch, {From, Message}}, #state{proto_state = ProtoState} = State) -> +handle_info({dispatch, {From, Message}}, #client_state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:send({From, Message}, ProtoState), - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = State) -> +handle_info({redeliver, {?PUBREL, PacketId}}, #client_state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({subscribe, Topic, Qos}, #state{proto_state = ProtoState} = State) -> +handle_info({subscribe, Topic, Qos}, #client_state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:handle({subscribe, Topic, Qos}, ProtoState), - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState}) -> +handle_info({stop, duplicate_id, _NewPid}, State=#client_state{proto_state = ProtoState}) -> lager:error("Shutdown for duplicate clientid: ~s", [emqttd_protocol:clientid(ProtoState)]), stop({shutdown, duplicate_id}, State); -handle_info({keepalive, start, TimeoutSec}, State = #state{request = Req}) -> +handle_info({keepalive, start, TimeoutSec}, State = #client_state{request = Req}) -> lager:debug("Client(WebSocket) ~s: Start KeepAlive with ~p seconds", [Req:get(peer), TimeoutSec]), %%TODO: fix esockd_transport... KeepAlive = emqttd_keepalive:new({esockd_transport, Req:get(socket)}, TimeoutSec, {keepalive, timeout}), - {noreply, State#state{keepalive = KeepAlive}}; + {noreply, State#client_state{keepalive = KeepAlive}}; -handle_info({keepalive, timeout}, State = #state{request = Req, keepalive = KeepAlive}) -> +handle_info({keepalive, timeout}, State = #client_state{request = Req, keepalive = KeepAlive}) -> case emqttd_keepalive:resume(KeepAlive) of timeout -> lager:debug("Client(WebSocket) ~s: Keepalive Timeout!", [Req:get(peer)]), - stop({shutdown, keepalive_timeout}, State#state{keepalive = undefined}); + stop({shutdown, keepalive_timeout}, State#client_state{keepalive = undefined}); {resumed, KeepAlive1} -> lager:debug("Client(WebSocket) ~s: Keepalive Resumed", [Req:get(peer)]), - {noreply, State#state{keepalive = KeepAlive1}} + {noreply, State#client_state{keepalive = KeepAlive1}} end; -handle_info({'EXIT', WsPid, Reason}, State = #state{ws_pid = WsPid}) -> - stop(Reason, State); +handle_info({'EXIT', WsPid, Reason}, State = #client_state{ws_pid = WsPid, proto_state = ProtoState}) -> + ClientId = emqttd_protocol:clientid(ProtoState), + lager:warning("Websocket client ~s exit: reason=~p", [ClientId, Reason]), + stop({shutdown, websocket_closed}, State); -handle_info(Info, State = #state{request = Req}) -> +handle_info(Info, State = #client_state{request = Req}) -> lager:critical("Client(WebSocket) ~s: Unexpected Info - ~p", [Req:get(peer), Info]), {noreply, State}. -terminate(Reason, #state{proto_state = ProtoState, keepalive = KeepAlive}) -> +terminate(Reason, #client_state{proto_state = ProtoState, keepalive = KeepAlive}) -> + lager:info("WebSocket client terminated: ~p", [Reason]), emqttd_keepalive:cancel(KeepAlive), case Reason of {shutdown, Error} -> emqttd_protocol:shutdown(Error, ProtoState); - _ -> ok + _ -> + emqttd_protocol:shutdown(Reason, ProtoState) end. code_change(_OldVsn, State, _Extra) -> From af3faf05db9fd43a9d64fd0840418e0deba2848d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 15 Jun 2015 00:04:39 +0800 Subject: [PATCH 073/104] 0.8.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b61e4a7..e969d6bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ emqttd ChangeLog ================== +0.8.6-beta (2015-06-15) +------------------------- + +Bugfix: issue #175 - should publish Will message when websocket is closed without 'DISCONNECT' packet + + 0.8.5-beta (2015-06-10) ------------------------- From d255a98c09384b817777042ef29543308c293777 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 15 Jun 2015 19:55:59 +0800 Subject: [PATCH 074/104] 0.9 --- apps/emqttd/src/emqttd_broker.erl | 14 ++++++++----- apps/emqttd/src/emqttd_client.erl | 4 ++-- apps/emqttd/src/emqttd_http.erl | 9 +++++---- apps/emqttd/src/emqttd_metrics.erl | 5 +++-- apps/emqttd/src/emqttd_mod_autosub.erl | 2 +- apps/emqttd/src/emqttd_mod_presence.erl | 12 ++++++----- apps/emqttd/src/emqttd_protocol.erl | 2 +- apps/emqttd/src/emqttd_session_sup.erl | 2 +- apps/emqttd/src/emqttd_sm.erl | 2 +- apps/emqttd/src/emqttd_stats.erl | 5 +++-- apps/emqttd/src/emqttd_ws_client.erl | 4 ++-- apps/zenmq/README | 14 ------------- apps/zenmq/src/zenmq.app.src | 12 ----------- apps/zenmq/src/zenmq.erl | 2 -- apps/zenmq/src/zenmq_app.erl | 16 --------------- apps/zenmq/src/zenmq_router.erl | 4 ---- apps/zenmq/src/zenmq_sup.erl | 27 ------------------------- 17 files changed, 35 insertions(+), 101 deletions(-) delete mode 100644 apps/zenmq/README delete mode 100644 apps/zenmq/src/zenmq.app.src delete mode 100644 apps/zenmq/src/zenmq.erl delete mode 100644 apps/zenmq/src/zenmq_app.erl delete mode 100644 apps/zenmq/src/zenmq_router.erl delete mode 100644 apps/zenmq/src/zenmq_sup.erl diff --git a/apps/emqttd/src/emqttd_broker.erl b/apps/emqttd/src/emqttd_broker.erl index 526d824b4..20bd27b45 100644 --- a/apps/emqttd/src/emqttd_broker.erl +++ b/apps/emqttd/src/emqttd_broker.erl @@ -280,20 +280,24 @@ create_topic(Topic) -> retain(brokers) -> Payload = list_to_binary(string:join([atom_to_list(N) || N <- running_nodes()], ",")), - publish(#mqtt_message{retain = true, topic = <<"$SYS/brokers">>, payload = Payload}). + publish(#mqtt_message{from = broker, + retain = true, + topic = <<"$SYS/brokers">>, + payload = Payload}). retain(Topic, Payload) when is_binary(Payload) -> - publish(#mqtt_message{retain = true, + publish(#mqtt_message{from = broker, + retain = true, topic = emqtt_topic:systop(Topic), payload = Payload}). publish(Topic, Payload) when is_binary(Payload) -> - publish( #mqtt_message{topic = emqtt_topic:systop(Topic), + publish( #mqtt_message{from = broker, + topic = emqtt_topic:systop(Topic), payload = Payload}). publish(Msg) -> - emqttd_pubsub:publish(broker, Msg). - + emqttd_pubsub:publish(Msg). uptime(#state{started_at = Ts}) -> Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, diff --git a/apps/emqttd/src/emqttd_client.erl b/apps/emqttd/src/emqttd_client.erl index a61b3373b..cbcfffb4b 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/apps/emqttd/src/emqttd_client.erl @@ -114,8 +114,8 @@ handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; -handle_info({subscribe, Topic, Qos}, #state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:handle({subscribe, Topic, Qos}, ProtoState), +handle_info({subscribe, TopicTable}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:handle({subscribe, TopicTable}, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; handle_info({inet_reply, _Ref, ok}, State) -> diff --git a/apps/emqttd/src/emqttd_http.erl b/apps/emqttd/src/emqttd_http.erl index 71c844ead..2c5c1a24f 100644 --- a/apps/emqttd/src/emqttd_http.erl +++ b/apps/emqttd/src/emqttd_http.erl @@ -53,10 +53,11 @@ handle_request('POST', "/mqtt/publish", Req) -> Message = list_to_binary(get_value("message", Params)), case {validate(qos, Qos), validate(topic, Topic)} of {true, true} -> - emqttd_pubsub:publish(http, #mqtt_message{qos = Qos, - retain = Retain, - topic = Topic, - payload = Message}), + emqttd_pubsub:publish(#mqtt_message{from = http, + qos = Qos, + retain = Retain, + topic = Topic, + payload = Message}), Req:ok({"text/plan", <<"ok\n">>}); {false, _} -> Req:respond({400, [], <<"Bad QoS">>}); diff --git a/apps/emqttd/src/emqttd_metrics.erl b/apps/emqttd/src/emqttd_metrics.erl index 550d5fcd7..84350f939 100644 --- a/apps/emqttd/src/emqttd_metrics.erl +++ b/apps/emqttd/src/emqttd_metrics.erl @@ -192,8 +192,9 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= publish(Metric, Val) -> - emqttd_pubsub:publish(metrics, #mqtt_message{topic = metric_topic(Metric), - payload = emqttd_util:integer_to_binary(Val)}). + emqttd_pubsub:publish(#mqtt_message{topic = metric_topic(Metric), + from = metrics, + payload = emqttd_util:integer_to_binary(Val)}). create_metric({gauge, Name}) -> ets:insert(?METRIC_TAB, {{Name, 0}, 0}); diff --git a/apps/emqttd/src/emqttd_mod_autosub.erl b/apps/emqttd/src/emqttd_mod_autosub.erl index a28db1642..8f7af60ac 100644 --- a/apps/emqttd/src/emqttd_mod_autosub.erl +++ b/apps/emqttd/src/emqttd_mod_autosub.erl @@ -49,7 +49,7 @@ load(Opts) -> client_connected(?CONNACK_ACCEPT, #mqtt_client{clientid = ClientId, client_pid = ClientPid}, Topics) -> F = fun(Topic) -> emqtt_topic:feed_var(<<"$c">>, ClientId, Topic) end, - [ClientPid ! {subscribe, F(Topic), Qos} || {Topic, Qos} <- Topics]; + ClientPid ! {subscribe, [{F(Topic), Qos} || {Topic, Qos} <- Topics]}; client_connected(_ConnAck, _Client, _Topics) -> ignore. diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/apps/emqttd/src/emqttd_mod_presence.erl index 3e5dbccb2..74c94a1c9 100644 --- a/apps/emqttd/src/emqttd_mod_presence.erl +++ b/apps/emqttd/src/emqttd_mod_presence.erl @@ -56,16 +56,18 @@ client_connected(ConnAck, #mqtt_client{clientid = ClientId, {protocol, ProtoVer}, {connack, ConnAck}, {ts, emqttd_vm:timestamp()}]), - Message = #mqtt_message{qos = proplists:get_value(qos, Opts, 0), + Message = #mqtt_message{from = presence, + qos = proplists:get_value(qos, Opts, 0), topic = topic(connected, ClientId), payload = iolist_to_binary(Json)}, - emqttd_pubsub:publish(presence, Message). + emqttd_pubsub:publish(Message). client_disconnected(Reason, ClientId, Opts) -> Json = mochijson2:encode([{reason, reason(Reason)}, {ts, emqttd_vm:timestamp()}]), - emqttd_pubsub:publish(presence, #mqtt_message{qos = proplists:get_value(qos, Opts, 0), - topic = topic(disconnected, ClientId), - payload = iolist_to_binary(Json)}). + emqttd_pubsub:publish(#mqtt_message{from = presence, + qos = proplists:get_value(qos, Opts, 0), + topic = topic(disconnected, ClientId), + payload = iolist_to_binary(Json)}). unload(_Opts) -> emqttd_broker:unhook(client_connected, {?MODULE, client_connected}), diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index c8dff7bf3..086bd6be3 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -299,7 +299,7 @@ shutdown(normal, #proto_state{peername = Peername, clientid = ClientId, will_msg lager:info([{client, ClientId}], "Client ~s@~s: normal shutdown", [ClientId, emqttd_net:format(Peername)]), try_unregister(ClientId), - send_willmsg(ClientId, WillMsg); + send_willmsg(ClientId, WillMsg), emqttd_broker:foreach_hooks(client_disconnected, [normal, ClientId]); shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> diff --git a/apps/emqttd/src/emqttd_session_sup.erl b/apps/emqttd/src/emqttd_session_sup.erl index 9fe7f8ca3..ff12b5055 100644 --- a/apps/emqttd/src/emqttd_session_sup.erl +++ b/apps/emqttd/src/emqttd_session_sup.erl @@ -55,7 +55,7 @@ start_session(CleanSess, ClientId, ClientPid) -> %%%============================================================================= init([]) -> - {ok, {{simple_one_for_one, 0, 1}, + {ok, {{simple_one_for_one, 10, 10}, [{session, {emqttd_session, start_link, []}, transient, 10000, worker, [emqttd_session]}]}}. diff --git a/apps/emqttd/src/emqttd_sm.erl b/apps/emqttd/src/emqttd_sm.erl index f1e7041f2..bbebf3db5 100644 --- a/apps/emqttd/src/emqttd_sm.erl +++ b/apps/emqttd/src/emqttd_sm.erl @@ -140,7 +140,7 @@ handle_call({start_session, {true, ClientId, ClientPid}}, _From, State) -> case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, MRef}] -> erlang:demonitor(MRef, [flush]), - emqttd_session:destroy_session(SessPid, ClientId); + emqttd_session:destroy(SessPid, ClientId); [] -> ok end, diff --git a/apps/emqttd/src/emqttd_stats.erl b/apps/emqttd/src/emqttd_stats.erl index 0b5e65522..45cec4ffe 100644 --- a/apps/emqttd/src/emqttd_stats.erl +++ b/apps/emqttd/src/emqttd_stats.erl @@ -154,8 +154,9 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= publish(Stat, Val) -> - emqttd_pubsub:publish(stats, #mqtt_message{topic = stats_topic(Stat), - payload = emqttd_util:integer_to_binary(Val)}). + emqttd_pubsub:publish(#mqtt_message{from = stats, + topic = stats_topic(Stat), + payload = emqttd_util:integer_to_binary(Val)}). stats_topic(Stat) -> emqtt_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/apps/emqttd/src/emqttd_ws_client.erl index aa4006b4f..25dc334c7 100644 --- a/apps/emqttd/src/emqttd_ws_client.erl +++ b/apps/emqttd/src/emqttd_ws_client.erl @@ -133,9 +133,9 @@ handle_cast({received, Packet}, State = #client_state{proto_state = ProtoState}) handle_cast(_Msg, State) -> {noreply, State}. -handle_info({deliver, Message}, #state{proto_state = ProtoState} = State) -> +handle_info({deliver, Message}, #client_state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:send(Message, ProtoState), - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; handle_info({redeliver, {?PUBREL, PacketId}}, #client_state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), diff --git a/apps/zenmq/README b/apps/zenmq/README deleted file mode 100644 index e91132732..000000000 --- a/apps/zenmq/README +++ /dev/null @@ -1,14 +0,0 @@ -## Overview - -ZenMQ is the core architecture of distributed pubsub messaging queue written in Erlang. - -## Responsibilties - -* Topic Trie Tree -* Message Route -* Queue Management -* Broker Cluster -* Distributed Broker - -**Notice that this is an experimental design** - diff --git a/apps/zenmq/src/zenmq.app.src b/apps/zenmq/src/zenmq.app.src deleted file mode 100644 index 2d191d048..000000000 --- a/apps/zenmq/src/zenmq.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, zenmq, - [ - {description, ""}, - {vsn, "1"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, { zenmq_app, []}}, - {env, []} - ]}. diff --git a/apps/zenmq/src/zenmq.erl b/apps/zenmq/src/zenmq.erl deleted file mode 100644 index 1e96b16f5..000000000 --- a/apps/zenmq/src/zenmq.erl +++ /dev/null @@ -1,2 +0,0 @@ --module(zenmq). - diff --git a/apps/zenmq/src/zenmq_app.erl b/apps/zenmq/src/zenmq_app.erl deleted file mode 100644 index 15200771a..000000000 --- a/apps/zenmq/src/zenmq_app.erl +++ /dev/null @@ -1,16 +0,0 @@ --module(zenmq_app). - --behaviour(application). - -%% Application callbacks --export([start/2, stop/1]). - -%% =================================================================== -%% Application callbacks -%% =================================================================== - -start(_StartType, _StartArgs) -> - zenmq_sup:start_link(). - -stop(_State) -> - ok. diff --git a/apps/zenmq/src/zenmq_router.erl b/apps/zenmq/src/zenmq_router.erl deleted file mode 100644 index ecd6511e0..000000000 --- a/apps/zenmq/src/zenmq_router.erl +++ /dev/null @@ -1,4 +0,0 @@ - --module(zenmq_router). - - diff --git a/apps/zenmq/src/zenmq_sup.erl b/apps/zenmq/src/zenmq_sup.erl deleted file mode 100644 index f626a12b1..000000000 --- a/apps/zenmq/src/zenmq_sup.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(zenmq_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). - -%% =================================================================== -%% API functions -%% =================================================================== - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% =================================================================== -%% Supervisor callbacks -%% =================================================================== - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - From 470ac34a6db7a225237760f4f963b248a7b214a3 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 15 Jun 2015 22:28:34 +0800 Subject: [PATCH 075/104] merge emqtt to emqttd app --- apps/emqtt/include/emqtt.hrl | 70 ------------ apps/emqtt/src/emqtt.app.src | 12 -- apps/emqttd/include/emqttd.hrl | 32 +++++- .../include/emqttd_protocol.hrl} | 27 ++++- apps/emqttd/include/emqttd_systop.hrl | 106 ------------------ apps/emqttd/include/emqttd_topic.hrl | 48 -------- apps/emqttd/src/emqttd_access_rule.erl | 10 +- apps/emqttd/src/emqttd_bridge.erl | 6 +- apps/emqttd/src/emqttd_broker.erl | 26 +++-- apps/emqttd/src/emqttd_client.erl | 10 +- apps/emqttd/src/emqttd_http.erl | 5 +- apps/emqttd/src/emqttd_inflight.erl | 2 +- .../src/emqttd_message.erl} | 6 +- apps/emqttd/src/emqttd_metrics.erl | 40 ++++++- apps/emqttd/src/emqttd_mod_autosub.erl | 8 +- apps/emqttd/src/emqttd_mod_presence.erl | 6 +- apps/emqttd/src/emqttd_mod_rewrite.erl | 4 +- apps/emqttd/src/emqttd_mqueue.erl | 7 +- apps/emqttd/src/emqttd_msg_store.erl | 8 +- .../src/emqttd_packet.erl} | 6 +- .../src/emqttd_parser.erl} | 6 +- apps/emqttd/src/emqttd_protocol.erl | 22 ++-- apps/emqttd/src/emqttd_pubsub.erl | 22 ++-- .../src/emqttd_serialiser.erl} | 6 +- apps/emqttd/src/emqttd_session.erl | 4 +- apps/emqttd/src/emqttd_stats.erl | 32 +++++- .../src/emqttd_topic.erl} | 2 +- apps/emqttd/src/emqttd_trie.erl | 6 +- apps/emqttd/src/emqttd_ws_client.erl | 28 ++--- .../test/emqttd_parser_tests.erl} | 29 +++-- .../test/emqttd_serialiser_tests.erl} | 37 +++--- .../test/emqttd_topic_tests.erl} | 12 +- rel/reltool.config | 2 - 33 files changed, 246 insertions(+), 401 deletions(-) delete mode 100644 apps/emqtt/include/emqtt.hrl delete mode 100644 apps/emqtt/src/emqtt.app.src rename apps/{emqtt/include/emqtt_packet.hrl => emqttd/include/emqttd_protocol.hrl} (91%) delete mode 100644 apps/emqttd/include/emqttd_systop.hrl delete mode 100644 apps/emqttd/include/emqttd_topic.hrl rename apps/{emqtt/src/emqtt_message.erl => emqttd/src/emqttd_message.erl} (98%) rename apps/{emqtt/src/emqtt_packet.erl => emqttd/src/emqttd_packet.erl} (98%) rename apps/{emqtt/src/emqtt_parser.erl => emqttd/src/emqttd_parser.erl} (99%) rename apps/{emqtt/src/emqtt_serialiser.erl => emqttd/src/emqttd_serialiser.erl} (98%) rename apps/{emqtt/src/emqtt_topic.erl => emqttd/src/emqttd_topic.erl} (99%) rename apps/{emqtt/test/emqtt_parser_tests.erl => emqttd/test/emqttd_parser_tests.erl} (92%) rename apps/{emqtt/test/emqtt_serialiser_tests.erl => emqttd/test/emqttd_serialiser_tests.erl} (67%) rename apps/{emqtt/test/emqtt_topic_tests.erl => emqttd/test/emqttd_topic_tests.erl} (92%) diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl deleted file mode 100644 index e0a2aab63..000000000 --- a/apps/emqtt/include/emqtt.hrl +++ /dev/null @@ -1,70 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% MQTT Common Header. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% MQTT Protocol Version and Levels -%%------------------------------------------------------------------------------ --define(MQTT_PROTO_V31, 3). --define(MQTT_PROTO_V311, 4). - --define(PROTOCOL_NAMES, [ - {?MQTT_PROTO_V31, <<"MQIsdp">>}, - {?MQTT_PROTO_V311, <<"MQTT">>}]). - --type mqtt_vsn() :: ?MQTT_PROTO_V31 | ?MQTT_PROTO_V311. - -%%------------------------------------------------------------------------------ -%% QoS Levels -%%------------------------------------------------------------------------------ - --define(QOS_0, 0). --define(QOS_1, 1). --define(QOS_2, 2). - --define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). - --type mqtt_qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2. - -%%------------------------------------------------------------------------------ -%% MQTT Message -%%------------------------------------------------------------------------------ - --type mqtt_msgid() :: undefined | 1..16#ffff. - --record(mqtt_message, { - topic :: binary(), %% topic published to - from :: binary() | atom(), %% from clientid - qos = ?QOS_0 :: mqtt_qos(), - retain = false :: boolean(), - dup = false :: boolean(), - sys = false :: boolean(), %% $SYS flag - msgid :: mqtt_msgid(), - payload :: binary() -}). - --type mqtt_message() :: #mqtt_message{}. - diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src deleted file mode 100644 index c84d855f6..000000000 --- a/apps/emqtt/src/emqtt.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqtt, - [ - {description, "Erlang MQTT Common Library"}, - {vsn, git}, - {modules, []}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {env, []} - ]}. diff --git a/apps/emqttd/include/emqttd.hrl b/apps/emqttd/include/emqttd.hrl index 63ca9a047..34aa3b4bd 100644 --- a/apps/emqttd/include/emqttd.hrl +++ b/apps/emqttd/include/emqttd.hrl @@ -36,6 +36,12 @@ -define(ERTS_MINIMUM, "6.0"). +%% System Topics. +-define(SYSTOP, <<"$SYS">>). + +%% Queue Topics. +-define(QTop, <<"$Q">>). + %%------------------------------------------------------------------------------ %% PubSub %%------------------------------------------------------------------------------ @@ -56,8 +62,8 @@ %%------------------------------------------------------------------------------ -record(mqtt_subscriber, { topic :: binary(), - qos = 0 :: 0 | 1 | 2, - pid :: pid() + subpid :: pid(), + qos = 0 :: 0 | 1 | 2 }). -type mqtt_subscriber() :: #mqtt_subscriber{}. @@ -94,13 +100,29 @@ -record(mqtt_session, { clientid, session_pid, - subscriptions = [], - awaiting_ack, - awaiting_rel + subscriptions = [] }). -type mqtt_session() :: #mqtt_session{}. +%%------------------------------------------------------------------------------ +%% MQTT Message +%%------------------------------------------------------------------------------ +-type mqtt_msgid() :: undefined | 1..16#ffff. + +-record(mqtt_message, { + topic :: binary(), %% The topic published to + from :: binary() | atom(), %% ClientId of publisher + qos = 0 :: 0 | 1 | 2, %% Message QoS + retain = false :: boolean(), %% Retain flag + dup = false :: boolean(), %% Dup flag + sys = false :: boolean(), %% $SYS flag + msgid :: mqtt_msgid(), %% Message ID + payload :: binary() %% Payload +}). + +-type mqtt_message() :: #mqtt_message{}. + %%------------------------------------------------------------------------------ %% MQTT Plugin %%------------------------------------------------------------------------------ diff --git a/apps/emqtt/include/emqtt_packet.hrl b/apps/emqttd/include/emqttd_protocol.hrl similarity index 91% rename from apps/emqtt/include/emqtt_packet.hrl rename to apps/emqttd/include/emqttd_protocol.hrl index 5a965e98c..5266fae54 100644 --- a/apps/emqtt/include/emqtt_packet.hrl +++ b/apps/emqttd/include/emqttd_protocol.hrl @@ -20,11 +20,35 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% MQTT Packet Header. +%%% MQTT Protocol Header. %%% %%% @end %%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% MQTT Protocol Version and Levels +%%------------------------------------------------------------------------------ +-define(MQTT_PROTO_V31, 3). +-define(MQTT_PROTO_V311, 4). + +-define(PROTOCOL_NAMES, [ + {?MQTT_PROTO_V31, <<"MQIsdp">>}, + {?MQTT_PROTO_V311, <<"MQTT">>}]). + +-type mqtt_vsn() :: ?MQTT_PROTO_V31 | ?MQTT_PROTO_V311. + +%%------------------------------------------------------------------------------ +%% MQTT QoS +%%------------------------------------------------------------------------------ + +-define(QOS_0, 0). %% At most once +-define(QOS_1, 1). %% At least once +-define(QOS_2, 2). %% Exactly once + +-define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). + +-type mqtt_qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2. + %%------------------------------------------------------------------------------ %% Max ClientId Length. Why 1024? NiDongDe! %%------------------------------------------------------------------------------ @@ -199,4 +223,3 @@ -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). - diff --git a/apps/emqttd/include/emqttd_systop.hrl b/apps/emqttd/include/emqttd_systop.hrl deleted file mode 100644 index 9308ba682..000000000 --- a/apps/emqttd/include/emqttd_systop.hrl +++ /dev/null @@ -1,106 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% eMQTT System Topics. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --define(SYSTOP, <<"$SYS">>). - -%%------------------------------------------------------------------------------ -%% $SYS Topics of Broker -%%------------------------------------------------------------------------------ --define(SYSTOP_BROKERS, [ - version, % Broker version - uptime, % Broker uptime - datetime, % Broker local datetime - sysdescr % Broker description -]). - -%%------------------------------------------------------------------------------ -%% $SYS Topics for Clients -%%------------------------------------------------------------------------------ --define(SYSTOP_CLIENTS, [ - 'clients/count', % clients connected current - 'clients/max' % max clients connected -]). - -%%------------------------------------------------------------------------------ -%% $SYS Topics for Sessions -%%------------------------------------------------------------------------------ --define(SYSTOP_SESSIONS, [ - 'sessions/count', - 'sessions/max' -]). - -%%------------------------------------------------------------------------------ -%% $SYS Topics for Subscribers -%%------------------------------------------------------------------------------ --define(SYSTOP_PUBSUB, [ - 'topics/count', % ... - 'topics/max', % ... - 'subscribers/count', % ... - 'subscribers/max', % ... - 'queues/count', % ... - 'queues/max' % ... -]). - -%%------------------------------------------------------------------------------ -%% Bytes sent and received of Broker -%%------------------------------------------------------------------------------ --define(SYSTOP_BYTES, [ - {counter, 'bytes/received'}, % Total bytes received - {counter, 'bytes/sent'} % Total bytes sent -]). - -%%------------------------------------------------------------------------------ -%% Packets sent and received of Broker -%%------------------------------------------------------------------------------ --define(SYSTOP_PACKETS, [ - {counter, 'packets/received'}, % All Packets received - {counter, 'packets/sent'}, % All Packets sent - {counter, 'packets/connect'}, % CONNECT Packets received - {counter, 'packets/connack'}, % CONNACK Packets sent - {counter, 'packets/publish/received'}, % PUBLISH packets received - {counter, 'packets/publish/sent'}, % PUBLISH packets sent - {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received - {counter, 'packets/suback'}, % SUBACK packets sent - {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received - {counter, 'packets/unsuback'}, % UNSUBACK Packets sent - {counter, 'packets/pingreq'}, % PINGREQ packets received - {counter, 'packets/pingresp'}, % PINGRESP Packets sent - {counter, 'packets/disconnect'} % DISCONNECT Packets received -]). - -%%------------------------------------------------------------------------------ -%% Messages sent and received of broker -%%------------------------------------------------------------------------------ --define(SYSTOP_MESSAGES, [ - {counter, 'messages/received'}, % Messages received - {counter, 'messages/sent'}, % Messages sent - {gauge, 'messages/retained/count'},% Messagea retained - {gauge, 'messages/stored/count'}, % Messages stored - {counter, 'messages/dropped'} % Messages dropped -]). - - diff --git a/apps/emqttd/include/emqttd_topic.hrl b/apps/emqttd/include/emqttd_topic.hrl deleted file mode 100644 index a98dc791a..000000000 --- a/apps/emqttd/include/emqttd_topic.hrl +++ /dev/null @@ -1,48 +0,0 @@ -%%------------------------------------------------------------------------------ -%% Copyright (c) 2012-2015, Feng Lee -%% -%% Permission is hereby granted, free of charge, to any person obtaining a copy -%% of this software and associated documentation files (the "Software"), to deal -%% in the Software without restriction, including without limitation the rights -%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%% copies of the Software, and to permit persons to whom the Software is -%% furnished to do so, subject to the following conditions: -%% -%% The above copyright notice and this permission notice shall be included in all -%% copies or substantial portions of the Software. -%% -%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%% SOFTWARE. -%%------------------------------------------------------------------------------ -%%% @doc -%%% emqtt topic header. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% MQTT Topic -%%------------------------------------------------------------------------------ --record(topic, { - name :: binary(), - node :: node() -}). - --type topic() :: #topic{}. - -%%------------------------------------------------------------------------------ -%% MQTT Topic Subscriber -%%------------------------------------------------------------------------------ --record(topic_subscriber, { - topic :: binary(), - qos = 0 :: 0 | 1 | 2, - subpid :: pid() -}). - --type topic_subscriber() :: #topic_subscriber{}. - diff --git a/apps/emqttd/src/emqttd_access_rule.erl b/apps/emqttd/src/emqttd_access_rule.erl index a492136cf..0b0fdc6ba 100644 --- a/apps/emqttd/src/emqttd_access_rule.erl +++ b/apps/emqttd/src/emqttd_access_rule.erl @@ -73,9 +73,9 @@ compile(who, {user, Username}) -> {user, bin(Username)}; compile(topic, {eq, Topic}) -> - {eq, emqtt_topic:words(bin(Topic))}; + {eq, emqttd_topic:words(bin(Topic))}; compile(topic, Topic) -> - Words = emqtt_topic:words(bin(Topic)), + Words = emqttd_topic:words(bin(Topic)), case 'pattern?'(Words) of true -> {pattern, Words}; false -> Words @@ -126,12 +126,12 @@ match_topics(_Client, _Topic, []) -> false; match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> TopicFilter = feed_var(Client, PatternFilter), - case match_topic(emqtt_topic:words(Topic), TopicFilter) of + case match_topic(emqttd_topic:words(Topic), TopicFilter) of true -> true; false -> match_topics(Client, Topic, Filters) end; match_topics(Client, Topic, [TopicFilter|Filters]) -> - case match_topic(emqtt_topic:words(Topic), TopicFilter) of + case match_topic(emqttd_topic:words(Topic), TopicFilter) of true -> true; false -> match_topics(Client, Topic, Filters) end. @@ -139,7 +139,7 @@ match_topics(Client, Topic, [TopicFilter|Filters]) -> match_topic(Topic, {eq, TopicFilter}) -> Topic =:= TopicFilter; match_topic(Topic, TopicFilter) -> - emqtt_topic:match(Topic, TopicFilter). + emqttd_topic:match(Topic, TopicFilter). feed_var(Client, Pattern) -> feed_var(Client, Pattern, []). diff --git a/apps/emqttd/src/emqttd_bridge.erl b/apps/emqttd/src/emqttd_bridge.erl index 7f2d8345d..2129a6bdb 100644 --- a/apps/emqttd/src/emqttd_bridge.erl +++ b/apps/emqttd/src/emqttd_bridge.erl @@ -30,13 +30,13 @@ -include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). - --behaviour(gen_server). +-include("emqttd_protocol.hrl"). %% API Function Exports -export([start_link/3]). +-behaviour(gen_server). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). diff --git a/apps/emqttd/src/emqttd_broker.erl b/apps/emqttd/src/emqttd_broker.erl index 20bd27b45..e4deb6414 100644 --- a/apps/emqttd/src/emqttd_broker.erl +++ b/apps/emqttd/src/emqttd_broker.erl @@ -28,13 +28,7 @@ -author("Feng Lee "). --include("emqttd_systop.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). - --behaviour(gen_server). - --define(SERVER, ?MODULE). +-include_lib("emqttd.hrl"). %% API Function Exports -export([start_link/0]). @@ -54,6 +48,10 @@ %% Tick API -export([start_tick/1, stop_tick/1]). +-behaviour(gen_server). + +-define(SERVER, ?MODULE). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -62,6 +60,14 @@ -record(state, {started_at, sys_interval, tick_tref}). +%% $SYS Topics of Broker +-define(SYSTOP_BROKERS, [ + version, % Broker version + uptime, % Broker uptime + datetime, % Broker local datetime + sysdescr % Broker description +]). + %%%============================================================================= %%% API %%%============================================================================= @@ -276,7 +282,7 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= create_topic(Topic) -> - emqttd_pubsub:create(emqtt_topic:systop(Topic)). + emqttd_pubsub:create(emqttd_topic:systop(Topic)). retain(brokers) -> Payload = list_to_binary(string:join([atom_to_list(N) || N <- running_nodes()], ",")), @@ -288,12 +294,12 @@ retain(brokers) -> retain(Topic, Payload) when is_binary(Payload) -> publish(#mqtt_message{from = broker, retain = true, - topic = emqtt_topic:systop(Topic), + topic = emqttd_topic:systop(Topic), payload = Payload}). publish(Topic, Payload) when is_binary(Payload) -> publish( #mqtt_message{from = broker, - topic = emqtt_topic:systop(Topic), + topic = emqttd_topic:systop(Topic), payload = Payload}). publish(Msg) -> diff --git a/apps/emqttd/src/emqttd_client.erl b/apps/emqttd/src/emqttd_client.erl index cbcfffb4b..bd9f16414 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/apps/emqttd/src/emqttd_client.erl @@ -28,9 +28,9 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). --include_lib("emqtt/include/emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API Function Exports -export([start_link/2, info/1]). @@ -68,7 +68,7 @@ init([SockArgs = {Transport, Sock, _SockFun}, PacketOpts]) -> {ok, ConnStr} = emqttd_net:connection_string(Sock, inbound), lager:info("Connect from ~s", [ConnStr]), SendFun = fun(Data) -> Transport:send(NewSock, Data) end, - ParserState = emqtt_parser:init(PacketOpts), + ParserState = emqttd_parser:init(PacketOpts), ProtoState = emqttd_protocol:init(Peername, SendFun, PacketOpts), State = control_throttle(#state{transport = Transport, socket = NewSock, @@ -177,7 +177,7 @@ process_received_bytes(Bytes, State = #state{packet_opts = PacketOpts, parse_state = ParseState, proto_state = ProtoState, conn_name = ConnStr}) -> - case emqtt_parser:parse(Bytes, ParseState) of + case emqttd_parser:parse(Bytes, ParseState) of {more, ParseState1} -> {noreply, control_throttle(State #state{parse_state = ParseState1}), @@ -186,7 +186,7 @@ process_received_bytes(Bytes, State = #state{packet_opts = PacketOpts, received_stats(Packet), case emqttd_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - process_received_bytes(Rest, State#state{parse_state = emqtt_parser:init(PacketOpts), + process_received_bytes(Rest, State#state{parse_state = emqttd_parser:init(PacketOpts), proto_state = ProtoState1}); {error, Error} -> lager:error("MQTT protocol error ~p for connection ~p~n", [Error, ConnStr]), diff --git a/apps/emqttd/src/emqttd_http.erl b/apps/emqttd/src/emqttd_http.erl index 2c5c1a24f..8b7afe54e 100644 --- a/apps/emqttd/src/emqttd_http.erl +++ b/apps/emqttd/src/emqttd_http.erl @@ -29,8 +29,7 @@ -author("Feng Lee "). -include("emqttd.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd_protocol.hrl"). -import(proplists, [get_value/2, get_value/3]). @@ -122,7 +121,7 @@ validate(qos, Qos) -> (Qos >= ?QOS_0) and (Qos =< ?QOS_2); validate(topic, Topic) -> - emqtt_topic:validate({name, Topic}). + emqttd_topic:validate({name, Topic}). int(S) -> list_to_integer(S). diff --git a/apps/emqttd/src/emqttd_inflight.erl b/apps/emqttd/src/emqttd_inflight.erl index 1eb69de8f..8f3df986c 100644 --- a/apps/emqttd/src/emqttd_inflight.erl +++ b/apps/emqttd/src/emqttd_inflight.erl @@ -29,7 +29,7 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). -export([new/2, is_full/1, len/1, in/2, ack/2]). diff --git a/apps/emqtt/src/emqtt_message.erl b/apps/emqttd/src/emqttd_message.erl similarity index 98% rename from apps/emqtt/src/emqtt_message.erl rename to apps/emqttd/src/emqttd_message.erl index 61da1011e..d96e5d922 100644 --- a/apps/emqtt/src/emqtt_message.erl +++ b/apps/emqttd/src/emqttd_message.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_message). +-module(emqttd_message). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). -export([from_packet/1, from_packet/2, to_packet/1]). diff --git a/apps/emqttd/src/emqttd_metrics.erl b/apps/emqttd/src/emqttd_metrics.erl index 84350f939..f0d45ec02 100644 --- a/apps/emqttd/src/emqttd_metrics.erl +++ b/apps/emqttd/src/emqttd_metrics.erl @@ -28,9 +28,7 @@ -author("Feng Lee "). --include("emqttd_systop.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). -behaviour(gen_server). @@ -48,9 +46,41 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-record(state, {tick_tref}). + -define(METRIC_TAB, mqtt_metric). --record(state, {tick_tref}). +%% Bytes sent and received of Broker +-define(SYSTOP_BYTES, [ + {counter, 'bytes/received'}, % Total bytes received + {counter, 'bytes/sent'} % Total bytes sent +]). + +%% Packets sent and received of Broker +-define(SYSTOP_PACKETS, [ + {counter, 'packets/received'}, % All Packets received + {counter, 'packets/sent'}, % All Packets sent + {counter, 'packets/connect'}, % CONNECT Packets received + {counter, 'packets/connack'}, % CONNACK Packets sent + {counter, 'packets/publish/received'}, % PUBLISH packets received + {counter, 'packets/publish/sent'}, % PUBLISH packets sent + {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received + {counter, 'packets/suback'}, % SUBACK packets sent + {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received + {counter, 'packets/unsuback'}, % UNSUBACK Packets sent + {counter, 'packets/pingreq'}, % PINGREQ packets received + {counter, 'packets/pingresp'}, % PINGRESP Packets sent + {counter, 'packets/disconnect'} % DISCONNECT Packets received +]). + +%% Messages sent and received of broker +-define(SYSTOP_MESSAGES, [ + {counter, 'messages/received'}, % Messages received + {counter, 'messages/sent'}, % Messages sent + {gauge, 'messages/retained/count'},% Messagea retained + {gauge, 'messages/stored/count'}, % Messages stored + {counter, 'messages/dropped'} % Messages dropped +]). %%%============================================================================= %%% API @@ -204,6 +234,6 @@ create_metric({counter, Name}) -> [ets:insert(?METRIC_TAB, {{Name, I}, 0}) || I <- Schedulers]. metric_topic(Metric) -> - emqtt_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). + emqttd_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). diff --git a/apps/emqttd/src/emqttd_mod_autosub.erl b/apps/emqttd/src/emqttd_mod_autosub.erl index 8f7af60ac..c5a1e136e 100644 --- a/apps/emqttd/src/emqttd_mod_autosub.erl +++ b/apps/emqttd/src/emqttd_mod_autosub.erl @@ -29,12 +29,10 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). - -include("emqttd.hrl"). +-include("emqttd_protocol.hrl"). + -behaviour(emqttd_gen_mod). -export([load/1, client_connected/3, unload/1]). @@ -48,7 +46,7 @@ load(Opts) -> {ok, #state{topics = Topics}}. client_connected(?CONNACK_ACCEPT, #mqtt_client{clientid = ClientId, client_pid = ClientPid}, Topics) -> - F = fun(Topic) -> emqtt_topic:feed_var(<<"$c">>, ClientId, Topic) end, + F = fun(Topic) -> emqttd_topic:feed_var(<<"$c">>, ClientId, Topic) end, ClientPid ! {subscribe, [{F(Topic), Qos} || {Topic, Qos} <- Topics]}; client_connected(_ConnAck, _Client, _Topics) -> diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/apps/emqttd/src/emqttd_mod_presence.erl index 74c94a1c9..fe5135c1e 100644 --- a/apps/emqttd/src/emqttd_mod_presence.erl +++ b/apps/emqttd/src/emqttd_mod_presence.erl @@ -28,8 +28,6 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). - -include("emqttd.hrl"). -export([load/1, unload/1]). @@ -75,9 +73,9 @@ unload(_Opts) -> topic(connected, ClientId) -> - emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); + emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); topic(disconnected, ClientId) -> - emqtt_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). + emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). reason(Reason) when is_atom(Reason) -> Reason; reason({Error, _}) when is_atom(Error) -> Error; diff --git a/apps/emqttd/src/emqttd_mod_rewrite.erl b/apps/emqttd/src/emqttd_mod_rewrite.erl index fc038d95f..eaaf287ae 100644 --- a/apps/emqttd/src/emqttd_mod_rewrite.erl +++ b/apps/emqttd/src/emqttd_mod_rewrite.erl @@ -29,7 +29,7 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). -behaviour(emqttd_gen_mod). @@ -104,7 +104,7 @@ compile(Sections) -> match_topic(Topic, []) -> Topic; match_topic(Topic, [{topic, Filter, Rules} | Sections]) -> - case emqtt_topic:match(Topic, Filter) of + case emqttd_topic:match(Topic, Filter) of true -> match_rule(Topic, Rules); false -> diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index 6a37a707e..c9c7527be 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -52,7 +52,8 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). +-include("emqttd_protocol.hrl"). -export([new/2, name/1, is_empty/1, is_full/1, @@ -62,8 +63,6 @@ -define(HIGH_WM, 0.6). --define(MAX_LEN, 1000). - -record(mqueue, {name, q = queue:new(), %% pending queue len = 0, %% current queue len @@ -89,7 +88,7 @@ %%------------------------------------------------------------------------------ -spec new(binary(), list(mqueue_option())) -> mqueue(). new(Name, Opts) -> - MaxLen = emqttd_opts:g(max_length, Opts, ?MAX_LEN), + MaxLen = emqttd_opts:g(max_length, Opts, 1000), #mqueue{name = Name, max_len = MaxLen, low_wm = round(MaxLen * emqttd_opts:g(low_watermark, Opts, ?LOW_WM)), diff --git a/apps/emqttd/src/emqttd_msg_store.erl b/apps/emqttd/src/emqttd_msg_store.erl index c0b37cbc8..f304d0aad 100644 --- a/apps/emqttd/src/emqttd_msg_store.erl +++ b/apps/emqttd/src/emqttd_msg_store.erl @@ -28,7 +28,7 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). %% Mnesia callbacks -export([mnesia/1]). @@ -74,7 +74,7 @@ retain(Msg = #mqtt_message{topic = Topic, TabSize = mnesia:table_info(message, size), case {TabSize < limit(table), size(Payload) < limit(payload)} of {true, true} -> - lager:debug("Retained ~s", [emqtt_message:format(Msg)]), + lager:debug("Retained ~s", [emqttd_message:format(Msg)]), mnesia:async_dirty(fun mnesia:write/3, [message, Msg, write]), emqttd_metrics:set('messages/retained/count', mnesia:table_info(message, size)); @@ -106,12 +106,12 @@ env() -> Topic :: binary(), CPid :: pid(). redeliver(Topic, CPid) when is_binary(Topic) andalso is_pid(CPid) -> - case emqtt_topic:wildcard(Topic) of + case emqttd_topic:wildcard(Topic) of false -> dispatch(CPid, mnesia:dirty_read(message, Topic)); true -> Fun = fun(Msg = #mqtt_message{topic = Name}, Acc) -> - case emqtt_topic:match(Name, Topic) of + case emqttd_topic:match(Name, Topic) of true -> [Msg|Acc]; false -> Acc end diff --git a/apps/emqtt/src/emqtt_packet.erl b/apps/emqttd/src/emqttd_packet.erl similarity index 98% rename from apps/emqtt/src/emqtt_packet.erl rename to apps/emqttd/src/emqttd_packet.erl index f2b001236..6cb6b415d 100644 --- a/apps/emqtt/src/emqtt_packet.erl +++ b/apps/emqttd/src/emqttd_packet.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_packet). +-module(emqttd_packet). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API -export([protocol_name/1, type_name/1, connack_name/1]). diff --git a/apps/emqtt/src/emqtt_parser.erl b/apps/emqttd/src/emqttd_parser.erl similarity index 99% rename from apps/emqtt/src/emqtt_parser.erl rename to apps/emqttd/src/emqttd_parser.erl index 387f041d4..bcc9fa1b4 100644 --- a/apps/emqtt/src/emqtt_parser.erl +++ b/apps/emqttd/src/emqttd_parser.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_parser). +-module(emqttd_parser). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API -export([init/1, parse/2]). diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 086bd6be3..2c4f9568c 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -28,12 +28,10 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). - -include("emqttd.hrl"). +-include("emqttd_protocol.hrl"). + %% API -export([init/3, info/1, clientid/1, client/1]). @@ -259,18 +257,18 @@ handle(?PACKET(?DISCONNECT), State) -> {stop, normal, State#proto_state{will_msg = undefined}}. do_publish(Session, ClientId, Packet) -> - Msg = emqtt_message:from_packet(ClientId, Packet), + Msg = emqttd_message:from_packet(ClientId, Packet), Msg1 = emqttd_broker:foldl_hooks(client_publish, [], Msg), emqttd_session:publish(Session, Msg1). -spec send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}. send(Msg, State) when is_record(Msg, mqtt_message) -> - send(emqtt_message:to_packet(Msg), State); + send(emqttd_message:to_packet(Msg), State); send(Packet, State = #proto_state{sendfun = SendFun, peername = Peername}) when is_record(Packet, mqtt_packet) -> trace(send, Packet, State), sent_stats(Packet), - Data = emqtt_serialiser:serialise(Packet), + Data = emqttd_serialiser:serialise(Packet), lager:debug("SENT to ~s: ~p", [emqttd_net:format(Peername), Data]), emqttd_metrics:inc('bytes/sent', size(Data)), SendFun(Data), @@ -278,11 +276,11 @@ send(Packet, State = #proto_state{sendfun = SendFun, peername = Peername}) when trace(recv, Packet, #proto_state{peername = Peername, clientid = ClientId}) -> lager:info([{client, ClientId}], "RECV from ~s@~s: ~s", - [ClientId, emqttd_net:format(Peername), emqtt_packet:format(Packet)]); + [ClientId, emqttd_net:format(Peername), emqttd_packet:format(Packet)]); trace(send, Packet, #proto_state{peername = Peername, clientid = ClientId}) -> lager:info([{client, ClientId}], "SEND to ~s@~s: ~s", - [ClientId, emqttd_net:format(Peername), emqtt_packet:format(Packet)]). + [ClientId, emqttd_net:format(Peername), emqttd_packet:format(Packet)]). %% @doc redeliver PUBREL PacketId redeliver({?PUBREL, PacketId}, State) -> @@ -310,7 +308,7 @@ shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg emqttd_broker:foreach_hooks(client_disconnected, [Error, ClientId]). willmsg(Packet) when is_record(Packet, mqtt_packet_connect) -> - emqtt_message:from_packet(Packet). + emqttd_message:from_packet(Packet). %% generate a clientId clientid(undefined, State) -> @@ -366,7 +364,7 @@ validate_clientid(#mqtt_packet_connect {proto_ver = Ver, clean_sess = CleanSess, validate_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH}, variable = #mqtt_packet_publish{topic_name = Topic}}) -> - case emqtt_topic:validate({name, Topic}) of + case emqttd_topic:validate({name, Topic}) of true -> ok; false -> lager:warning("Error publish topic: ~p", [Topic]), {error, badtopic} end; @@ -390,7 +388,7 @@ validate_topics(Type, []) when Type =:= name orelse Type =:= filter -> validate_topics(Type, Topics) when Type =:= name orelse Type =:= filter -> ErrTopics = [Topic || {Topic, Qos} <- Topics, - not (emqtt_topic:validate({Type, Topic}) and validate_qos(Qos))], + not (emqttd_topic:validate({Type, Topic}) and validate_qos(Qos))], case ErrTopics of [] -> ok; _ -> lager:error("Error Topics: ~p", [ErrTopics]), {error, badtopic} diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/apps/emqttd/src/emqttd_pubsub.erl index 51fcc3c1e..1e1304d1a 100644 --- a/apps/emqttd/src/emqttd_pubsub.erl +++ b/apps/emqttd/src/emqttd_pubsub.erl @@ -31,9 +31,7 @@ -include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% Mnesia Callbacks -export([mnesia/1]). @@ -165,7 +163,7 @@ publish(From, #mqtt_message{topic=Topic} = Msg) -> case emqttd_msg_store:retain(Msg) of ok -> %TODO: why unset 'retain' flag? - publish(From, Topic, emqtt_message:unset_flag(Msg)); + publish(From, Topic, emqttd_message:unset_flag(Msg)); ignore -> publish(From, Topic, Msg) end. @@ -197,7 +195,7 @@ dispatch(Topic, #mqtt_message{qos = Qos} = Msg ) when is_binary(Topic) -> Subscribers = mnesia:dirty_read(subscriber, Topic), setstats(dropped, Subscribers =:= []), %%TODO:... lists:foreach( - fun(#mqtt_subscriber{qos = SubQos, pid=SubPid}) -> + fun(#mqtt_subscriber{subpid=SubPid, qos = SubQos}) -> Msg1 = if Qos > SubQos -> Msg#mqtt_message{qos = SubQos}; true -> Msg @@ -226,7 +224,7 @@ handle_call({subscribe, SubPid, Topics}, _From, State) -> #mqtt_queue{name = Queue, subpid = SubPid, qos = Qos}; ({Topic, Qos}) -> {#mqtt_topic{topic = Topic, node = node()}, - #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}} + #mqtt_subscriber{topic = Topic, subpid = SubPid, qos = Qos}} end, Topics), F = fun() -> lists:map(fun(QueueR) when is_record(QueueR, mqtt_queue) -> @@ -261,7 +259,7 @@ handle_call({subscribe, SubPid, <<"$Q/", _/binary>> = Queue, Qos}, _From, State) handle_call({subscribe, SubPid, Topic, Qos}, _From, State) -> TopicR = #mqtt_topic{topic = Topic, node = node()}, - Subscriber = #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}, + Subscriber = #mqtt_subscriber{topic = Topic, subpid = SubPid, qos = Qos}, case mnesia:transaction(fun add_subscriber/1, [{TopicR, Subscriber}]) of {atomic, ok} -> setstats(all), @@ -280,7 +278,7 @@ handle_cast({unsubscribe, SubPid, Topics}, State) when is_list(Topics) -> #mqtt_queue{name = Queue, subpid = SubPid}; (Topic) -> {#mqtt_topic{topic = Topic, node = node()}, - #mqtt_subscriber{topic = Topic, _ = '_', pid = SubPid}} + #mqtt_subscriber{topic = Topic, subpid = SubPid, _ = '_'}} end, Topics), F = fun() -> lists:foreach( @@ -309,7 +307,7 @@ handle_cast({unsubscribe, SubPid, <<"$Q/", _/binary>> = Queue}, State) -> handle_cast({unsubscribe, SubPid, Topic}, State) -> TopicR = #mqtt_topic{topic = Topic, node = node()}, - Subscriber = #mqtt_subscriber{topic = Topic, _ = '_', pid = SubPid}, + Subscriber = #mqtt_subscriber{topic = Topic, subpid = SubPid, _ = '_'}, case mnesia:transaction(fun remove_subscriber/1, [{TopicR, Subscriber}]) of {atomic, _} -> ok; {aborted, Error} -> lager:error("unsubscribe ~s error: ~p", [Topic, Error]) @@ -333,7 +331,7 @@ handle_info({'DOWN', _Mon, _Type, DownPid, _Info}, State = #state{submap = SubMa end, Queues), %% remove subscribers... - Subscribers = mnesia:index_read(subscriber, DownPid, #mqtt_subscriber.pid), + Subscribers = mnesia:index_read(subscriber, DownPid, #mqtt_subscriber.subpid), lists:foreach(fun(Sub = #mqtt_subscriber{topic = Topic}) -> mnesia:delete_object(subscriber, Sub, write), try_remove_topic(#mqtt_topic{topic = Topic, node = Node}) @@ -387,12 +385,12 @@ add_topic(TopicR = #mqtt_topic{topic = Topic}) -> end. %% Fix issue #53 - Remove Overlapping Subscriptions -add_subscriber({TopicR, Subscriber = #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}}) +add_subscriber({TopicR, Subscriber = #mqtt_subscriber{topic = Topic, subpid = SubPid, qos = Qos}}) when is_record(TopicR, mqtt_topic) -> case add_topic(TopicR) of ok -> OverlapSubs = [Sub || Sub = #mqtt_subscriber{topic = SubTopic, qos = SubQos} - <- mnesia:index_read(subscriber, SubPid, #mqtt_subscriber.pid), + <- mnesia:index_read(subscriber, SubPid, #mqtt_subscriber.subpid), SubTopic =:= Topic, SubQos =/= Qos], %% remove overlapping subscribers diff --git a/apps/emqtt/src/emqtt_serialiser.erl b/apps/emqttd/src/emqttd_serialiser.erl similarity index 98% rename from apps/emqtt/src/emqtt_serialiser.erl rename to apps/emqttd/src/emqttd_serialiser.erl index 60d1f6dfa..e109c19a3 100644 --- a/apps/emqtt/src/emqtt_serialiser.erl +++ b/apps/emqttd/src/emqttd_serialiser.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_serialiser). +-module(emqttd_serialiser). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API -export([serialise/1]). diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 4cd480165..d4e8f081d 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -49,9 +49,7 @@ -include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% Session API -export([start_link/3, resume/3, destroy/2]). diff --git a/apps/emqttd/src/emqttd_stats.erl b/apps/emqttd/src/emqttd_stats.erl index 45cec4ffe..1f005369e 100644 --- a/apps/emqttd/src/emqttd_stats.erl +++ b/apps/emqttd/src/emqttd_stats.erl @@ -28,16 +28,14 @@ -author("Feng Lee "). --include("emqttd_systop.hrl"). +-include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). +-export([start_link/0]). -behaviour(gen_server). -define(SERVER, ?MODULE). --export([start_link/0]). - %% statistics API. -export([statsfun/1, statsfun/2, getstats/0, getstat/1, @@ -47,9 +45,31 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-record(state, {tick_tref}). + -define(STATS_TAB, mqtt_stats). --record(state, {tick_tref}). +%% $SYS Topics for Clients +-define(SYSTOP_CLIENTS, [ + 'clients/count', % clients connected current + 'clients/max' % max clients connected +]). + +%% $SYS Topics for Sessions +-define(SYSTOP_SESSIONS, [ + 'sessions/count', + 'sessions/max' +]). + +%% $SYS Topics for Subscribers +-define(SYSTOP_PUBSUB, [ + 'topics/count', % ... + 'topics/max', % ... + 'subscribers/count', % ... + 'subscribers/max', % ... + 'queues/count', % ... + 'queues/max' % ... +]). %%%============================================================================= %%% API @@ -159,5 +179,5 @@ publish(Stat, Val) -> payload = emqttd_util:integer_to_binary(Val)}). stats_topic(Stat) -> - emqtt_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). + emqttd_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). diff --git a/apps/emqtt/src/emqtt_topic.erl b/apps/emqttd/src/emqttd_topic.erl similarity index 99% rename from apps/emqtt/src/emqtt_topic.erl rename to apps/emqttd/src/emqttd_topic.erl index 0fa183614..13e9d55a3 100644 --- a/apps/emqtt/src/emqtt_topic.erl +++ b/apps/emqttd/src/emqttd_topic.erl @@ -24,7 +24,7 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_topic). +-module(emqttd_topic). -author("Feng Lee "). diff --git a/apps/emqttd/src/emqttd_trie.erl b/apps/emqttd/src/emqttd_trie.erl index 9beaa339a..30e6d69d2 100644 --- a/apps/emqttd/src/emqttd_trie.erl +++ b/apps/emqttd/src/emqttd_trie.erl @@ -102,7 +102,7 @@ insert(Topic) when is_binary(Topic) -> mnesia:write(TrieNode#trie_node{topic=Topic}); [] -> %add trie path - [add_path(Triple) || Triple <- emqtt_topic:triples(Topic)], + [add_path(Triple) || Triple <- emqttd_topic:triples(Topic)], %add last node mnesia:write(#trie_node{node_id=Topic, topic=Topic}) end. @@ -113,7 +113,7 @@ insert(Topic) when is_binary(Topic) -> %%------------------------------------------------------------------------------ -spec find(Topic :: binary()) -> list(MatchedTopic :: binary()). find(Topic) when is_binary(Topic) -> - TrieNodes = match_node(root, emqtt_topic:words(Topic), []), + TrieNodes = match_node(root, emqttd_topic:words(Topic), []), [Name || #trie_node{topic=Name} <- TrieNodes, Name=/= undefined]. %%------------------------------------------------------------------------------ @@ -125,7 +125,7 @@ delete(Topic) when is_binary(Topic) -> case mnesia:read(trie_node, Topic) of [#trie_node{edge_count=0}] -> mnesia:delete({trie_node, Topic}), - delete_path(lists:reverse(emqtt_topic:triples(Topic))); + delete_path(lists:reverse(emqttd_topic:triples(Topic))); [TrieNode] -> mnesia:write(TrieNode#trie_node{topic=Topic}); [] -> diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/apps/emqttd/src/emqttd_ws_client.erl index 25dc334c7..0270d7d38 100644 --- a/apps/emqttd/src/emqttd_ws_client.erl +++ b/apps/emqttd/src/emqttd_ws_client.erl @@ -29,30 +29,24 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). --include_lib("emqtt/include/emqtt_packet.hrl"). - --behaviour(gen_server). +-include("emqttd_protocol.hrl"). %% API Exports -export([start_link/1, ws_loop/3]). +-behaviour(gen_server). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% WebSocket loop state --record(wsocket_state, {request, - client_pid, - packet_opts, - parser_state}). +%% WebSocket Loop State +-record(wsocket_state, {request, client_pid, packet_opts, parser_state}). -%% Client state --record(client_state, {ws_pid, - request, - proto_state, - keepalive}). +%% Client State +-record(client_state, {ws_pid, request, proto_state, keepalive}). %%------------------------------------------------------------------------------ %% @doc Start WebSocket client. @@ -65,7 +59,7 @@ start_link(Req) -> ReentryWs(#wsocket_state{request = Req, client_pid = ClientPid, packet_opts = PktOpts, - parser_state = emqtt_parser:init(PktOpts)}). + parser_state = emqttd_parser:init(PktOpts)}). %%------------------------------------------------------------------------------ %% @private @@ -88,7 +82,7 @@ ws_loop(Data, State = #wsocket_state{request = Req, parser_state = ParserState}, ReplyChannel) -> Peer = Req:get(peer), lager:debug("RECV from ~s(WebSocket): ~p", [Peer, Data]), - case emqtt_parser:parse(iolist_to_binary(Data), ParserState) of + case emqttd_parser:parse(iolist_to_binary(Data), ParserState) of {more, ParserState1} -> State#wsocket_state{parser_state = ParserState1}; {ok, Packet, Rest} -> @@ -100,7 +94,7 @@ ws_loop(Data, State = #wsocket_state{request = Req, end. reset_parser(State = #wsocket_state{packet_opts = PktOpts}) -> - State#wsocket_state{parser_state = emqtt_parser:init(PktOpts)}. + State#wsocket_state{parser_state = emqttd_parser:init(PktOpts)}. %%%============================================================================= %%% gen_fsm callbacks diff --git a/apps/emqtt/test/emqtt_parser_tests.erl b/apps/emqttd/test/emqttd_parser_tests.erl similarity index 92% rename from apps/emqtt/test/emqtt_parser_tests.erl rename to apps/emqttd/test/emqttd_parser_tests.erl index 6cb3c9286..fb15019eb 100644 --- a/apps/emqtt/test/emqtt_parser_tests.erl +++ b/apps/emqttd/test/emqttd_parser_tests.erl @@ -20,21 +20,20 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqtt_parser tests. +%%% emqttd_parser tests. %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_parser_tests). +-module(emqttd_parser_tests). --include("emqtt.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). parse_connect_test() -> - State = emqtt_parser:init([]), + State = emqttd_parser:init([]), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V31ConnBin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115,113,112,117,98,47,49,48,52,53,49,45,105,77,97,99,46,108,111,99,97>>, ?assertMatch({ok, #mqtt_packet{ @@ -46,7 +45,7 @@ parse_connect_test() -> proto_name = <<"MQIsdp">>, clientid = <<"mosqpub/10451-iMac.loca">>, clean_sess = true, - keep_alive = 60}}, <<>>}, emqtt_parser:parse(V31ConnBin, State)), + keep_alive = 60}}, <<>>}, emqttd_parser:parse(V31ConnBin, State)), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V311ConnBin = <<16,35,0,4,77,81,84,84,4,2,0,60,0,23,109,111,115,113,112,117,98,47,49,48,52,53,49,45,105,77,97,99,46,108,111,99,97>>, ?assertMatch({ok, #mqtt_packet{ @@ -58,7 +57,7 @@ parse_connect_test() -> proto_name = <<"MQTT">>, clientid = <<"mosqpub/10451-iMac.loca">>, clean_sess = true, - keep_alive = 60 } }, <<>>}, emqtt_parser:parse(V311ConnBin, State)), + keep_alive = 60 } }, <<>>}, emqttd_parser:parse(V311ConnBin, State)), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId="", ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60) V311ConnWithoutClientId = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, @@ -71,7 +70,7 @@ parse_connect_test() -> proto_name = <<"MQTT">>, clientid = <<>>, clean_sess = true, - keep_alive = 60 } }, <<>>}, emqtt_parser:parse(V311ConnWithoutClientId, State)), + keep_alive = 60 } }, <<>>}, emqttd_parser:parse(V311ConnWithoutClientId, State)), %%CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10452-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=test, Password=******, Will(Qos=1, Retain=false, Topic=/will, Msg=willmsg)) ConnBinWithWill = <<16,67,0,6,77,81,73,115,100,112,3,206,0,60,0,23,109,111,115,113,112,117,98,47,49,48,52,53,50,45,105,77,97,99,46,108,111,99,97,0,5,47,119,105,108,108,0,7,119,105,108,108,109,115,103,0,4,116,101,115,116,0,6,112,117,98,108,105,99>>, ?assertMatch({ok, #mqtt_packet{ @@ -91,11 +90,11 @@ parse_connect_test() -> will_msg = <<"willmsg">> , username = <<"test">>, password = <<"public">>}}, - <<>> }, emqtt_parser:parse(ConnBinWithWill, State)), + <<>> }, emqttd_parser:parse(ConnBinWithWill, State)), ok. parse_publish_test() -> - State = emqtt_parser:init([]), + State = emqttd_parser:init([]), %%PUBLISH(Qos=1, Retain=false, Dup=false, TopicName=a/b/c, PacketId=1, Payload=<<"hahah">>) PubBin = <<50,14,0,5,97,47,98,47,99,0,1,104,97,104,97,104>>, ?assertMatch({ok, #mqtt_packet{ @@ -105,7 +104,7 @@ parse_publish_test() -> retain = false}, variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, packet_id = 1}, - payload = <<"hahah">> }, <<>>}, emqtt_parser:parse(PubBin, State)), + payload = <<"hahah">> }, <<>>}, emqttd_parser:parse(PubBin, State)), %PUBLISH(Qos=0, Retain=false, Dup=false, TopicName=xxx/yyy, PacketId=undefined, Payload=<<"hello">>) %DISCONNECT(Qos=0, Retain=false, Dup=false) @@ -117,13 +116,13 @@ parse_publish_test() -> retain = false}, variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, packet_id = undefined}, - payload = <<"hello">> }, <<224,0>>}, emqtt_parser:parse(PubBin1, State)), + payload = <<"hello">> }, <<224,0>>}, emqttd_parser:parse(PubBin1, State)), ?assertMatch({ok, #mqtt_packet{ header = #mqtt_packet_header{type = ?DISCONNECT, dup = false, qos = 0, retain = false} - }, <<>>}, emqtt_parser:parse(<<224, 0>>, State)). + }, <<>>}, emqttd_parser:parse(<<224, 0>>, State)). parse_puback_test() -> %%PUBACK(Qos=0, Retain=false, Dup=false, PacketId=1) @@ -133,7 +132,7 @@ parse_puback_test() -> dup = false, qos = 0, retain = false } - }, <<>>}, emqtt_parser:parse(PubAckBin, emqtt_parser:init([]))), + }, <<>>}, emqttd_parser:parse(PubAckBin, emqttd_parser:init([]))), ok. parse_subscribe_test() -> @@ -150,7 +149,7 @@ parse_disconnect_test() -> dup = false, qos = 0, retain = false} - }, <<>>}, emqtt_parser:parse(Bin, emqtt_parser:init([]))). + }, <<>>}, emqttd_parser:parse(Bin, emqttd_parser:init([]))). -endif. diff --git a/apps/emqtt/test/emqtt_serialiser_tests.erl b/apps/emqttd/test/emqttd_serialiser_tests.erl similarity index 67% rename from apps/emqtt/test/emqtt_serialiser_tests.erl rename to apps/emqttd/test/emqttd_serialiser_tests.erl index 5cec36a18..1f3a2b4c0 100644 --- a/apps/emqtt/test/emqtt_serialiser_tests.erl +++ b/apps/emqttd/test/emqttd_serialiser_tests.erl @@ -20,58 +20,59 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqtt_serialiser tests. +%%% emqttd_serialiser tests. %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_serialiser_tests). - --include("emqtt.hrl"). --include("emqtt_packet.hrl"). +-module(emqttd_serialiser_tests). -ifdef(TEST). +-include("emqttd_protocol.hrl"). + -include_lib("eunit/include/eunit.hrl"). +-import(emqttd_serialiser, [serialise/1]). + serialise_connect_test() -> - emqtt_serialiser:serialise(?CONNECT_PACKET(#mqtt_packet_connect{})). + serialise(?CONNECT_PACKET(#mqtt_packet_connect{})). serialise_connack_test() -> ConnAck = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{ack_flags = 0, return_code = 0}}, - ?assertEqual(<<32,2,0,0>>, emqtt_serialiser:serialise(ConnAck)). + ?assertEqual(<<32,2,0,0>>, serialise(ConnAck)). serialise_publish_test() -> - emqtt_serialiser:serialise(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), - emqtt_serialiser:serialise(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)). + serialise(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), + serialise(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)). serialise_puback_test() -> - emqtt_serialiser:serialise(?PUBACK_PACKET(?PUBACK, 10384)). + serialise(?PUBACK_PACKET(?PUBACK, 10384)). serialise_pubrel_test() -> - emqtt_serialiser:serialise(?PUBREL_PACKET(10384)). + serialise(?PUBREL_PACKET(10384)). serialise_subscribe_test() -> TopicTable = [{<<"TopicQos0">>, ?QOS_0}, {<<"TopicQos1">>, ?QOS_1}, {<<"TopicQos2">>, ?QOS_2}], - emqtt_serialiser:serialise(?SUBSCRIBE_PACKET(10, TopicTable)). + serialise(?SUBSCRIBE_PACKET(10, TopicTable)). serialise_suback_test() -> - emqtt_serialiser:serialise(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). + serialise(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). serialise_unsubscribe_test() -> - emqtt_serialiser:serialise(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). + serialise(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). serialise_unsuback_test() -> - emqtt_serialiser:serialise(?UNSUBACK_PACKET(10)). + serialise(?UNSUBACK_PACKET(10)). serialise_pingreq_test() -> - emqtt_serialiser:serialise(?PACKET(?PINGREQ)). + serialise(?PACKET(?PINGREQ)). serialise_pingresp_test() -> - emqtt_serialiser:serialise(?PACKET(?PINGRESP)). + serialise(?PACKET(?PINGRESP)). serialise_disconnect_test() -> - emqtt_serialiser:serialise(?PACKET(?DISCONNECT)). + serialise(?PACKET(?DISCONNECT)). -endif. diff --git a/apps/emqtt/test/emqtt_topic_tests.erl b/apps/emqttd/test/emqttd_topic_tests.erl similarity index 92% rename from apps/emqtt/test/emqtt_topic_tests.erl rename to apps/emqttd/test/emqttd_topic_tests.erl index c4fb75377..a21b6c520 100644 --- a/apps/emqtt/test/emqtt_topic_tests.erl +++ b/apps/emqttd/test/emqttd_topic_tests.erl @@ -19,14 +19,14 @@ %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE %% SOFTWARE. %%------------------------------------------------------------------------------ --module(emqtt_topic_tests). - --import(emqtt_topic, [validate/1, wildcard/1, match/2, triples/1, words/1]). +-module(emqttd_topic_tests). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). +-import(emqttd_topic, [validate/1, wildcard/1, match/2, triples/1, words/1]). + -define(N, 100000). validate_test() -> @@ -116,11 +116,11 @@ words_test() -> ok. feed_var_test() -> - ?assertEqual(<<"$Q/client/clientId">>, emqtt_topic:feed_var(<<"$c">>, <<"clientId">>, <<"$Q/client/$c">>)). + ?assertEqual(<<"$Q/client/clientId">>, emqttd_topic:feed_var(<<"$c">>, <<"clientId">>, <<"$Q/client/$c">>)). join_test() -> - ?assertEqual(<<"/ab/cd/ef/">>, emqtt_topic:join(words(<<"/ab/cd/ef/">>))), - ?assertEqual(<<"ab/+/#">>, emqtt_topic:join(words(<<"ab/+/#">>))). + ?assertEqual(<<"/ab/cd/ef/">>, emqttd_topic:join(words(<<"/ab/cd/ef/">>))), + ?assertEqual(<<"ab/+/#">>, emqttd_topic:join(words(<<"ab/+/#">>))). -endif. diff --git a/rel/reltool.config b/rel/reltool.config index 2dbb76ea5..497676d87 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -25,7 +25,6 @@ gproc, esockd, mochiweb, - {emqtt, load}, emqttd ]}, {rel, "start_clean", "", @@ -62,7 +61,6 @@ {app, gproc, [{incl_cond, include}]}, {app, esockd, [{mod_cond, app}, {incl_cond, include}]}, {app, mochiweb, [{mod_cond, app}, {incl_cond, include}]}, - {app, emqtt, [{mod_cond, app}, {incl_cond, include}]}, {app, emqttd, [{mod_cond, app}, {incl_cond, include}]} ]}. From 08a64ee97b01cd7827978b6664350d5169c0e870 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 16 Jun 2015 02:41:03 +0800 Subject: [PATCH 076/104] session --- apps/emqttd/include/emqttd_protocol.hrl | 5 + apps/emqttd/src/emqttd_protocol.erl | 55 +++++----- apps/emqttd/src/emqttd_pubsub.erl | 20 ++-- apps/emqttd/src/emqttd_session.erl | 128 ++++++++++++------------ 4 files changed, 107 insertions(+), 101 deletions(-) diff --git a/apps/emqttd/include/emqttd_protocol.hrl b/apps/emqttd/include/emqttd_protocol.hrl index 5266fae54..35637e3d5 100644 --- a/apps/emqttd/include/emqttd_protocol.hrl +++ b/apps/emqttd/include/emqttd_protocol.hrl @@ -196,6 +196,11 @@ packet_id = PacketId}, payload = Payload}). +-define(PUBLISH(Qos, PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = Qos}, + variable = #mqtt_packet_publish{packet_id = PacketId}}). + -define(PUBACK_PACKET(Type, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = Type}, variable = #mqtt_packet_puback{packet_id = PacketId}}). diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 2c4f9568c..7bdc1d67c 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -170,37 +170,18 @@ handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername} %% Send connack send(?CONNACK_PACKET(ReturnCode1), State3); -handle(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), - State = #proto_state{clientid = ClientId, session = Session}) -> +handle(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), + State = #proto_state{clientid = ClientId}) -> + case check_acl(publish, Topic, State) of - allow -> - do_publish(Session, ClientId, Packet); + allow -> + publish(Packet, State); deny -> lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]) end, {ok, State}; -handle(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), - State = #proto_state{clientid = ClientId, session = Session}) -> - case check_acl(publish, Topic, State) of - allow -> - do_publish(Session, ClientId, Packet), - send(?PUBACK_PACKET(?PUBACK, PacketId), State); - deny -> - lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]), - {ok, State} - end; -handle(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), - State = #proto_state{clientid = ClientId, session = Session}) -> - case check_acl(publish, Topic, State) of - allow -> - do_publish(Session, ClientId, Packet), - send(?PUBACK_PACKET(?PUBREC, PacketId), State); - deny -> - lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]), - {ok, State} - end; handle(?PUBACK_PACKET(?PUBACK, PacketId), State = #proto_state{session = Session}) -> emqttd_session:puback(Session, PacketId), @@ -256,10 +237,26 @@ handle(?PACKET(?DISCONNECT), State) -> % clean willmsg {stop, normal, State#proto_state{will_msg = undefined}}. -do_publish(Session, ClientId, Packet) -> - Msg = emqttd_message:from_packet(ClientId, Packet), - Msg1 = emqttd_broker:foldl_hooks(client_publish, [], Msg), - emqttd_session:publish(Session, Msg1). +publish(Packet = ?PUBLISH(?QOS_0, _PacketId), #proto_state{clientid = ClientId, session = Session}) -> + emqttd_session:publish(Session, emqttd_message:from_packet(ClientId, Packet)); + +publish(Packet = ?PUBLISH(?QOS_1, PacketId), State = #proto_state{clientid = ClientId, session = Session}) -> + case emqttd_session:publish(Session, emqttd_message:from_packet(ClientId, Packet)) of + ok -> + send(?PUBACK_PACKET(?PUBACK, PacketId), State); + {error, Error} -> + %%TODO: log format... + lager:error("Client ~s: publish qos1 error ~p", [ClientId, Error]) + end; + +publish(Packet = ?PUBLISH(?QOS_2, PacketId), State = #proto_state{clientid = ClientId, session = Session}) -> + case emqttd_session:publish(Session, emqttd_message:from_packet(ClientId, Packet)) of + ok -> + send(?PUBACK_PACKET(?PUBREC, PacketId), State); + {error, Error} -> + %%TODO: log format... + lager:error("Client ~s: publish qos2 error ~p", [ClientId, Error]) + end. -spec send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}. send(Msg, State) when is_record(Msg, mqtt_message) -> @@ -323,7 +320,7 @@ send_willmsg(_ClientId, undefined) -> ignore; send_willmsg(ClientId, WillMsg) -> lager:info("Client ~s send willmsg: ~p", [ClientId, WillMsg]), - emqttd_pubsub:publish(ClientId, WillMsg). + emqttd_pubsub:publish(WillMsg#mqtt_message{from = ClientId}). start_keepalive(0) -> ignore; diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/apps/emqttd/src/emqttd_pubsub.erl index 1e1304d1a..3699bfc9f 100644 --- a/apps/emqttd/src/emqttd_pubsub.erl +++ b/apps/emqttd/src/emqttd_pubsub.erl @@ -47,7 +47,7 @@ -export([create/1, subscribe/1, unsubscribe/1, - publish/2, + publish/1, %local node dispatch/2, match/1]). @@ -81,7 +81,7 @@ mnesia(boot) -> {ram_copies, [node()]}, {record_name, mqtt_subscriber}, {attributes, record_info(fields, mqtt_subscriber)}, - {index, [pid]}, + {index, [subpid]}, {local_content, true}]); mnesia(copy) -> @@ -156,19 +156,23 @@ cast(Msg) -> %% @doc Publish to cluster nodes %% @end %%------------------------------------------------------------------------------ --spec publish(From :: mqtt_clientid() | atom(), Msg :: mqtt_message()) -> ok. -publish(From, #mqtt_message{topic=Topic} = Msg) -> +-spec publish(Msg :: mqtt_message()) -> ok. +publish(#mqtt_message{topic=Topic, from = From} = Msg) -> trace(publish, From, Msg), + + %%TODO:call hooks here... + %%Msg1 = emqttd_broker:foldl_hooks(client_publish, [], Msg), + %% Retain message first. Don't create retained topic. case emqttd_msg_store:retain(Msg) of ok -> %TODO: why unset 'retain' flag? - publish(From, Topic, emqttd_message:unset_flag(Msg)); + publish(Topic, emqttd_message:unset_flag(Msg)); ignore -> - publish(From, Topic, Msg) + publish(Topic, Msg) end. -publish(From, <<"$Q/", _/binary>> = Queue, #mqtt_message{qos = Qos} = Msg) -> +publish(<<"$Q/", _/binary>> = Queue, #mqtt_message{qos = Qos} = Msg) -> lists:foreach( fun(#mqtt_queue{subpid = SubPid, qos = SubQos}) -> Msg1 = if @@ -178,7 +182,7 @@ publish(From, <<"$Q/", _/binary>> = Queue, #mqtt_message{qos = Qos} = Msg) -> SubPid ! {dispatch, Msg1} end, mnesia:dirty_read(queue, Queue)); -publish(_From, Topic, Msg) when is_binary(Topic) -> +publish(Topic, Msg) when is_binary(Topic) -> lists:foreach(fun(#mqtt_topic{topic=Name, node=Node}) -> case Node =:= node() of true -> dispatch(Name, Msg); diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index d4e8f081d..384e49ae1 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -21,13 +21,13 @@ %%%----------------------------------------------------------------------------- %%% @doc %%% -%%% emqttd session for persistent client. +%%% Session for persistent MQTT client. %%% %%% Session State in the broker consists of: %%% %%% 1. The Client’s subscriptions. %%% -%%% 2. inflight qos1, qos2 messages sent to the client but unacked, QoS 1 and QoS 2 +%%% 2. inflight qos1/2 messages sent to the client but unacked, QoS 1 and QoS 2 %%% messages which have been sent to the Client, but have not been completely %%% acknowledged. %%% @@ -59,6 +59,8 @@ puback/2, pubrec/2, pubrel/2, pubcomp/2, subscribe/2, unsubscribe/2]). +-behaviour(gen_server). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -116,7 +118,7 @@ max_awaiting_rel = 100, %% session expired after 48 hours - expired_after = 172800, + expired_after = 48, expired_timer, @@ -126,6 +128,7 @@ %% @doc Start a session. %% @end %%------------------------------------------------------------------------------ +-spec start_link(boolean(), binary(), pid()) -> {ok, pid()} | {error, any()}. start_link(CleanSess, ClientId, ClientPid) -> gen_server:start_link(?MODULE, [CleanSess, ClientId, ClientPid], []). @@ -133,7 +136,8 @@ start_link(CleanSess, ClientId, ClientPid) -> %% @doc Resume a session. %% @end %%------------------------------------------------------------------------------ -resume(Session, ClientId, ClientPid) when is_pid(Session) -> +-spec resume(pid(), binary(), pid()) -> ok. +resume(Session, ClientId, ClientPid) -> gen_server:cast(Session, {resume, ClientId, ClientPid}). %%------------------------------------------------------------------------------ @@ -141,73 +145,69 @@ resume(Session, ClientId, ClientPid) when is_pid(Session) -> %% @end %%------------------------------------------------------------------------------ -spec destroy(Session:: pid(), ClientId :: binary()) -> ok. -destroy(Session, ClientId) when is_pid(Session) -> +destroy(Session, ClientId) -> gen_server:call(Session, {destroy, ClientId}). +%%------------------------------------------------------------------------------ +%% @doc Subscribe Topics +%% @end +%%------------------------------------------------------------------------------ +-spec subscribe(pid(), [{binary(), mqtt_qos()}]) -> {ok, [mqtt_qos()]}. +subscribe(Session, TopicTable) -> + gen_server:call(Session, {subscribe, TopicTable}). + %%------------------------------------------------------------------------------ %% @doc Publish message %% @end %%------------------------------------------------------------------------------ -spec publish(Session :: pid(), {mqtt_qos(), mqtt_message()}) -> ok. -publish(Session, Msg = #mqtt_message{qos = ?QOS_0}) when is_pid(Session) -> +publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> %% publish qos0 directly emqttd_pubsub:publish(Msg); -publish(Session, Msg = #mqtt_message{qos = ?QOS_1}) when is_pid(Session) -> - %% publish qos1 directly, and client will puback +publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> + %% publish qos1 directly, and client will puback automatically emqttd_pubsub:publish(Msg); -publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) when is_pid(Session) -> +publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> %% publish qos2 by session - gen_server:cast(Session, {publish, Msg}). + gen_server:call(Session, {publish, Msg}). %%------------------------------------------------------------------------------ %% @doc PubAck message %% @end %%------------------------------------------------------------------------------ --spec puback(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. -puback(Session, MsgId) when is_pid(Session) -> +-spec puback(pid(), mqtt_msgid()) -> ok. +puback(Session, MsgId) -> gen_server:cast(Session, {puback, MsgId}). --spec pubrec(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. -pubrec(Session, MsgId) when is_pid(Session) -> +-spec pubrec(pid(), mqtt_msgid()) -> ok. +pubrec(Session, MsgId) -> gen_server:cast(Session, {pubrec, MsgId}). --spec pubrel(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. -pubrel(Session, MsgId) when is_pid(Session) -> +-spec pubrel(pid(), mqtt_msgid()) -> ok. +pubrel(Session, MsgId) -> gen_server:cast(Session, {pubrel, MsgId}). --spec pubcomp(Session :: pid(), MsgId :: mqtt_packet_id()) -> ok. -pubcomp(Session, MsgId) when is_pid(Session) -> +-spec pubcomp(pid(), mqtt_msgid()) -> ok. +pubcomp(Session, MsgId) -> gen_server:cast(Session, {pubcomp, MsgId}). -%%------------------------------------------------------------------------------ -%% @doc Subscribe Topics -%% @end -%%------------------------------------------------------------------------------ --spec subscribe(Session :: pid(), [{binary(), mqtt_qos()}]) -> {ok, [mqtt_qos()]}. -subscribe(Session, Topics) when is_pid(Session) -> - gen_server:call(Session, {subscribe, Topics}). - %%------------------------------------------------------------------------------ %% @doc Unsubscribe Topics %% @end %%------------------------------------------------------------------------------ --spec unsubscribe(Session :: pid(), [Topic :: binary()]) -> ok. -unsubscribe(Session, Topics) when is_pid(Session) -> +-spec unsubscribe(pid(), [binary()]) -> ok. +unsubscribe(Session, Topics) -> gen_server:call(Session, {unsubscribe, Topics}). %%%============================================================================= %%% gen_server callbacks %%%============================================================================= + init([CleanSess, ClientId, ClientPid]) -> - if - CleanSess =:= false -> - process_flag(trap_exit, true), - true = link(ClientPid); - CleanSess =:= true -> - ok - end, + process_flag(trap_exit, true), + true = link(ClientPid), QEnv = emqttd:env(mqtt, queue), SessEnv = emqttd:env(mqtt, session), PendingQ = emqttd_mqueue:new(ClientId, QEnv), @@ -227,25 +227,25 @@ init([CleanSess, ClientId, ClientPid]) -> await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv), max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv), expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600, - timestamp = os:timestamp() - }, + timestamp = os:timestamp()}, {ok, Session, hibernate}. -handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId, subscriptions = Subscriptions}) -> +handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId, + subscriptions = Subscriptions}) -> %% subscribe first and don't care if the subscriptions have been existed {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), - lager:info([{client, ClientId}], "Session ~s subscribe ~p. Granted QoS: ~p", - [ClientId, Topics, GrantedQos]), + lager:info([{client, ClientId}], "Session ~s subscribe ~p, Granted QoS: ~p", + [ClientId, Topics, GrantedQos]), Subscriptions1 = lists:foldl(fun({Topic, Qos}, Acc) -> case lists:keyfind(Topic, 1, Acc) of {Topic, Qos} -> - lager:warning([{client, ClientId}], "~s resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc; + lager:warning([{client, ClientId}], "Session ~s resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc; {Topic, Old} -> - lager:warning([{client, ClientId}], "~s resubscribe ~p: old qos=~p, new qos=~p", + lager:warning([{client, ClientId}], "Session ~s resubscribe ~p: old qos=~p, new qos=~p", [ClientId, Topic, Old, Qos]), lists:keyreplace(Topic, 1, Acc, {Topic, Qos}); false -> @@ -263,7 +263,7 @@ handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId %%unsubscribe from topic tree ok = emqttd_pubsub:unsubscribe(Topics), - lager:info([{client, ClientId}], "Client ~s unsubscribe ~p.", [ClientId, Topics]), + lager:info([{client, ClientId}], "Session ~s unsubscribe ~p.", [ClientId, Topics]), Subscriptions1 = lists:foldl(fun(Topic, Acc) -> @@ -277,12 +277,24 @@ handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId {reply, ok, Session#session{subscriptions = Subscriptions1}}; +handle_call({publish, Message = #mqtt_message{qos = ?QOS_2, msgid = MsgId}}, _From, + Session = #session{clientid = ClientId, awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> + case check_awaiting_rel(Session) of + true -> + TRef = timer(Timeout, {timeout, awaiting_rel, MsgId}), + {reply, ok, Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}}; + false -> + lager:error([{clientid, ClientId}], "Session ~s " + " dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]), + {reply, {error, dropped}, Session} + end; + handle_call({destroy, ClientId}, _From, Session = #session{clientid = ClientId}) -> lager:warning("Session ~s destroyed", [ClientId]), {stop, {shutdown, destroy}, ok, Session}; handle_call(Req, _From, State) -> - lager:error("Unexpected Request: ~p", [Req]), + lager:critical("Unexpected Request: ~p", [Req]), {reply, {error, badreq}, State}. handle_cast({resume, ClientId, ClientPid}, State = #session{ @@ -331,9 +343,6 @@ handle_cast({resume, ClientId, ClientPid}, State = #session{ pending_queue = emqttd_queue:clear(Queue), expired_timer = undefined}, hibernate}; -handle_cast({publish, Message = #mqtt_message{qos = ?QOS_2}}, Session) -> - {noreply, publish_qos2(Message, Session)}; - handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, inflight_queue = Q, awaiting_ack = Awaiting}) -> case maps:find(MsgId, Awaiting) of @@ -362,14 +371,14 @@ handle_cast({pubrec, MsgId}, Session = #session{clientid = ClientId, {noreply, Session} end; -handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> - case maps:find(MsgId, Awaiting) of +handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = AwaitingRel}) -> + case maps:find(MsgId, AwaitingRel) of {ok, {Msg, TRef}} -> catch erlang:cancel_timer(TRef), emqttd_pubsub:publish(Msg), - {noreply, Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}}; + {noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}}; error -> - lager:error("Session ~s cannot find PUBREL'~p'!", [ClientId, MsgId]), + lager:error("Session ~s cannot find PUBREL '~p'!", [ClientId, MsgId]), {noreply, Session} end; @@ -408,6 +417,10 @@ handle_info({'EXIT', ClientPid, Reason}, Session = #session{clean_sess = false, TRef = timer(Expires * 1000, session_expired), {noreply, Session#session{expired_timer = TRef}}; +handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, client_pid = ClientPid}) -> + %%TODO: reason... + {stop, normal, Session}; + handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> lager:critical("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), {noreply, State}; @@ -456,19 +469,6 @@ code_change(_OldVsn, Session, _Extra) -> %% @end %%------------------------------------------------------------------------------ -publish_qos2(Message = #mqtt_message{qos = ?QOS_2,msgid = MsgId}, Session = #session{clientid = ClientId, - awaiting_rel = AwaitingRel, - await_rel_timeout = Timeout}) -> - - case check_awaiting_rel(Session) of - true -> - TRef = timer(Timeout, {timeout, awaiting_rel, MsgId}), - Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}; - false -> - lager:error([{clientid, ClientId}], "Session ~s " - " dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]), - Session - end. check_awaiting_rel(#session{max_awaiting_rel = 0}) -> true; From 38e0ba08d24f87740f87d904ad2987b010ac8c14 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Jun 2015 01:25:08 +0800 Subject: [PATCH 077/104] session, finally count down --- apps/emqttd/src/emqttd_inflight.erl | 71 ---- apps/emqttd/src/emqttd_mqueue.erl | 17 +- apps/emqttd/src/emqttd_msg_store.erl | 2 +- apps/emqttd/src/emqttd_session.erl | 484 +++++++++++++++------------ 4 files changed, 278 insertions(+), 296 deletions(-) delete mode 100644 apps/emqttd/src/emqttd_inflight.erl diff --git a/apps/emqttd/src/emqttd_inflight.erl b/apps/emqttd/src/emqttd_inflight.erl deleted file mode 100644 index 8f3df986c..000000000 --- a/apps/emqttd/src/emqttd_inflight.erl +++ /dev/null @@ -1,71 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% Inflight window of message queue. Wrap a list with len. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttd_inflight). - --author("Feng Lee "). - --include("emqttd.hrl"). - --export([new/2, is_full/1, len/1, in/2, ack/2]). - --define(MAX_SIZE, 100). - --record(inflight, {name, q = [], len = 0, size = ?MAX_SIZE}). - --type inflight() :: #inflight{}. - --export_type([inflight/0]). - -new(Name, Max) -> - #inflight{name = Name, size = Max}. - -is_full(#inflight{size = 0}) -> - false; -is_full(#inflight{len = Len, size = Size}) when Len < Size -> - false; -is_full(_Inflight) -> - true. - -len(#inflight{len = Len}) -> - Len. - -in(_Msg, #inflight{len = Len, size = Size}) - when Len =:= Size -> {error, full}; - -in(Msg = #mqtt_message{msgid = MsgId}, Inflight = #inflight{q = Q, len = Len}) -> - {ok, Inflight#inflight{q = [{MsgId, Msg}|Q], len = Len +1}}. - -ack(MsgId, Inflight = #inflight{q = Q, len = Len}) -> - case lists:keyfind(MsgId, 1, Q) of - false -> - lager:error("Inflight(~s) cannot find msgid: ~p", [MsgId]), - Inflight; - _Msg -> - Inflight#inflight{q = lists:keydelete(MsgId, 1, Q), len = Len - 1} - end. - diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index c9c7527be..85d598844 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -112,28 +112,21 @@ len(#mqueue{len = Len}) -> Len. %% @end %%------------------------------------------------------------------------------ --spec in({new | old, mqtt_message()}, mqueue()) -> mqueue(). +-spec in({newcome | pending, mqtt_message()}, mqueue()) -> mqueue(). %% drop qos0 in({_, #mqtt_message{qos = ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; %% simply drop the oldest one if queue is full, improve later -in({new, Msg}, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen}) +in(Msg, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen}) when Len =:= MaxLen -> {{value, OldMsg}, Q2} = queue:out(Q), - lager:error("queue(~s) drop message: ~p", [Name, OldMsg]), + lager:error("MQueue(~s) drop message: ~p", [Name, OldMsg]), MQ#mqueue{q = queue:in(Msg, Q2)}; -in({old, Msg}, MQ = #mqueue{name = Name, len = Len, max_len = MaxLen}) - when Len =:= MaxLen -> - lager:error("queue(~s) drop message: ~p", [Name, Msg]), MQ; - -in({new, Msg}, MQ = #mqueue{q = Q, len = Len}) -> - maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); - -in({old, Msg}, MQ = #mqueue{q = Q, len = Len}) -> - MQ#mqueue{q = queue:in_r(Msg, Q), len = Len + 1}. +in(Msg, MQ = #mqueue{q = Q, len = Len}) -> + maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}). out(MQ = #mqueue{len = 0}) -> {empty, MQ}; diff --git a/apps/emqttd/src/emqttd_msg_store.erl b/apps/emqttd/src/emqttd_msg_store.erl index f304d0aad..fb2df9171 100644 --- a/apps/emqttd/src/emqttd_msg_store.erl +++ b/apps/emqttd/src/emqttd_msg_store.erl @@ -123,7 +123,7 @@ redeliver(Topic, CPid) when is_binary(Topic) andalso is_pid(CPid) -> dispatch(_CPid, []) -> ignore; dispatch(CPid, Msgs) when is_list(Msgs) -> - CPid ! {dispatch, [Msg || Msg <- Msgs]}; + [CPid ! {dispatch, Msg} || Msg <- Msgs]; dispatch(CPid, Msg) when is_record(Msg, mqtt_message) -> CPid ! {dispatch, Msg}. diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl index 384e49ae1..5cba1619d 100644 --- a/apps/emqttd/src/emqttd_session.erl +++ b/apps/emqttd/src/emqttd_session.erl @@ -40,6 +40,8 @@ %%% %%% 5. Optionally, QoS 0 messages pending transmission to the Client. %%% +%%% State of Message: newcome, inflight, pending +%%% %%% @end %%%----------------------------------------------------------------------------- @@ -86,13 +88,15 @@ %% QoS 1 and QoS 2 messages which have been sent to the Client, %% but have not been completely acknowledged. %% Client <- Broker - inflight_queue :: emqttd_inflight:inflight(), + inflight_queue :: list(), + + max_inflight = 0, %% All qos1, qos2 messages published to when client is disconnected. %% QoS 1 and QoS 2 messages pending transmission to the Client. %% %% Optionally, QoS 0 messages pending transmission to the Client. - pending_queue :: emqttd_mqueue:mqueue(), + message_queue :: emqttd_mqueue:mqueue(), %% Inflight qos2 messages received from client and waiting for pubrel. %% QoS 2 messages which have been received from the Client, @@ -100,25 +104,26 @@ %% Client -> Broker awaiting_rel :: map(), + %% Awaiting PUBREL timeout + await_rel_timeout = 8, + + %% Max Packets that Awaiting PUBREL + max_awaiting_rel = 100, + %% Awaiting timers for ack, rel and comp. awaiting_ack :: map(), - awaiting_comp :: map(), - %% Retries to resend the unacked messages unack_retries = 3, %% 4, 8, 16 seconds if 3 retries:) unack_timeout = 4, - %% Awaiting PUBREL timeout - await_rel_timeout = 8, - - %% Max Packets that Awaiting PUBREL - max_awaiting_rel = 100, + %% Awaiting for PUBCOMP + awaiting_comp :: map(), %% session expired after 48 hours - expired_after = 48, + expired_after = 172800, expired_timer, @@ -128,7 +133,7 @@ %% @doc Start a session. %% @end %%------------------------------------------------------------------------------ --spec start_link(boolean(), binary(), pid()) -> {ok, pid()} | {error, any()}. +-spec start_link(boolean(), mqtt_clientid(), pid()) -> {ok, pid()} | {error, any()}. start_link(CleanSess, ClientId, ClientPid) -> gen_server:start_link(?MODULE, [CleanSess, ClientId, ClientPid], []). @@ -136,7 +141,7 @@ start_link(CleanSess, ClientId, ClientPid) -> %% @doc Resume a session. %% @end %%------------------------------------------------------------------------------ --spec resume(pid(), binary(), pid()) -> ok. +-spec resume(pid(), mqtt_clientid(), pid()) -> ok. resume(Session, ClientId, ClientPid) -> gen_server:cast(Session, {resume, ClientId, ClientPid}). @@ -144,7 +149,7 @@ resume(Session, ClientId, ClientPid) -> %% @doc Destroy a session. %% @end %%------------------------------------------------------------------------------ --spec destroy(Session:: pid(), ClientId :: binary()) -> ok. +-spec destroy(pid(), mqtt_clientid()) -> ok. destroy(Session, ClientId) -> gen_server:call(Session, {destroy, ClientId}). @@ -160,7 +165,7 @@ subscribe(Session, TopicTable) -> %% @doc Publish message %% @end %%------------------------------------------------------------------------------ --spec publish(Session :: pid(), {mqtt_qos(), mqtt_message()}) -> ok. +-spec publish(pid(), mqtt_message()) -> ok. publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> %% publish qos0 directly emqttd_pubsub:publish(Msg); @@ -210,24 +215,23 @@ init([CleanSess, ClientId, ClientPid]) -> true = link(ClientPid), QEnv = emqttd:env(mqtt, queue), SessEnv = emqttd:env(mqtt, session), - PendingQ = emqttd_mqueue:new(ClientId, QEnv), - InflightQ = emqttd_inflight:new(ClientId, emqttd_opts:g(max_inflight, SessEnv)), Session = #session{ - clean_sess = CleanSess, - clientid = ClientId, - client_pid = ClientPid, - subscriptions = [], - inflight_queue = InflightQ, - pending_queue = PendingQ, - awaiting_rel = #{}, - awaiting_ack = #{}, - awaiting_comp = #{}, - unack_retries = emqttd_opts:g(unack_retries, SessEnv), - unack_timeout = emqttd_opts:g(unack_timeout, SessEnv), - await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv), - max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv), - expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600, - timestamp = os:timestamp()}, + clean_sess = CleanSess, + clientid = ClientId, + client_pid = ClientPid, + subscriptions = [], + inflight_queue = [], + max_inflight = emqttd_opts:g(max_inflight, SessEnv, 0), + message_queue = emqttd_mqueue:new(ClientId, QEnv), + awaiting_rel = #{}, + awaiting_ack = #{}, + awaiting_comp = #{}, + unack_retries = emqttd_opts:g(unack_retries, SessEnv), + unack_timeout = emqttd_opts:g(unack_timeout, SessEnv), + await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv), + max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv), + expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600, + timestamp = os:timestamp()}, {ok, Session, hibernate}. handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId, @@ -237,33 +241,36 @@ handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId, {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), lager:info([{client, ClientId}], "Session ~s subscribe ~p, Granted QoS: ~p", - [ClientId, Topics, GrantedQos]), + [ClientId, Topics, GrantedQos]), Subscriptions1 = lists:foldl(fun({Topic, Qos}, Acc) -> - case lists:keyfind(Topic, 1, Acc) of - {Topic, Qos} -> - lager:warning([{client, ClientId}], "Session ~s resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc; - {Topic, Old} -> - lager:warning([{client, ClientId}], "Session ~s resubscribe ~p: old qos=~p, new qos=~p", - [ClientId, Topic, Old, Qos]), - lists:keyreplace(Topic, 1, Acc, {Topic, Qos}); - false -> - %%TODO: the design is ugly, rewrite later...:( - %% : 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_msg_store:redeliver(Topic, self()), - [{Topic, Qos} | Acc] - end - end, Subscriptions, Topics), + case lists:keyfind(Topic, 1, Acc) of + {Topic, Qos} -> + lager:warning([{client, ClientId}], "Session ~s " + "resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc; + {Topic, OldQos} -> + lager:warning([{client, ClientId}], "Session ~s " + "resubscribe ~p: old qos=~p, new qos=~p", [ClientId, Topic, OldQos, Qos]), + lists:keyreplace(Topic, 1, Acc, {Topic, Qos}); + false -> + %%TODO: the design is ugly, rewrite later...:( + %% : 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_msg_store:redeliver(Topic, self()), + [{Topic, Qos} | Acc] + end + end, Subscriptions, Topics), {reply, {ok, GrantedQos}, Session#session{subscriptions = Subscriptions1}}; -handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId, subscriptions = Subscriptions}) -> +handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId, + subscriptions = Subscriptions}) -> - %%unsubscribe from topic tree + %% unsubscribe from topic tree ok = emqttd_pubsub:unsubscribe(Topics), - lager:info([{client, ClientId}], "Session ~s unsubscribe ~p.", [ClientId, Topics]), + + lager:info([{client, ClientId}], "Session ~s unsubscribe ~p", [ClientId, Topics]), Subscriptions1 = lists:foldl(fun(Topic, Acc) -> @@ -271,21 +278,24 @@ handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId {Topic, _Qos} -> lists:keydelete(Topic, 1, Acc); false -> - lager:warning([{client, ClientId}], "~s not subscribe ~s", [ClientId, Topic]), Acc + lager:warning([{client, ClientId}], "Session ~s not subscribe ~s", [ClientId, Topic]), Acc end end, Subscriptions, Topics), {reply, ok, Session#session{subscriptions = Subscriptions1}}; -handle_call({publish, Message = #mqtt_message{qos = ?QOS_2, msgid = MsgId}}, _From, - Session = #session{clientid = ClientId, awaiting_rel = AwaitingRel, await_rel_timeout = Timeout}) -> +handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, msgid = MsgId}}, _From, + Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}) -> case check_awaiting_rel(Session) of - true -> + true -> TRef = timer(Timeout, {timeout, awaiting_rel, MsgId}), - {reply, ok, Session#session{awaiting_rel = maps:put(MsgId, {Message, TRef}, AwaitingRel)}}; + AwaitingRel1 = maps:put(MsgId, {Msg, TRef}, AwaitingRel), + {reply, ok, Session#session{awaiting_rel = AwaitingRel1}}; false -> - lager:error([{clientid, ClientId}], "Session ~s " - " dropped Qos2 message for too many awaiting_rel: ~p", [ClientId, Message]), + lager:critical([{clientid, ClientId}], "Session ~s dropped Qos2 message " + "for too many awaiting_rel: ~p", [ClientId, Msg]), {reply, {error, dropped}, Session} end; @@ -297,59 +307,52 @@ handle_call(Req, _From, State) -> lager:critical("Unexpected Request: ~p", [Req]), {reply, {error, badreq}, State}. -handle_cast({resume, ClientId, ClientPid}, State = #session{ - clientid = ClientId, - client_pid = OldClientPid, - pending_queue = Queue, - awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp, - expired_timer = ETimer}) -> +handle_cast({resume, ClientId, ClientPid}, Session) -> + + #session{clientid = ClientId, + client_pid = OldClientPid, + inflight_queue = InflightQ, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + expired_timer = ETimer} = Session, lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), - %% kick old client... - if - OldClientPid =:= undefined -> - ok; - OldClientPid =:= ClientPid -> - ok; - true -> - lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), - unlink(OldClientPid), - OldClientPid ! {stop, duplicate_id, ClientPid} - end, + %% cancel expired timer + cancel_timer(ETimer), - %% cancel timeout timer - emqttd_util:cancel_timer(ETimer), + kick(ClientId, ClientPid, OldClientPid), - %% redelivery PUBREL - lists:foreach(fun(MsgId) -> - ClientPid ! {redeliver, {?PUBREL, MsgId}} - end, maps:keys(AwaitingComp)), + %% Redeliver PUBREL + [ClientPid ! {redeliver, {?PUBREL, MsgId}} || MsgId <- maps:keys(AwaitingComp)], - %% redelivery messages that awaiting PUBACK or PUBREC - Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end, - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Dup(Msg)}} - end, maps:values(AwaitingAck)), + %% Clear awaiting_ack timers + [cancel_timer(TRef) || {_, TRef} <- maps:values(AwaitingAck)], - %% send offline messages - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Msg}} - end, emqttd_queue:all(Queue)), + %% Clear awaiting_comp timers + [cancel_timer(TRef) || TRef <- maps:values(AwaitingComp)], - {noreply, State#session{client_pid = ClientPid, - %%TODO: - pending_queue = emqttd_queue:clear(Queue), - expired_timer = undefined}, hibernate}; + Session1 = Session#session{client_pid = ClientPid, + awaiting_ack = #{}, + awaiting_comp = #{}, + expired_timer = undefined}, + %% Redeliver inflight messages + Session2 = + lists:foldl(fun({_Id, Msg}, Sess) -> + redeliver(Msg#mqtt_message{dup = true}, Sess) + end, Session1, lists:reverse(InflightQ)), -handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, inflight_queue = Q, awaiting_ack = Awaiting}) -> + %% Dequeue pending messages + {noreply, dequeue(Session2), hibernate}; + +%% PUBRAC +handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, awaiting_ack = Awaiting}) -> case maps:find(MsgId, Awaiting) of {ok, {_, TRef}} -> - catch erlang:cancel_timer(TRef), - {noreply, dispatch(Session#session{inflight_queue = emqttd_inflight:ack(MsgId, Q), - awaiting_ack = maps:remove(MsgId, Awaiting)})}; + cancel_timer(TRef), + Session1 = acked(MsgId, Session), + {noreply, dequeue(Session1)}; error -> lager:error("Session ~s cannot find PUBACK '~p'!", [ClientId, MsgId]), {noreply, Session} @@ -362,33 +365,36 @@ handle_cast({pubrec, MsgId}, Session = #session{clientid = ClientId, await_rel_timeout = Timeout}) -> case maps:find(MsgId, AwaitingAck) of {ok, {_, TRef}} -> - catch erlang:cancel_timer(TRef), + cancel_timer(TRef), TRef1 = timer(Timeout, {timeout, awaiting_comp, MsgId}), - {noreply, dispatch(Session#session{awaiting_ack = maps:remove(MsgId, AwaitingAck), - awaiting_comp = maps:put(MsgId, TRef1, AwaitingComp)})}; + Session1 = acked(MsgId, Session#session{awaiting_comp = maps:put(MsgId, TRef1, AwaitingComp)}), + {noreply, dequeue(Session1)}; error -> lager:error("Session ~s cannot find PUBREC '~p'!", [ClientId, MsgId]), {noreply, Session} end; -handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = AwaitingRel}) -> +%% PUBREL +handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel}) -> case maps:find(MsgId, AwaitingRel) of {ok, {Msg, TRef}} -> - catch erlang:cancel_timer(TRef), + cancel_timer(TRef), emqttd_pubsub:publish(Msg), {noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}}; error -> - lager:error("Session ~s cannot find PUBREL '~p'!", [ClientId, MsgId]), + lager:error("Session ~s cannot find PUBREL: msgid=~p!", [ClientId, MsgId]), {noreply, Session} end; %% PUBCOMP handle_cast({pubcomp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = AwaitingComp}) -> - case maps:is_key(MsgId, AwaitingComp) of - true -> + case maps:find(MsgId, AwaitingComp) of + {ok, TRef} -> + cancel_timer(TRef), {noreply, Session#session{awaiting_comp = maps:remove(MsgId, AwaitingComp)}}; - false -> - lager:error("Session ~s cannot find PUBREC MsgId '~p'", [ClientId, MsgId]), + error -> + lager:error("Session ~s cannot find PUBCOMP: MsgId=~p", [ClientId, MsgId]), {noreply, Session} end; @@ -396,61 +402,103 @@ handle_cast(Msg, State) -> lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), {noreply, State}. -handle_info({dispatch, MsgList}, Session) when is_list(MsgList) -> - NewSession = lists:foldl(fun(Msg, S) -> - dispatch({new, Msg}, S) - end, Session, MsgList), - {noreply, NewSession}; +%% Queue messages when client is offline +handle_info({dispatch, Msg}, Session = #session{client_pid = undefined, + message_queue = Q}) + when is_record(Msg, mqtt_message) -> + {noreply, Session#session{message_queue = emqttd_mqueue:in(Msg, Q)}}; -handle_info({dispatch, {old, Msg}}, Session) when is_record(Msg, mqtt_message) -> - {noreply, dispatch({old, Msg}, Session)}; +%% Dispatch qos0 message directly to client +handle_info({dispatch, Msg = #mqtt_message{qos = ?QOS_0}}, + Session = #session{client_pid = ClientPid}) -> + ClientPid ! {deliver, Msg}, + {noreply, Session}; -handle_info({dispatch, Msg}, Session) when is_record(Msg, mqtt_message) -> - {noreply, dispatch({new, Msg}, Session)}; +handle_info({dispatch, Msg = #mqtt_message{qos = QoS}}, + Session = #session{clientid = ClientId, message_queue = MsgQ}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + + case check_inflight(Session) of + true -> + {noreply, deliver(Msg, Session)}; + false -> + lager:warning([{client, ClientId}], "Session ~s inflight queue is full!", [ClientId]), + {noreply, Session#session{message_queue = emqttd_mqueue:in(Msg, MsgQ)}} + end; + +handle_info({timeout, awaiting_ack, MsgId}, Session = #session{client_pid = undefined, + awaiting_ack = AwaitingAck}) -> + %% just remove awaiting + {noreply, Session#session{awaiting_ack = maps:remove(MsgId, AwaitingAck)}}; + +handle_info({timeout, awaiting_ack, MsgId}, Session = #session{clientid = ClientId, + inflight_queue = InflightQ, + awaiting_ack = AwaitingAck}) -> + case maps:find(MsgId, AwaitingAck) of + {ok, {{0, _Timeout}, _TRef}} -> + Session1 = Session#session{inflight_queue = lists:keydelete(MsgId, 1, InflightQ), + awaiting_ack = maps:remove(MsgId, AwaitingAck)}, + {noreply, dequeue(Session1)}; + {ok, {{Retries, Timeout}, _TRef}} -> + TRef = timer(Timeout, {timeout, awaiting_ack, MsgId}), + AwaitingAck1 = maps:put(MsgId, {{Retries-1, Timeout*2}, TRef}, AwaitingAck), + {noreply, Session#session{awaiting_ack = AwaitingAck1}}; + error -> + lager:error([{client, ClientId}], "Session ~s " + "cannot find Awaiting Ack:~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({timeout, awaiting_rel, MsgId}, Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel}) -> + case maps:find(MsgId, AwaitingRel) of + {ok, {Msg, _TRef}} -> + lager:error([{client, ClientId}], "Session ~s AwaitingRel Timout!~n" + "Drop Message:~p", [ClientId, Msg]), + {noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}}; + error -> + lager:error([{client, ClientId}], "Session ~s Cannot find AwaitingRel: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({timeout, awaiting_comp, MsgId}, Session = #session{clientid = ClientId, + awaiting_comp = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, _TRef} -> + lager:error([{client, ClientId}], "Session ~s " + "Awaiting PUBCOMP Timout: MsgId=~p!", [ClientId, MsgId]), + {noreply, Session#session{awaiting_comp = maps:remove(MsgId, Awaiting)}}; + error -> + lager:error([{client, ClientId}], "Session ~s " + "Cannot find Awaiting PUBCOMP: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, + client_pid = ClientPid}) -> + {stop, normal, Session}; handle_info({'EXIT', ClientPid, Reason}, Session = #session{clean_sess = false, clientid = ClientId, client_pid = ClientPid, expired_after = Expires}) -> - %%TODO: Clean puback, pubrel, pubcomp timers - lager:info("Session ~s: client ~p exited for ~p", [ClientId, ClientPid, Reason]), - TRef = timer(Expires * 1000, session_expired), - {noreply, Session#session{expired_timer = TRef}}; + lager:info("Session ~s unlink with client ~p: reason=~p", [ClientId, ClientPid, Reason]), + TRef = timer(Expires, session_expired), + {noreply, Session#session{expired_timer = TRef}, hibernate}; -handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, client_pid = ClientPid}) -> - %%TODO: reason... - {stop, normal, Session}; +handle_info({'EXIT', Pid, _Reason}, Session = #session{clientid = ClientId, + client_pid = ClientPid}) -> + + lager:error("Session ~s received unexpected EXIT:" + " client_pid=~p, exit_pid=~p", [ClientId, ClientPid, Pid]), + {noreply, Session}; -handle_info({'EXIT', ClientPid0, _Reason}, State = #session{client_pid = ClientPid}) -> - lager:critical("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), - {noreply, State}; - -handle_info(session_expired, State = #session{clientid = ClientId}) -> +handle_info(session_expired, Session = #session{clientid = ClientId}) -> lager:error("Session ~s expired, shutdown now!", [ClientId]), - {stop, {shutdown, expired}, State}; + {stop, {shutdown, expired}, Session}; -handle_info({timeout, awaiting_rel, MsgId}, Session = #session{clientid = ClientId, awaiting_rel = Awaiting}) -> - case maps:find(MsgId, Awaiting) of - {ok, {Msg, _TRef}} -> - lager:error([{client, ClientId}], "Session ~s Awaiting Rel Timout!~nDrop Message:~p", [ClientId, Msg]), - {noreply, Session#session{awaiting_rel = maps:remove(MsgId, Awaiting)}}; - error -> - lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting Rel: MsgId=~p", [ClientId, MsgId]), - {noreply, Session} - end; - -handle_info({timeout, awaiting_comp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = Awaiting}) -> - case maps:find(MsgId, Awaiting) of - {ok, _TRef} -> - lager:error([{client, ClientId}], "Session ~s Awaiting PUBCOMP Timout: MsgId=~p!", [ClientId, MsgId]), - {noreply, Session#session{awaiting_comp = maps:remove(MsgId, Awaiting)}}; - error -> - lager:error([{client, ClientId}], "Session ~s Cannot find Awaiting PUBCOMP: MsgId=~p", [ClientId, MsgId]), - {noreply, Session} - end; - -handle_info(Info, Session) -> - lager:critical("Unexpected Info: ~p, Session: ~p", [Info, Session]), +handle_info(Info, Session = #session{clientid = ClientId}) -> + lager:critical("Session ~s received unexpected info: ~p", [ClientId, Info]), {noreply, Session}. terminate(_Reason, _Session) -> @@ -464,11 +512,26 @@ code_change(_OldVsn, Session, _Extra) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @private -%% @doc Plubish Qos2 message from client -> broker, and then wait for pubrel. -%% @end +%% Kick duplicated client %%------------------------------------------------------------------------------ +kick(_ClientId, _ClientPid, undefined) -> + ok; +kick(_ClientId, ClientPid, ClientPid) -> + ok; +kick(ClientId, ClientPid, OldClientPid) -> + lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), + unlink(OldClientPid), + OldClientPid ! {stop, duplicate_id, ClientPid}. + +%%------------------------------------------------------------------------------ +%% Check inflight and awaiting_rel +%%------------------------------------------------------------------------------ + +check_inflight(#session{max_inflight = 0}) -> + true; +check_inflight(#session{max_inflight = Max, inflight_queue = Q}) -> + Max > length(Q). check_awaiting_rel(#session{max_awaiting_rel = 0}) -> true; @@ -476,72 +539,61 @@ check_awaiting_rel(#session{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) -> maps:size(AwaitingRel) < MaxLen. -%%%============================================================================= -%%% Dispatch message from broker -> client. -%%%============================================================================= +%%------------------------------------------------------------------------------ +%% Dequeue and Deliver +%%------------------------------------------------------------------------------ -dispatch(Session = #session{client_pid = undefined}) -> - %% do nothing +dequeue(Session = #session{client_pid = undefined}) -> + %% do nothing if client is disconnected Session; -dispatch(Session = #session{pending_queue = PendingQ}) -> - case emqttd_mqueue:out(PendingQ) of - {empty, _Q} -> - Session; - {{value, Msg}, Q1} -> - self() ! {dispatch, {old, Msg}}, - Session#session{pending_queue = Q1} - end. - -%% queued the message if client is offline -dispatch({Type, Msg}, Session = #session{client_pid = undefined, - pending_queue= PendingQ}) -> - Session#session{pending_queue = emqttd_mqueue:in({Type, Msg}, PendingQ)}; - -%% dispatch qos0 directly to client process -dispatch({_Type, Msg} = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> - ClientPid ! {deliver, Msg}, Session; - -%% dispatch qos1/2 message and wait for puback -dispatch({Type, Msg = #mqtt_message{qos = Qos}}, Session = #session{clientid = ClientId, - client_pid = ClientPid, - message_id = MsgId, - pending_queue = PendingQ, - inflight_queue= InflightQ}) - when Qos =:= ?QOS_1 orelse Qos =:= ?QOS_2 -> - %% assign id first - Msg1 = Msg#mqtt_message{msgid = MsgId}, - Msg2 = - if - Qos =:= ?QOS_1 -> Msg1; - Qos =:= ?QOS_2 -> Msg1#mqtt_message{dup = false} - end, - case emqttd_inflight:in(Msg1, InflightQ) of - {error, full} -> - lager:error("Session ~s inflight queue is full!", [ClientId]), - Session#session{pending_queue = emqttd_mqueue:in({Type, Msg}, PendingQ)}; - {ok, InflightQ1} -> - ClientPid ! {deliver, Msg1}, - await_ack(Msg1, next_msgid(Session#session{inflight_queue = InflightQ1})) +dequeue(Session) -> + case check_inflight(Session) of + true -> dequeue2(Session); + false -> Session end. -deliver(Msg, Session) -> - ok. +dequeue2(Session = #session{message_queue = Q}) -> + case emqttd_mqueue:out(Q) of + {empty, _Q} -> Session; + {{value, Msg}, Q1} -> + Session1 = deliver(Msg, Session#session{message_queue = Q1}), + dequeue(Session1) %% dequeue more + end. -await(Msg, Session) -> - ok. +deliver(Msg = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> + ClientPid ! {deliver, Msg}, Session; -% message(qos1/2) is awaiting ack -await_ack(Msg = #mqtt_message{msgid = MsgId}, Session = #session{awaiting_ack = Awaiting, - unack_retries = Retries, - unack_timeout = Timeout}) -> - - TRef = timer(Timeout * 1000, {retry, MsgId}), +deliver(Msg = #mqtt_message{qos = QoS}, Session = #session{message_id = MsgId, + client_pid = ClientPid, + inflight_queue = InflightQ}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + Msg1 = Msg#mqtt_message{msgid = MsgId, dup = false}, + ClientPid ! {deliver, Msg1}, + await(Msg1, next_msgid(Session#session{inflight_queue = [{MsgId, Msg1}|InflightQ]})). + +redeliver(Msg = #mqtt_message{qos = ?QOS_0}, Session) -> + deliver(Msg, Session); + +redeliver(Msg = #mqtt_message{qos = QoS}, Session = #session{client_pid = ClientPid}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + ClientPid ! {deliver, Msg}, + await(Msg, Session). + +%%------------------------------------------------------------------------------ +%% Awaiting ack for qos1, qos2 message +%%------------------------------------------------------------------------------ +await(#mqtt_message{msgid = MsgId}, Session = #session{awaiting_ack = Awaiting, + unack_retries = Retries, + unack_timeout = Timeout}) -> + TRef = timer(Timeout, {timeout, awaiting_ack, MsgId}), Awaiting1 = maps:put(MsgId, {{Retries, Timeout}, TRef}, Awaiting), Session#session{awaiting_ack = Awaiting1}. -timer(Timeout, TimeoutMsg) -> - erlang:send_after(Timeout * 1000, self(), TimeoutMsg). +acked(MsgId, Session = #session{inflight_queue = InflightQ, + awaiting_ack = Awaiting}) -> + Session#session{inflight_queue = lists:keydelete(MsgId, 1, InflightQ), + awaiting_ack = maps:remove(MsgId, Awaiting)}. next_msgid(Session = #session{message_id = 16#ffff}) -> Session#session{message_id = 1}; @@ -549,3 +601,11 @@ next_msgid(Session = #session{message_id = 16#ffff}) -> next_msgid(Session = #session{message_id = MsgId}) -> Session#session{message_id = MsgId + 1}. +timer(Timeout, TimeoutMsg) -> + erlang:send_after(Timeout * 1000, self(), TimeoutMsg). + +cancel_timer(undefined) -> + undefined; +cancel_timer(Ref) -> + catch erlang:cancel_timer(Ref). + From 41e87a54856471f138cd65fb797531b0ffd48d20 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Jun 2015 01:27:30 +0800 Subject: [PATCH 078/104] doc --- doc/design/Seq.md | 14 ++++++++++++++ doc/design/qos0_seq.png | Bin 0 -> 15103 bytes 2 files changed, 14 insertions(+) create mode 100644 doc/design/Seq.md create mode 100644 doc/design/qos0_seq.png diff --git a/doc/design/Seq.md b/doc/design/Seq.md new file mode 100644 index 000000000..924de84d3 --- /dev/null +++ b/doc/design/Seq.md @@ -0,0 +1,14 @@ +## QoS0 Publish Sequence + + +``` +title QoS0 Publish Sequence + +C1->PubSub: Publish QoS0 +PubSub-->S2: Dispatch QoS0 +S2-->C2: Deliver QoS0 +``` + +## QoS1 Publish Sequence + + diff --git a/doc/design/qos0_seq.png b/doc/design/qos0_seq.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ca32c0dbd0429d11a29de17dbf1adeb61fefe8 GIT binary patch literal 15103 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 literal 0 HcmV?d00001 From 02c773bb3f2f969f3b36734f270f037b29a6081d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 17 Jun 2015 11:32:12 +0800 Subject: [PATCH 079/104] drop --- apps/emqttd/src/emqttd_mqueue.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/apps/emqttd/src/emqttd_mqueue.erl index 85d598844..d30e755da 100644 --- a/apps/emqttd/src/emqttd_mqueue.erl +++ b/apps/emqttd/src/emqttd_mqueue.erl @@ -122,7 +122,7 @@ in({_, #mqtt_message{qos = ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> in(Msg, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen}) when Len =:= MaxLen -> {{value, OldMsg}, Q2} = queue:out(Q), - lager:error("MQueue(~s) drop message: ~p", [Name, OldMsg]), + lager:error("MQueue(~s) drop ~s", [Name, emqttd_message:format(OldMsg)]), MQ#mqueue{q = queue:in(Msg, Q2)}; in(Msg, MQ = #mqueue{q = Q, len = Len}) -> From f37de3a4a7334e5bcb4edec19e70d366f6cecff3 Mon Sep 17 00:00:00 2001 From: Feng Date: Wed, 17 Jun 2015 13:30:34 +0800 Subject: [PATCH 080/104] willmsg --- apps/emqttd/src/emqttd_protocol.erl | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/apps/emqttd/src/emqttd_protocol.erl b/apps/emqttd/src/emqttd_protocol.erl index 2c07abbee..ad5d69a3e 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/apps/emqttd/src/emqttd_protocol.erl @@ -302,20 +302,8 @@ shutdown(duplicate_id, _State) -> shutdown(_, #proto_state{clientid = undefined}) -> ignore; -shutdown(normal, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> - lager:info([{client, ClientId}], "Client ~s@~s: normal shutdown", - [ClientId, emqttd_net:format(Peername)]), - try_unregister(ClientId), - if - WillMsg =/= undefined -> - send_willmsg(ClientId, WillMsg); - true -> - ok - end, - emqttd_broker:foreach_hooks(client_disconnected, [normal, ClientId]); - shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> - lager:info([{client, ClientId}], "Protocol ~s@~s: Shutdown for ~p", + lager:info([{client, ClientId}], "Client ~s@~s: shutdown ~p", [ClientId, emqttd_net:format(Peername), Error]), send_willmsg(ClientId, WillMsg), try_unregister(ClientId), From 16b9c7a43801abfe09cb3f3227b2fe5617a44da7 Mon Sep 17 00:00:00 2001 From: Feng Date: Wed, 17 Jun 2015 13:34:09 +0800 Subject: [PATCH 081/104] 0.8.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b61e4a7..88690f432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ emqttd ChangeLog ================== +0.8.6-beta (2015-06-17) +------------------------- + +Bugfix: issue #175 - publish Will message when websocket is closed without 'DISCONNECT' packet + + 0.8.5-beta (2015-06-10) ------------------------- From d991e30135f4b9a912e90c04e0637b99a1184917 Mon Sep 17 00:00:00 2001 From: Feng Date: Wed, 17 Jun 2015 14:20:27 +0800 Subject: [PATCH 082/104] rel --- go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go b/go index ac0709fd1..7e7d9d1dd 100755 --- a/go +++ b/go @@ -2,4 +2,4 @@ # -*- tab-width:4;indent-tabs-mode:nil -*- # ex: ts=4 sw=4 et -make && make dist && cd rel/emqttd && ./bin/emqttd console +make && make rel && cd rel/emqttd && ./bin/emqttd console From 8702ab838c629089ab379add783e0f81ac49409d Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 22 Jun 2015 16:18:33 +0800 Subject: [PATCH 083/104] 0.9 project structure --- go | 5 - {apps/emqttd/include => include}/emqttd.hrl | 0 .../include => include}/emqttd_protocol.hrl | 0 plugins/emqttd_auth_mysql/.placehodler | 0 plugins/emqttd_auth_mysql/README.md | 48 -- plugins/emqttd_auth_mysql/etc/plugin.config | 17 - .../src/emqttd_auth_mysql.app.src | 12 - .../src/emqttd_auth_mysql.erl | 79 -- .../src/emqttd_auth_mysql_app.erl | 59 -- plugins/emysql/README.md | 42 - plugins/emysql/include/emysql.hrl | 2 - plugins/emysql/src/emysql.app.src | 14 - plugins/emysql/src/emysql.erl | 514 ------------ plugins/emysql/src/emysql_app.erl | 27 - plugins/emysql/src/emysql_auth.erl | 102 --- plugins/emysql/src/emysql_conn.erl | 739 ------------------ plugins/emysql/src/emysql_recv.erl | 130 --- plugins/emysql/src/emysql_sup.erl | 34 - {apps/emqttd/priv => priv}/www/index.html | 0 {apps/emqttd/priv => priv}/www/mqttws31.js | 0 {apps/emqttd/priv => priv}/www/websocket.html | 0 {apps/emqttd/src => src}/emqttd.app.src | 0 {apps/emqttd/src => src}/emqttd.erl | 0 .../src => src}/emqttd_access_control.erl | 0 .../emqttd/src => src}/emqttd_access_rule.erl | 0 .../src => src}/emqttd_acl_internal.erl | 0 {apps/emqttd/src => src}/emqttd_acl_mod.erl | 0 {apps/emqttd/src => src}/emqttd_alarm.erl | 0 {apps/emqttd/src => src}/emqttd_app.erl | 0 .../src => src}/emqttd_auth_anonymous.erl | 0 .../src => src}/emqttd_auth_clientid.erl | 0 {apps/emqttd/src => src}/emqttd_auth_ldap.erl | 0 {apps/emqttd/src => src}/emqttd_auth_mod.erl | 0 .../src => src}/emqttd_auth_username.erl | 0 {apps/emqttd/src => src}/emqttd_bridge.erl | 0 .../emqttd/src => src}/emqttd_bridge_sup.erl | 0 {apps/emqttd/src => src}/emqttd_broker.erl | 0 {apps/emqttd/src => src}/emqttd_client.erl | 0 {apps/emqttd/src => src}/emqttd_cm.erl | 0 {apps/emqttd/src => src}/emqttd_cm_sup.erl | 0 {apps/emqttd/src => src}/emqttd_ctl.erl | 0 {apps/emqttd/src => src}/emqttd_gen_mod.erl | 0 {apps/emqttd/src => src}/emqttd_http.erl | 0 {apps/emqttd/src => src}/emqttd_keepalive.erl | 0 {apps/emqttd/src => src}/emqttd_message.erl | 0 {apps/emqttd/src => src}/emqttd_metrics.erl | 0 {apps/emqttd/src => src}/emqttd_mnesia.erl | 0 .../emqttd/src => src}/emqttd_mod_autosub.erl | 0 .../src => src}/emqttd_mod_presence.erl | 0 .../emqttd/src => src}/emqttd_mod_rewrite.erl | 0 {apps/emqttd/src => src}/emqttd_mod_sup.erl | 0 {apps/emqttd/src => src}/emqttd_mqueue.erl | 0 {apps/emqttd/src => src}/emqttd_msg_store.erl | 0 {apps/emqttd/src => src}/emqttd_net.erl | 0 {apps/emqttd/src => src}/emqttd_opts.erl | 0 {apps/emqttd/src => src}/emqttd_packet.erl | 0 {apps/emqttd/src => src}/emqttd_parser.erl | 0 {apps/emqttd/src => src}/emqttd_pooler.erl | 0 .../emqttd/src => src}/emqttd_pooler_sup.erl | 0 {apps/emqttd/src => src}/emqttd_protocol.erl | 0 {apps/emqttd/src => src}/emqttd_pubsub.erl | 0 .../emqttd/src => src}/emqttd_pubsub_sup.erl | 0 .../emqttd/src => src}/emqttd_serialiser.erl | 0 {apps/emqttd/src => src}/emqttd_session.erl | 0 .../emqttd/src => src}/emqttd_session_sup.erl | 0 {apps/emqttd/src => src}/emqttd_sm.erl | 0 {apps/emqttd/src => src}/emqttd_sm_sup.erl | 0 {apps/emqttd/src => src}/emqttd_stats.erl | 0 {apps/emqttd/src => src}/emqttd_sup.erl | 0 {apps/emqttd/src => src}/emqttd_sysmon.erl | 0 {apps/emqttd/src => src}/emqttd_throttle.erl | 0 {apps/emqttd/src => src}/emqttd_topic.erl | 0 {apps/emqttd/src => src}/emqttd_trace.erl | 0 {apps/emqttd/src => src}/emqttd_trie.erl | 0 {apps/emqttd/src => src}/emqttd_util.erl | 0 {apps/emqttd/src => src}/emqttd_vm.erl | 0 {apps/emqttd/src => src}/emqttd_ws_client.erl | 0 .../emqttd_access_control_tests.erl | 0 .../emqttd_access_rule_tests.erl | 0 .../test => test}/emqttd_acl_test_mod.erl | 0 .../emqttd/test => test}/emqttd_acl_tests.erl | 0 .../emqttd_auth_anonymous_test_mod.erl | 0 .../test => test}/emqttd_opts_tests.erl | 0 .../test => test}/emqttd_parser_tests.erl | 0 .../test => test}/emqttd_serialiser_tests.erl | 0 .../test => test}/emqttd_topic_tests.erl | 0 {apps/emqttd/test => test}/esockd_access.erl | 0 {apps/emqttd/test => test}/test_acl.config | 0 88 files changed, 1824 deletions(-) delete mode 100755 go rename {apps/emqttd/include => include}/emqttd.hrl (100%) rename {apps/emqttd/include => include}/emqttd_protocol.hrl (100%) delete mode 100644 plugins/emqttd_auth_mysql/.placehodler delete mode 100644 plugins/emqttd_auth_mysql/README.md delete mode 100644 plugins/emqttd_auth_mysql/etc/plugin.config delete mode 100644 plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src delete mode 100644 plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl delete mode 100644 plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl delete mode 100644 plugins/emysql/README.md delete mode 100755 plugins/emysql/include/emysql.hrl delete mode 100644 plugins/emysql/src/emysql.app.src delete mode 100644 plugins/emysql/src/emysql.erl delete mode 100644 plugins/emysql/src/emysql_app.erl delete mode 100644 plugins/emysql/src/emysql_auth.erl delete mode 100644 plugins/emysql/src/emysql_conn.erl delete mode 100644 plugins/emysql/src/emysql_recv.erl delete mode 100644 plugins/emysql/src/emysql_sup.erl rename {apps/emqttd/priv => priv}/www/index.html (100%) rename {apps/emqttd/priv => priv}/www/mqttws31.js (100%) rename {apps/emqttd/priv => priv}/www/websocket.html (100%) rename {apps/emqttd/src => src}/emqttd.app.src (100%) rename {apps/emqttd/src => src}/emqttd.erl (100%) rename {apps/emqttd/src => src}/emqttd_access_control.erl (100%) rename {apps/emqttd/src => src}/emqttd_access_rule.erl (100%) rename {apps/emqttd/src => src}/emqttd_acl_internal.erl (100%) rename {apps/emqttd/src => src}/emqttd_acl_mod.erl (100%) rename {apps/emqttd/src => src}/emqttd_alarm.erl (100%) rename {apps/emqttd/src => src}/emqttd_app.erl (100%) rename {apps/emqttd/src => src}/emqttd_auth_anonymous.erl (100%) rename {apps/emqttd/src => src}/emqttd_auth_clientid.erl (100%) rename {apps/emqttd/src => src}/emqttd_auth_ldap.erl (100%) rename {apps/emqttd/src => src}/emqttd_auth_mod.erl (100%) rename {apps/emqttd/src => src}/emqttd_auth_username.erl (100%) rename {apps/emqttd/src => src}/emqttd_bridge.erl (100%) rename {apps/emqttd/src => src}/emqttd_bridge_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_broker.erl (100%) rename {apps/emqttd/src => src}/emqttd_client.erl (100%) rename {apps/emqttd/src => src}/emqttd_cm.erl (100%) rename {apps/emqttd/src => src}/emqttd_cm_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_ctl.erl (100%) rename {apps/emqttd/src => src}/emqttd_gen_mod.erl (100%) rename {apps/emqttd/src => src}/emqttd_http.erl (100%) rename {apps/emqttd/src => src}/emqttd_keepalive.erl (100%) rename {apps/emqttd/src => src}/emqttd_message.erl (100%) rename {apps/emqttd/src => src}/emqttd_metrics.erl (100%) rename {apps/emqttd/src => src}/emqttd_mnesia.erl (100%) rename {apps/emqttd/src => src}/emqttd_mod_autosub.erl (100%) rename {apps/emqttd/src => src}/emqttd_mod_presence.erl (100%) rename {apps/emqttd/src => src}/emqttd_mod_rewrite.erl (100%) rename {apps/emqttd/src => src}/emqttd_mod_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_mqueue.erl (100%) rename {apps/emqttd/src => src}/emqttd_msg_store.erl (100%) rename {apps/emqttd/src => src}/emqttd_net.erl (100%) rename {apps/emqttd/src => src}/emqttd_opts.erl (100%) rename {apps/emqttd/src => src}/emqttd_packet.erl (100%) rename {apps/emqttd/src => src}/emqttd_parser.erl (100%) rename {apps/emqttd/src => src}/emqttd_pooler.erl (100%) rename {apps/emqttd/src => src}/emqttd_pooler_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_protocol.erl (100%) rename {apps/emqttd/src => src}/emqttd_pubsub.erl (100%) rename {apps/emqttd/src => src}/emqttd_pubsub_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_serialiser.erl (100%) rename {apps/emqttd/src => src}/emqttd_session.erl (100%) rename {apps/emqttd/src => src}/emqttd_session_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_sm.erl (100%) rename {apps/emqttd/src => src}/emqttd_sm_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_stats.erl (100%) rename {apps/emqttd/src => src}/emqttd_sup.erl (100%) rename {apps/emqttd/src => src}/emqttd_sysmon.erl (100%) rename {apps/emqttd/src => src}/emqttd_throttle.erl (100%) rename {apps/emqttd/src => src}/emqttd_topic.erl (100%) rename {apps/emqttd/src => src}/emqttd_trace.erl (100%) rename {apps/emqttd/src => src}/emqttd_trie.erl (100%) rename {apps/emqttd/src => src}/emqttd_util.erl (100%) rename {apps/emqttd/src => src}/emqttd_vm.erl (100%) rename {apps/emqttd/src => src}/emqttd_ws_client.erl (100%) rename {apps/emqttd/test => test}/emqttd_access_control_tests.erl (100%) rename {apps/emqttd/test => test}/emqttd_access_rule_tests.erl (100%) rename {apps/emqttd/test => test}/emqttd_acl_test_mod.erl (100%) rename {apps/emqttd/test => test}/emqttd_acl_tests.erl (100%) rename {apps/emqttd/test => test}/emqttd_auth_anonymous_test_mod.erl (100%) rename {apps/emqttd/test => test}/emqttd_opts_tests.erl (100%) rename {apps/emqttd/test => test}/emqttd_parser_tests.erl (100%) rename {apps/emqttd/test => test}/emqttd_serialiser_tests.erl (100%) rename {apps/emqttd/test => test}/emqttd_topic_tests.erl (100%) rename {apps/emqttd/test => test}/esockd_access.erl (100%) rename {apps/emqttd/test => test}/test_acl.config (100%) diff --git a/go b/go deleted file mode 100755 index 7e7d9d1dd..000000000 --- a/go +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -make && make rel && cd rel/emqttd && ./bin/emqttd console diff --git a/apps/emqttd/include/emqttd.hrl b/include/emqttd.hrl similarity index 100% rename from apps/emqttd/include/emqttd.hrl rename to include/emqttd.hrl diff --git a/apps/emqttd/include/emqttd_protocol.hrl b/include/emqttd_protocol.hrl similarity index 100% rename from apps/emqttd/include/emqttd_protocol.hrl rename to include/emqttd_protocol.hrl diff --git a/plugins/emqttd_auth_mysql/.placehodler b/plugins/emqttd_auth_mysql/.placehodler deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_auth_mysql/README.md b/plugins/emqttd_auth_mysql/README.md deleted file mode 100644 index 02db17e10..000000000 --- a/plugins/emqttd_auth_mysql/README.md +++ /dev/null @@ -1,48 +0,0 @@ - -## Overview - -Authentication with user table of MySQL database. - -## etc/plugin.config - -``` - {emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, ""}, - {password, ""}, - {database, "mqtt"}, - {encoding, utf8} - ]}, - {emqttd_auth_mysql, [ - {user_table, mqtt_users}, - %% plain password only - {password_hash, plain}, - {field_mapper, [ - {username, username}, - {password, password} - ]} - ]} -``` - -## Users Table(Demo) - -Notice: This is a demo table. You could authenticate with any user tables. - -``` -CREATE TABLE `mqtt_users` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(60) DEFAULT NULL, - `password` varchar(60) DEFAULT NULL, - `salt` varchar(20) DEFAULT NULL, - `created` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `mqtt_users_username` (`username`) -) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -``` - -## Load Plugin - -Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded by the broker. - diff --git a/plugins/emqttd_auth_mysql/etc/plugin.config b/plugins/emqttd_auth_mysql/etc/plugin.config deleted file mode 100644 index 1ab1a6e07..000000000 --- a/plugins/emqttd_auth_mysql/etc/plugin.config +++ /dev/null @@ -1,17 +0,0 @@ -{emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, "root"}, - {password, "public"}, - {database, "mqtt"}, - {encoding, utf8} -]}, -{emqttd_auth_mysql, [ - {user_table, mqtt_users}, - {password_hash, plain}, - {field_mapper, [ - {username, username}, - {password, password} - ]} -]} diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src deleted file mode 100644 index 965e1825e..000000000 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_auth_mysql, - [ - {description, "emqttd MySQL Authentication Plugin"}, - {vsn, "1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {emqttd_auth_mysql_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl deleted file mode 100644 index be8685103..000000000 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl +++ /dev/null @@ -1,79 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd authentication by mysql 'user' table. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_mysql). - --author("Feng Lee "). - --include_lib("emqttd/include/emqttd.hrl"). - --behaviour(emqttd_auth_mod). - --export([init/1, check/3, description/0]). - --record(state, {user_table, name_field, pass_field, pass_hash}). - -init(Opts) -> - Mapper = proplists:get_value(field_mapper, Opts), - {ok, #state{user_table = proplists:get_value(user_table, Opts), - name_field = proplists:get_value(username, Mapper), - pass_field = proplists:get_value(password, Mapper), - pass_hash = proplists:get_value(password_hash, Opts)}}. - -check(#mqtt_client{username = undefined}, _Password, _State) -> - {error, "Username undefined"}; -check(#mqtt_client{username = <<>>}, _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{user_table = UserTab, pass_hash = Type, - name_field = NameField, pass_field = PassField}) -> - Where = {'and', {NameField, Username}, {PassField, hash(Type, Password)}}, - case emysql:select(UserTab, Where) of - {ok, []} -> {error, "Username or Password "}; - {ok, _Record} -> ok - end. - -description() -> "Authentication by MySQL". - -hash(plain, Password) -> - Password; - -hash(md5, Password) -> - hexstring(crypto:hash(md5, Password)); - -hash(sha, Password) -> - hexstring(crypto:hash(sha, Password)). - -hexstring(<>) -> - lists:flatten(io_lib:format("~32.16.0b", [X])); - -hexstring(<>) -> - lists:flatten(io_lib:format("~40.16.0b", [X])). - diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl deleted file mode 100644 index 11734fbad..000000000 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl +++ /dev/null @@ -1,59 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd mysql authentication app. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_mysql_app). - --behaviour(application). -%% Application callbacks --export([start/2, prep_stop/1, stop/1]). - --behaviour(supervisor). -%% Supervisor callbacks --export([init/1]). - -%%%============================================================================= -%%% Application callbacks -%%%============================================================================= - -start(_StartType, _StartArgs) -> - Env = application:get_all_env(), - ok = emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -prep_stop(State) -> - emqttd_access_control:unregister_mod(auth, emqttd_auth_mysql), State. - -stop(_State) -> - ok. - -%%%============================================================================= -%%% Supervisor callbacks(Dummy) -%%%============================================================================= - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - - diff --git a/plugins/emysql/README.md b/plugins/emysql/README.md deleted file mode 100644 index 18b61598c..000000000 --- a/plugins/emysql/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# emysql - -Erlang MySQL client - -## config - -``` - -``` - -## Select API - -* emyssql:select(tab). -* emysql:select({tab, [col1,col2]}). -* emysql:select({tab, [col1, col2], {id,1}}). -* emysql:select(Query, Load). - -## Update API - -* emysql:update(tab, [{Field1, Val}, {Field2, Val2}], {id, 1}). - -## Insert API - -* emysql:insert(tab, [{Field1, Val}, {Field2, Val2}]). - -## Delete API - -* emysql:delete(tab, {name, Name}]). - -## Query API - -* emysql:sqlquery("select * from tab;"). - -## Prepare API - -* emysql:prepare(find_with_id, "select * from tab where id = ?;"). -* emysql:execute(find_with_id, [Id]). -* emysql:unprepare(find_with_id). - -## MySQL Client Protocal - -* http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol diff --git a/plugins/emysql/include/emysql.hrl b/plugins/emysql/include/emysql.hrl deleted file mode 100755 index fde65fe7a..000000000 --- a/plugins/emysql/include/emysql.hrl +++ /dev/null @@ -1,2 +0,0 @@ -%% MySQL result record: --record(mysql_result, {fieldinfo = [], rows = [], affectedrows = 0, insert_id =0, error = ""}). diff --git a/plugins/emysql/src/emysql.app.src b/plugins/emysql/src/emysql.app.src deleted file mode 100644 index d494fd954..000000000 --- a/plugins/emysql/src/emysql.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emysql, - [{description, "Erlang MySQL Driver"}, - {vsn, "1.0"}, - {modules, [ - emysql, - emysql_app, - emysql_sup, - emysql_auth, - emysql_conn, - emysql_recv]}, - {registered, []}, - {applications, [kernel, stdlib, sasl, crypto]}, - {env, []}, - {mod, {emysql_app, []}}]}. diff --git a/plugins/emysql/src/emysql.erl b/plugins/emysql/src/emysql.erl deleted file mode 100644 index f6a620cfd..000000000 --- a/plugins/emysql/src/emysql.erl +++ /dev/null @@ -1,514 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : emysql.erl -%%% Author : Ery Lee -%%% Purpose : Mysql access api. -%%% Created : 19 May 2009 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2012, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql). - --author('ery.lee@gmail.com'). - --include("emysql.hrl"). - --export([start_link/1]). - --ifdef(use_specs). - --spec(conns/0 :: () -> list()). - --endif. - -%command functions --export([info/0, - pool/1, - conns/0]). - -%sql functions --export([insert/2, - insert/3, - select/1, - select/2, - select/3, - update/2, - update/3, - delete/1, - delete/2, - truncate/1, - prepare/2, - execute/1, - execute/2, - unprepare/1, - sqlquery/1, - sqlquery/2]). - --behavior(gen_server). - --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - --record(state, {ids}). - -%% External exports --export([encode/1, - encode/2, - escape/1, - escape_like/1]). - -start_link(PoolSize) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [PoolSize], []). - -info() -> - [emysql_conn:info(Pid) || Pid <- - pg2:get_local_members(emysql_conn)]. - -%pool pool -pool(Id) -> - gen_server:cast(?MODULE, {pool, Id}). - -conns() -> - gen_server:call(?MODULE, conns). - -insert(Tab, Record) when is_atom(Tab) -> - sqlquery(encode_insert(Tab, Record)). - -insert(_Tab, _Fields, Values) when length(Values) == 0 -> - {updated, {0, 0}}; - -insert(Tab, Fields, Values) when length(Values) > 0 -> - sqlquery(encode_insert(Tab, Fields, Values)). - -encode_insert(Tab, Record) -> - {Fields, Values} = lists:unzip([{atom_to_list(F), encode(V)} - || {F, V} <- Record]), - ["insert into ", atom_to_list(Tab), "(", - string:join(Fields, ","), ") values(", - string:join(Values, ","), ");"]. - -encode_insert(Tab, Fields, Rows) -> - Encode = fun(Row) -> string:join([encode(V) || V <- Row], ",") end, - Rows1 = [lists:concat(["(", Encode(Row), ")"]) || Row <- Rows], - ["insert into ", atom_to_list(Tab), "(", - string:join([atom_to_list(F) || F <- Fields], ","), - ") values", string:join(Rows1, ","), ";"]. - -select(Tab) when is_atom(Tab) -> - sqlquery(encode_select(Tab)); - -select(Select) when is_tuple(Select) -> - sqlquery(encode_select(Select)). - -select(Tab, Where) when is_atom(Tab) and is_tuple(Where) -> - sqlquery(encode_select({Tab, Where})); - -select(Tab, Fields) when is_atom(Tab) and is_list(Fields) -> - sqlquery(encode_select({Tab, Fields})); - -select(Select, Load) when is_tuple(Select) and is_integer(Load) -> - sqlquery(encode_select(Select), Load). - -select(Tab, Fields, Where) when is_atom(Tab) - and is_list(Fields) and is_tuple(Where) -> - sqlquery(encode_select({Tab, Fields, Where})). - -encode_select(Tab) when is_atom(Tab) -> - encode_select({Tab, ['*'], undefined}); - -encode_select({Tab, Fields}) when is_atom(Tab) - and is_list(Fields) -> - encode_select({Tab, Fields, undefined}); - -encode_select({Tab, Where}) when is_atom(Tab) - and is_tuple(Where) -> - encode_select({Tab, ['*'], Where}); - -encode_select({Tab, Fields, undefined}) when is_atom(Tab) - and is_list(Fields) -> - ["select ", encode_fields(Fields), " from ", atom_to_list(Tab), ";"]; - -encode_select({Tab, Fields, Where}) when is_atom(Tab) - and is_list(Fields) and is_tuple(Where) -> - ["select ", encode_fields(Fields), " from ", - atom_to_list(Tab), " where ", encode_where(Where), ";"]. - -encode_fields(Fields) -> - string:join([atom_to_list(F) || F <- Fields], " ,"). - -update(Tab, Record) when is_atom(Tab) - and is_list(Record) -> - case proplists:get_value(id, Record) of - undefined -> - Updates = string:join([encode_column(Col) || Col <- Record], ","), - Query = ["update ", atom_to_list(Tab), " set ", Updates, ";"], - sqlquery(Query); - Id -> - update(Tab, lists:keydelete(id, 1, Record), {id, Id}) - end. - -update(Tab, Record, Where) -> - Update = string:join([encode_column(Col) || Col <- Record], ","), - Query = ["update ", atom_to_list(Tab), " set ", Update, - " where ", encode_where(Where), ";"], - sqlquery(Query). - -encode_column({F, V}) when is_atom(F) -> - lists:concat([atom_to_list(F), "=", encode(V)]). - -delete(Tab) when is_atom(Tab) -> - sqlquery(["delete from ", atom_to_list(Tab), ";"]). - -delete(Tab, Id) when is_atom(Tab) - and is_integer(Id) -> - Query = ["delete from ", atom_to_list(Tab), - " where ", encode_where({id, Id})], - sqlquery(Query); - -delete(Tab, Where) when is_atom(Tab) - and is_tuple(Where) -> - Query = ["delete from ", atom_to_list(Tab), - " where ", encode_where(Where)], - sqlquery(Query). - -truncate(Tab) when is_atom(Tab) -> - sqlquery(["truncate table ", atom_to_list(Tab), ";"]). - -sqlquery(Query) -> - sqlquery(Query, 1). - -sqlquery(Query, Load) -> - with_next_conn(fun(Conn) -> - case catch mysql_to_odbc(emysql_conn:sqlquery(Conn, iolist_to_binary(Query))) of - {selected, NewFields, Records} -> - {ok, to_tuple_records(NewFields, Records)}; - {error, Reason} -> - {error, Reason}; - Res -> - Res - end - end, Load). - -prepare(Name, Stmt) when is_list(Stmt) -> - prepare(Name, list_to_binary(Stmt)); - -prepare(Name, Stmt) when is_binary(Stmt) -> - with_all_conns(fun(Conn) -> - emysql_conn:prepare(Conn, Name, Stmt) - end). - -execute(Name) -> - execute(Name, []). - -execute(Name, Params) -> - with_next_conn(fun(Conn) -> - case catch mysql_to_odbc(emysql_conn:execute(Conn, Name, Params)) of - {selected, NewFields, Records} -> - {ok, to_tuple_records(NewFields, Records)}; - {error, Reason} -> - {error, Reason}; - Res -> - Res - end - end, 1). - -unprepare(Name) -> - with_all_conns(fun(Conn) -> - emysql_conn:unprepare(Conn, Name) - end). - -with_next_conn(Fun, _Load) -> - Fun(pg2:get_closest_pid(emysql_conn)). - -with_all_conns(Fun) -> - [Fun(Pid) || Pid <- pg2:get_local_members(emysql_conn)]. - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([PoolSize]) -> - Ids = lists:seq(1, PoolSize), - [put(Id, 0) || Id <- Ids], - [put({count, Id}, 0) || Id <- Ids], - {ok, #state{ids = Ids}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- - -handle_call(info, _From, State) -> - Reply = [{conn, Id, Pid, get(Id), get({total, Id})} - || {Id, Pid} <- get_all_conns()], - {reply, Reply, State}; - -handle_call({next_conn, Load}, _From, #state{ids = Ids} = State) -> - {ConnId, ConnLoad} = - lists:foldl(fun(Id, {MinId, MinLoad}) -> - ThisLoad = get(Id), - if - ThisLoad =< MinLoad -> {Id, ThisLoad}; - true -> {MinId, MinLoad} - end - end, {undefined, 16#ffffffff}, Ids), - Reply = - case ConnId of - undefined -> - undefined; - _ -> - ConnPid = get_conn_pid(ConnId), - put(ConnId, ConnLoad+Load), - Count = get({total, ConnId}), - put({total, ConnId}, Count+1), - {ConnId, ConnPid} - end, - {reply, Reply, State}; - -handle_call(conns, _From, State) -> - Conns = get_all_conns(), - {reply, Conns, State}; - -handle_call(Req, From, State) -> - gen_server:reply(From, {badcall, Req}), - {stop, {badcall, Req}, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast({pool, Id}, State) -> - put(Id, 0), - put({total, Id}, 0), - {noreply, State}; - -handle_cast({done, ConnId, Load}, State) -> - put(ConnId, get(ConnId) - Load), - {noreply, State}; - -handle_cast(Msg, State) -> - {stop, {badcast, Msg}, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info(Info, State) -> - {stop, {badinfo, Info}, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any necessary -%% cleaning up. When it returns, the gen_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -get_conn_pid(CId) -> - [{CId, Pid, _Type, _Modules} | _] = - lists:dropwhile(fun ({Id, _Pid, _Type, _Modules}) - when Id =:= CId -> false; - (_) -> true - end, - supervisor:which_children(emysql_sup)), - Pid. - -get_all_conns() -> - [{Id, Pid} || {Id, Pid, _Type, _Modules} <- - supervisor:which_children(emysql_sup), is_integer(Id)]. - -%% Convert MySQL query result to Erlang ODBC result formalism -mysql_to_odbc({updated, #mysql_result{affectedrows=AffectedRows, insert_id = InsertId} = _MySQLRes}) -> - {updated, {AffectedRows, InsertId}}; - -mysql_to_odbc({data, #mysql_result{fieldinfo = FieldInfo, rows=AllRows} = _MySQLRes}) -> - mysql_item_to_odbc(FieldInfo, AllRows); - -mysql_to_odbc({error, MySQLRes}) when is_list(MySQLRes) -> - {error, MySQLRes}; - -mysql_to_odbc({error, #mysql_result{error=Reason} = _MySQLRes}) -> - {error, Reason}; - -mysql_to_odbc({error, Reason}) -> - {error, Reason}. - -%% When tabular data is returned, convert it to the ODBC formalism -mysql_item_to_odbc(Columns, Recs) -> - %% For now, there is a bug and we do not get the correct value from MySQL - %% module: - {selected, - [element(2, Column) || Column <- Columns], - [list_to_tuple(Rec) || Rec <- Recs]}. - -%%internal functions -encode_where({'and', L, R}) -> - encode_where(L) ++ " and " ++ encode_where(R); - -encode_where({'and', List}) when is_list(List) -> - string:join([encode_where(E) || E <- List], " and "); - -encode_where({'or', L, R}) -> - encode_where(L) ++ " or " ++ encode_where(R); - -encode_where({'or', List}) when is_list(List) -> - string:join([encode_where(E) || E <- List], " or "); - -encode_where({like, Field, Value}) -> - atom_to_list(Field) ++ " like " ++ encode(Value); - -encode_where({'<', Field, Value}) -> - atom_to_list(Field) ++ " < " ++ encode(Value); - -encode_where({'<=', Field, Value}) -> - atom_to_list(Field) ++ " <= " ++ encode(Value); - -encode_where({'>', Field, Value}) -> - atom_to_list(Field) ++ " > " ++ encode(Value); - -encode_where({'>=', Field, Value}) -> - atom_to_list(Field) ++ " >= " ++ encode(Value); - -encode_where({'in', Field, Values}) -> - InStr = string:join([encode(Value) || Value <- Values], ","), - atom_to_list(Field) ++ " in (" ++ InStr ++ ")"; - -encode_where({Field, Value}) -> - atom_to_list(Field) ++ " = " ++ encode(Value). - -to_tuple_records(_Fields, []) -> - []; - -to_tuple_records(Fields, Records) -> - [to_tuple_record(Fields, tuple_to_list(Record)) || Record <- Records]. - -to_tuple_record(Fields, Record) when length(Fields) == length(Record) -> - to_tuple_record(Fields, Record, []). - -to_tuple_record([], [], Acc) -> - Acc; - -to_tuple_record([_F|FT], [undefined|VT], Acc) -> - to_tuple_record(FT, VT, Acc); - -to_tuple_record([F|FT], [V|VT], Acc) -> - to_tuple_record(FT, VT, [{list_to_atom(binary_to_list(F)), V} | Acc]). - -%% Escape character that will confuse an SQL engine -%% Percent and underscore only need to be escaped for pattern matching like -%% statement -escape_like(S) when is_list(S) -> - [escape_like(C) || C <- S]; -escape_like($%) -> "\\%"; -escape_like($_) -> "\\_"; -escape_like(C) -> escape(C). - -%% Escape character that will confuse an SQL engine -escape(S) when is_list(S) -> - [escape(C) || C <- S]; -%% Characters to escape -escape($\0) -> "\\0"; -escape($\n) -> "\\n"; -escape($\t) -> "\\t"; -escape($\b) -> "\\b"; -escape($\r) -> "\\r"; -escape($') -> "\\'"; -escape($") -> "\\\""; -escape($\\) -> "\\\\"; -escape(C) -> C. - -encode(Val) -> - encode(Val, false). -encode(Val, false) when Val == undefined; Val == null -> - "NULL"; -encode(Val, true) when Val == undefined; Val == null -> - <<"NULL">>; -encode(Val, false) when is_binary(Val) -> - binary_to_list(quote(Val)); -encode(Val, true) when is_binary(Val) -> - quote(Val); -encode(Val, true) -> - list_to_binary(encode(Val,false)); -encode(Val, false) when is_atom(Val) -> - quote(atom_to_list(Val)); -encode(Val, false) when is_list(Val) -> - quote(Val); -encode(Val, false) when is_integer(Val) -> - integer_to_list(Val); -encode(Val, false) when is_float(Val) -> - [Res] = io_lib:format("~w", [Val]), - Res; -encode({datetime, Val}, AsBinary) -> - encode(Val, AsBinary); -encode({{Year, Month, Day}, {Hour, Minute, Second}}, false) -> - Res = two_digits([Year, Month, Day, Hour, Minute, Second]), - lists:flatten(Res); -encode({TimeType, Val}, AsBinary) - when TimeType == 'date'; - TimeType == 'time' -> - encode(Val, AsBinary); -encode({Time1, Time2, Time3}, false) -> - Res = two_digits([Time1, Time2, Time3]), - lists:flatten(Res); -encode(Val, _AsBinary) -> - {error, {unrecognized_value, Val}}. - -two_digits(Nums) when is_list(Nums) -> - [two_digits(Num) || Num <- Nums]; -two_digits(Num) -> - [Str] = io_lib:format("~b", [Num]), - case length(Str) of - 1 -> [$0 | Str]; - _ -> Str - end. - -%% Quote a string or binary value so that it can be included safely in a -%% MySQL query. -quote(String) when is_list(String) -> - [39 | lists:reverse([39 | quote(String, [])])]; %% 39 is $' -quote(Bin) when is_binary(Bin) -> - list_to_binary(quote(binary_to_list(Bin))). - -quote([], Acc) -> - Acc; -quote([0 | Rest], Acc) -> - quote(Rest, [$0, $\\ | Acc]); -quote([10 | Rest], Acc) -> - quote(Rest, [$n, $\\ | Acc]); -quote([13 | Rest], Acc) -> - quote(Rest, [$r, $\\ | Acc]); -quote([$\\ | Rest], Acc) -> - quote(Rest, [$\\ , $\\ | Acc]); -quote([39 | Rest], Acc) -> %% 39 is $' - quote(Rest, [39, $\\ | Acc]); %% 39 is $' -quote([34 | Rest], Acc) -> %% 34 is $" - quote(Rest, [34, $\\ | Acc]); %% 34 is $" -quote([26 | Rest], Acc) -> - quote(Rest, [$Z, $\\ | Acc]); -quote([C | Rest], Acc) -> - quote(Rest, [C | Acc]). - diff --git a/plugins/emysql/src/emysql_app.erl b/plugins/emysql/src/emysql_app.erl deleted file mode 100644 index 4be2e70db..000000000 --- a/plugins/emysql/src/emysql_app.erl +++ /dev/null @@ -1,27 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : emysql_app.erl -%%% Author : Ery Lee -%%% Purpose : mysql driver application -%%% Created : 21 May 2009 -%%% Updated : 11 Jan 2010 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2007-2010, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql_app). - --author('ery.lee@gmail.com'). - --behavior(application). - --export([start/0, start/2, stop/1]). - -start() -> - application:start(emysql). - -start(normal, _Args) -> - emysql_sup:start_link(application:get_all_env()). - -stop(_) -> - ok. - diff --git a/plugins/emysql/src/emysql_auth.erl b/plugins/emysql/src/emysql_auth.erl deleted file mode 100644 index 72aebfd3b..000000000 --- a/plugins/emysql/src/emysql_auth.erl +++ /dev/null @@ -1,102 +0,0 @@ --module(emysql_auth). - --export([make_auth/2, make_new_auth/3, password_old/2, password_new/2]). - -%%-------------------------------------------------------------------- -%% Macros -%%-------------------------------------------------------------------- --define(LONG_PASSWORD, 1). --define(LONG_FLAG, 4). --define(PROTOCOL_41, 512). --define(TRANSACTIONS, 8192). --define(SECURE_CONNECTION, 32768). --define(CONNECT_WITH_DB, 8). --define(MAX_PACKET_SIZE, 1000000). - -password_old(Password, Salt) -> - {P1, P2} = hash(Password), - {S1, S2} = hash(Salt), - Seed1 = P1 bxor S1, - Seed2 = P2 bxor S2, - List = rnd(9, Seed1, Seed2), - {L, [Extra]} = lists:split(8, List), - list_to_binary(lists:map(fun (E) -> E bxor (Extra - 64) end, L)). - -%% part of do_old_auth/4, which is part of mysql_init/4 -make_auth(User, Password) -> - Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS, - Maxsize = 0, - UserB = list_to_binary(User), - PasswordB = Password, - <>. - -%% part of do_new_auth/4, which is part of mysql_init/4 -make_new_auth(User, Password, Database) -> - DBCaps = case Database of - none -> - 0; - _ -> - ?CONNECT_WITH_DB - end, - Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS bor - ?PROTOCOL_41 bor ?SECURE_CONNECTION bor DBCaps, - Maxsize = ?MAX_PACKET_SIZE, - UserB = list_to_binary(User), - PasswordL = size(Password), - DatabaseB = case Database of - none -> - <<>>; - _ -> - list_to_binary(Database) - end, - <>. - -hash(S) -> - hash(S, 1345345333, 305419889, 7). - -hash([C | S], N1, N2, Add) -> - N1_1 = N1 bxor (((N1 band 63) + Add) * C + N1 * 256), - N2_1 = N2 + ((N2 * 256) bxor N1_1), - Add_1 = Add + C, - hash(S, N1_1, N2_1, Add_1); -hash([], N1, N2, _Add) -> - Mask = (1 bsl 31) - 1, - {N1 band Mask , N2 band Mask}. - -rnd(N, Seed1, Seed2) -> - Mod = (1 bsl 30) - 1, - rnd(N, [], Seed1 rem Mod, Seed2 rem Mod). - -rnd(0, List, _, _) -> - lists:reverse(List); -rnd(N, List, Seed1, Seed2) -> - Mod = (1 bsl 30) - 1, - NSeed1 = (Seed1 * 3 + Seed2) rem Mod, - NSeed2 = (NSeed1 + Seed2 + 33) rem Mod, - Float = (float(NSeed1) / float(Mod))*31, - Val = trunc(Float)+64, - rnd(N - 1, [Val | List], NSeed1, NSeed2). - - -dualmap(_F, [], []) -> - []; -dualmap(F, [E1 | R1], [E2 | R2]) -> - [F(E1, E2) | dualmap(F, R1, R2)]. - -bxor_binary(B1, B2) -> - list_to_binary(dualmap(fun (E1, E2) -> - E1 bxor E2 - end, binary_to_list(B1), binary_to_list(B2))). - -password_new(Password, Salt) -> - Stage1 = crypto:sha(Password), - Stage2 = crypto:sha(Stage1), - Res = crypto:sha_final( - crypto:sha_update( - crypto:sha_update(crypto:sha_init(), Salt), - Stage2) - ), - bxor_binary(Res, Stage1). - diff --git a/plugins/emysql/src/emysql_conn.erl b/plugins/emysql/src/emysql_conn.erl deleted file mode 100644 index 124e23a9d..000000000 --- a/plugins/emysql/src/emysql_conn.erl +++ /dev/null @@ -1,739 +0,0 @@ -%%% File : emysql_conn.erl -%%% Author : Ery Lee -%%% Purpose : connection of mysql driver -%%% Created : 11 Jan 2010 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2012, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql_conn). - --include("emysql.hrl"). - --import(proplists, [get_value/2, get_value/3]). - --behaviour(gen_server). - -%% External exports --export([start_link/2, - info/1, - sqlquery/2, - sqlquery/3, - prepare/3, - execute/3, - execute/4, - unprepare/2]). - -%% Callback --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - --record(state, { - id, - host, - port, - user, - password, - database, - encoding, - mysql_version, - recv_pid, - socket, - data}). - -%%-define(KEEPALIVE_QUERY, <<"SELECT 1;">>). - --define(SECURE_CONNECTION, 32768). - --define(MYSQL_QUERY_OP, 3). - -%CALL > CONNECT --define(CALL_TIMEOUT, 301000). - --define(CONNECT_TIMEOUT, 300000). - --define(MYSQL_4_0, 40). %% Support for MySQL 4.0.x - --define(MYSQL_4_1, 41). %% Support for MySQL 4.1.x et 5.0.x - -%%-------------------------------------------------------------------- -%% Function: start(Opts) -%% Descrip.: Starts a mysql_conn process that connects to a MySQL -%% server, logs in and chooses a database. -%% Returns : {ok, Pid} | {error, Reason} -%% Pid = pid() -%% Reason = string() -%%-------------------------------------------------------------------- -start_link(Id, Opts) -> - gen_server:start_link(?MODULE, [Id, Opts], []). - -info(Conn) -> - gen_server:call(Conn, info). - -%%-------------------------------------------------------------------- -%% Function: sqlquery(Query) -%% Queries = A single binary() query or a list of binary() queries. -%% If a list is provided, the return value is the return -%% of the last query, or the first query that has -%% returned an error. If an error occurs, execution of -%% the following queries is aborted. -%% From = pid() or term(), use a From of self() when -%% using this module for a single connection, -%% or pass the gen_server:call/3 From argument if -%% using a gen_server to do the querys (e.g. the -%% mysql_dispatcher) -%% Timeout = integer() | infinity, gen_server timeout value -%% Descrip.: Send a query or a list of queries and wait for the result -%% if running stand-alone (From = self()), but don't block -%% the caller if we are not running stand-alone -%% (From = gen_server From). -%% Returns : ok | (non-stand-alone mode) -%% {data, #mysql_result} | (stand-alone mode) -%% {updated, #mysql_result} | (stand-alone mode) -%% {error, #mysql_result} (stand-alone mode) -%% FieldInfo = term() -%% Rows = list() of [string()] -%% Reason = term() -%%-------------------------------------------------------------------- -sqlquery(Conn, Query) -> - sqlquery(Conn, Query, ?CALL_TIMEOUT). - -sqlquery(Conn, Query, Timeout) -> - call(Conn, {sqlquery, Query}, Timeout). - -prepare(Conn, Name, Stmt) -> - call(Conn, {prepare, Name, Stmt}). - -execute(Conn, Name, Params) -> - execute(Conn, Name, Params, ?CALL_TIMEOUT). - -execute(Conn, Name, Params, Timeout) -> - call(Conn, {execute, Name, Params}, Timeout). - -unprepare(Conn, Name) -> - call(Conn, {unprepare, Name}). - -%%-------------------------------------------------------------------- -%% Function: init(Host, Port, User, Password, Database, Parent) -%% Host = string() -%% Port = integer() -%% User = string() -%% Password = string() -%% Database = string() -%% Parent = pid() of process starting this mysql_conn -%% Descrip.: Connect to a MySQL server, log in and chooses a database. -%% Report result of this to Parent, and then enter loop() if -%% we were successfull. -%% Returns : void() | does not return -%%-------------------------------------------------------------------- -init([Id, Opts]) -> - put(queries, 0), - Host = get_value(host, Opts, "localhost"), - Port = get_value(port, Opts, 3306), - UserName = get_value(username, Opts, "root"), - Password = get_value(password, Opts, "public"), - Database = get_value(database, Opts), - Encoding = get_value(encoding, Opts, utf8), - case emysql_recv:start_link(Host, Port) of - {ok, RecvPid, Sock} -> - case mysql_init(Sock, RecvPid, UserName, Password) of - {ok, Version} -> - Db = iolist_to_binary(Database), - case do_query(Sock, RecvPid, <<"use ", Db/binary>>, Version) of - {error, #mysql_result{error = Error} = _MySQLRes} -> - error_logger:error_msg("emysql_conn: use '~p' error: ~p", [Database, Error]), - {stop, using_db_error}; - {_ResultType, _MySQLRes} -> - emysql:pool(Id), %pool it - pg2:create(emysql_conn), - pg2:join(emysql_conn, self()), - EncodingBinary = list_to_binary(atom_to_list(Encoding)), - do_query(Sock, RecvPid, <<"set names '", EncodingBinary/binary, "'">>, Version), - State = #state{ - id = Id, - host = Host, - port = Port, - user = UserName, - password = Password, - database = Database, - encoding = Encoding, - mysql_version = Version, - recv_pid = RecvPid, - socket = Sock, - data = <<>>}, - {ok, State} - end; - {error, Reason} -> - {stop, {login_failed, Reason}} - end; - {error, Reason} -> - {stop, Reason} - end. - -handle_call(info, _From, #state{id = Id} = State) -> - Reply = {Id, self(), get(queries)}, - {reply, Reply, State}; - -handle_call({sqlquery, Query}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - put(queries, get(queries) + 1), - case do_query(Socket, RecvPid, Query, Ver) of - {error, mysql_timeout} = Err -> - {stop, mysql_timeout, Err, State}; - Res -> - {reply, Res, State} - end; - -handle_call({prepare, Name, Stmt}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - - case do_prepare(Socket, RecvPid, Name, Stmt, Ver) of - {error, mysql_timeout} -> - {stop, mysql_timeout, State}; - _ -> - {reply, ok, State} - end; - -handle_call({unprepare, Name}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - case do_unprepare(Socket, RecvPid, Name, Ver) of - {error, mysql_timeout} -> - {stop, mysql_timeout, State}; - _ -> - {reply, ok, State} - end; - -handle_call({execute, Name, Params}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - case do_execute(Socket, RecvPid, Name, Params, Ver) of - {error, mysql_timeout} = Err -> - {stop, mysql_timeout, Err, State}; - Res -> - {reply, Res, State} - end; - -handle_call(Req, _From, State) -> - error_logger:error_msg("badreq to emysql_conn: ~p", [Req]), - {reply, {error, badreq}, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({mysql_recv, _RecvPid, data, _Packet, SeqNum}, State) -> - error_logger:error_msg("unexpected mysql_recv: seq_num = ~p", [SeqNum]), - {noreply, State}; - -handle_info({mysql_recv, _RecvPid, closed, E}, State) -> - error_logger:error_msg("mysql socket closed: ~p", [E]), - {stop, socket_closed, State}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -do_queries(Sock, RecvPid, Queries, Version) -> - catch - lists:foldl( - fun(Query, _LastResponse) -> - case do_query(Sock, RecvPid, Query, Version) of - {error, _} = Err -> throw(Err); - Res -> Res - end - end, ok, Queries). - -do_query(Sock, RecvPid, Query, Version) -> - Query1 = iolist_to_binary(Query), - %?DEBUG("sqlquery ~p (id ~p)", [Query1, RecvPid]), - Packet = <>, - case do_send(Sock, Packet, 0) of - ok -> - get_query_response(RecvPid, Version); - {error, Reason} -> - {error, Reason} - end. - -do_prepare(Socket, RecvPid, Name, Stmt, Ver) -> - NameBin = atom_to_binary(Name), - StmtBin = <<"PREPARE ", NameBin/binary, " FROM '", Stmt/binary, "'">>, - do_query(Socket, RecvPid, StmtBin, Ver). - -do_execute(Socket, RecvPid, Name, Params, Ver) -> - Stmts = make_statements(Name, Params), - do_queries(Socket, RecvPid, Stmts, Ver). - -do_unprepare(Socket, RecvPid, Name, Ver) -> - NameBin = atom_to_binary(Name), - StmtBin = <<"UNPREPARE ", NameBin/binary>>, - do_query(Socket, RecvPid, StmtBin, Ver). - -make_statements(Name, []) -> - NameBin = atom_to_binary(Name), - [<<"EXECUTE ", NameBin/binary>>]; - -make_statements(Name, Params) -> - NumParams = length(Params), - ParamNums = lists:seq(1, NumParams), - NameBin = atom_to_binary(Name), - ParamNames = - lists:foldl( - fun(Num, Acc) -> - ParamName = [$@ | integer_to_list(Num)], - if Num == 1 -> - ParamName ++ Acc; - true -> - [$, | ParamName] ++ Acc - end - end, [], lists:reverse(ParamNums)), - ParamNamesBin = list_to_binary(ParamNames), - ExecStmt = <<"EXECUTE ", NameBin/binary, " USING ", - ParamNamesBin/binary>>, - - ParamVals = lists:zip(ParamNums, Params), - Stmts = lists:foldl( - fun({Num, Val}, Acc) -> - NumBin = emysql:encode(Num, true), - ValBin = emysql:encode(Val, true), - [<<"SET @", NumBin/binary, "=", ValBin/binary>> | Acc] - end, [ExecStmt], lists:reverse(ParamVals)), - Stmts. - -atom_to_binary(Val) -> - <<_:4/binary, Bin/binary>> = term_to_binary(Val), - Bin. - -%%-------------------------------------------------------------------- -%% authentication -%%-------------------------------------------------------------------- -do_old_auth(Sock, RecvPid, SeqNum, User, Password, Salt1) -> - Auth = emysql_auth:password_old(Password, Salt1), - Packet = emysql_auth:make_auth(User, Auth), - do_send(Sock, Packet, SeqNum), - do_recv(RecvPid, SeqNum). - -do_new_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, Salt2) -> - Auth = emysql_auth:password_new(Password, Salt1 ++ Salt2), - Packet2 = emysql_auth:make_new_auth(User, Auth, none), - do_send(Sock, Packet2, SeqNum), - case do_recv(RecvPid, SeqNum) of - {ok, Packet3, SeqNum2} -> - case Packet3 of - <<254:8>> -> - AuthOld = emysql_auth:password_old(Password, Salt1), - do_send(Sock, <>, SeqNum2 + 1), - do_recv(RecvPid, SeqNum2 + 1); - _ -> - {ok, Packet3, SeqNum2} - end; - {error, Reason} -> - {error, Reason} - end. - -%%-------------------------------------------------------------------- -%% Function: mysql_init(Sock, RecvPid, User, Password) -%% Sock = term(), gen_tcp socket -%% RecvPid = pid(), mysql_recv process -%% User = string() -%% Password = string() -%% LogFun = undefined | function() with arity 3 -%% Descrip.: Try to authenticate on our new socket. -%% Returns : ok | {error, Reason} -%% Reason = string() -%%-------------------------------------------------------------------- -mysql_init(Sock, RecvPid, User, Password) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, InitSeqNum} -> - {Version, Salt1, Salt2, Caps} = greeting(Packet), - %?DEBUG("version: ~p, ~p, ~p, ~p", [Version, Salt1, Salt2, Caps]), - AuthRes = - case Caps band ?SECURE_CONNECTION of - ?SECURE_CONNECTION -> - do_new_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1, Salt2); - _ -> - do_old_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1) - end, - case AuthRes of - {ok, <<0:8, _Rest/binary>>, _RecvNum} -> - {ok,Version}; - {ok, <<255:8, _Code:16/little, Message/binary>>, _RecvNum} -> - {error, binary_to_list(Message)}; - {ok, RecvPacket, _RecvNum} -> - {error, binary_to_list(RecvPacket)}; - {error, Reason} -> - %?ERROR("init failed receiving data : ~p", [Reason]), - {error, Reason} - end; - {error, Reason} -> - {error, Reason} - end. - -greeting(Packet) -> - <<_Protocol:8, Rest/binary>> = Packet, - {Version, Rest2} = asciz(Rest), - <<_TreadID:32/little, Rest3/binary>> = Rest2, - {Salt, Rest4} = asciz(Rest3), - <> = Rest4, - <<_ServerChar:16/binary-unit:8, Rest6/binary>> = Rest5, - {Salt2, _Rest7} = asciz(Rest6), - %?DEBUG("greeting version ~p (protocol ~p) salt ~p caps ~p serverchar ~p" - %"salt2 ~p", - %[Version, Protocol, Salt, Caps, ServerChar, Salt2]), - {normalize_version(Version), Salt, Salt2, Caps}. - -%% part of greeting/2 -asciz(Data) when is_binary(Data) -> - asciz_binary(Data, []); -asciz(Data) when is_list(Data) -> - {String, [0 | Rest]} = lists:splitwith(fun (C) -> - C /= 0 - end, Data), - {String, Rest}. - -%% @doc Find the first zero-byte in Data and add everything before it -%% to Acc, as a string. -%% -%% @spec asciz_binary(Data::binary(), Acc::list()) -> -%% {NewList::list(), Rest::binary()} -asciz_binary(<<>>, Acc) -> - {lists:reverse(Acc), <<>>}; -asciz_binary(<<0:8, Rest/binary>>, Acc) -> - {lists:reverse(Acc), Rest}; -asciz_binary(<>, Acc) -> - asciz_binary(Rest, [C | Acc]). - -%%-------------------------------------------------------------------- -%% Function: get_query_response(RecvPid) -%% RecvPid = pid(), mysql_recv process -%% Version = integer(), Representing MySQL version used -%% Descrip.: Wait for frames until we have a complete query response. -%% Returns : {data, #mysql_result} -%% {updated, #mysql_result} -%% {error, #mysql_result} -%% FieldInfo = list() of term() -%% Rows = list() of [string()] -%% AffectedRows = int() -%% Reason = term() -%%-------------------------------------------------------------------- -get_query_response(RecvPid, Version) -> - case do_recv(RecvPid, undefined) of - {ok, <>, _} -> - case Fieldcount of - 0 -> - %% No Tabular data - {AffectedRows, Rest1} = decode_length_binary(Rest), - {InsertId, _} = decode_length_binary(Rest1), - {updated, #mysql_result{insert_id = InsertId, affectedrows=AffectedRows}}; - 255 -> - <<_Code:16/little, Message/binary>> = Rest, - {error, #mysql_result{error=Message}}; - _ -> - %% Tabular data received - case get_fields(RecvPid, [], Version) of - {ok, Fields} -> - case get_rows(Fields, RecvPid, []) of - {ok, Rows} -> - {data, #mysql_result{fieldinfo=Fields, - rows=Rows}}; - {error, Reason} -> - {error, Reason} - end; - {error, Reason} -> - {error, Reason} - end - end; - {error, Reason} -> - {error, Reason} - end. - -decode_length_binary(<>) -> - if - Len =< 251 -> - {Len, Rest}; - Len == 252 -> %two bytes - <> = Rest, - {Val, Rest1}; - Len == 253 -> %three - <> = Rest, - {Val, Rest1}; - Len == 254 -> %eight - <> = Rest, - {Val, Rest1}; - true -> - %?ERROR("affectedrows: ~p", [Len]), - {0, Rest} - end. - -%%-------------------------------------------------------------------- -%% Function: do_recv(RecvPid, SeqNum) -%% RecvPid = pid(), mysql_recv process -%% SeqNum = undefined | integer() -%% Descrip.: Wait for a frame decoded and sent to us by RecvPid. -%% Either wait for a specific frame if SeqNum is an integer, -%% or just any frame if SeqNum is undefined. -%% Returns : {ok, Packet, Num} | -%% {error, Reason} -%% Reason = term() -%% -%% Note : Only to be used externally by the 'mysql_auth' module. -%%-------------------------------------------------------------------- -do_recv(RecvPid, SeqNum) when SeqNum == undefined -> - receive - {mysql_recv, RecvPid, data, Packet, Num} -> - {ok, Packet, Num}; - {mysql_recv, RecvPid, closed, _E} -> - {error, socket_closed} - after ?CONNECT_TIMEOUT -> - {error, mysql_timeout} - end; - -do_recv(RecvPid, SeqNum) when is_integer(SeqNum) -> - ResponseNum = SeqNum + 1, - receive - {mysql_recv, RecvPid, data, Packet, ResponseNum} -> - {ok, Packet, ResponseNum}; - {mysql_recv, RecvPid, closed, _E} -> - {error, socket_closed} - after ?CONNECT_TIMEOUT -> - {error, mysql_timeout} - end. - -call(Conn, Req) -> - gen_server:call(Conn, Req). - -call(Conn, Req, Timeout) -> - gen_server:call(Conn, Req, Timeout). - -%%-------------------------------------------------------------------- -%% Function: get_fields(RecvPid, [], Version) -%% RecvPid = pid(), mysql_recv process -%% Version = integer(), Representing MySQL version used -%% Descrip.: Received and decode field information. -%% Returns : {ok, FieldInfo} | -%% {error, Reason} -%% FieldInfo = list() of term() -%% Reason = term() -%%-------------------------------------------------------------------- -%% Support for MySQL 4.0.x: -get_fields(RecvPid, Res, ?MYSQL_4_0) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, _Num} -> - case Packet of - <<254:8>> -> - {ok, lists:reverse(Res)}; - <<254:8, Rest/binary>> when size(Rest) < 8 -> - {ok, lists:reverse(Res)}; - _ -> - {Table, Rest} = get_with_length(Packet), - {Field, Rest2} = get_with_length(Rest), - {LengthB, Rest3} = get_with_length(Rest2), - LengthL = size(LengthB) * 8, - <> = LengthB, - {Type, Rest4} = get_with_length(Rest3), - {_Flags, _Rest5} = get_with_length(Rest4), - This = {Table, - Field, - Length, - %% TODO: Check on MySQL 4.0 if types are specified - %% using the same 4.1 formalism and could - %% be expanded to atoms: - Type}, - get_fields(RecvPid, [This | Res], ?MYSQL_4_0) - end; - {error, Reason} -> - {error, Reason} - end; -%% Support for MySQL 4.1.x and 5.x: -get_fields(RecvPid, Res, ?MYSQL_4_1) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, _Num} -> - case Packet of - <<254:8>> -> - {ok, lists:reverse(Res)}; - <<254:8, Rest/binary>> when size(Rest) < 8 -> - {ok, lists:reverse(Res)}; - _ -> - {_Catalog, Rest} = get_with_length(Packet), - {_Database, Rest2} = get_with_length(Rest), - {Table, Rest3} = get_with_length(Rest2), - %% OrgTable is the real table name if Table is an alias - {_OrgTable, Rest4} = get_with_length(Rest3), - {Field, Rest5} = get_with_length(Rest4), - %% OrgField is the real field name if Field is an alias - {_OrgField, Rest6} = get_with_length(Rest5), - - <<_Metadata:8/little, _Charset:16/little, - Length:32/little, Type:8/little, - _Flags:16/little, _Decimals:8/little, - _Rest7/binary>> = Rest6, - - This = {Table, - Field, - Length, - get_field_datatype(Type)}, - get_fields(RecvPid, [This | Res], ?MYSQL_4_1) - end; - {error, Reason} -> - {error, Reason} - end. - -%%-------------------------------------------------------------------- -%% Function: get_rows(N, RecvPid, []) -%% N = integer(), number of rows to get -%% RecvPid = pid(), mysql_recv process -%% Descrip.: Receive and decode a number of rows. -%% Returns : {ok, Rows} | -%% {error, Reason} -%% Rows = list() of [string()] -%%-------------------------------------------------------------------- -get_rows(Fields, RecvPid, Res) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, _Num} -> - case Packet of - <<254:8, Rest/binary>> when size(Rest) < 8 -> - {ok, lists:reverse(Res)}; - _ -> - {ok, This} = get_row(Fields, Packet, []), - get_rows(Fields, RecvPid, [This | Res]) - end; - {error, Reason} -> - {error, Reason} - end. - -%% part of get_rows/4 -get_row([], _Data, Res) -> - {ok, lists:reverse(Res)}; -get_row([Field | OtherFields], Data, Res) -> - {Col, Rest} = get_with_length(Data), - This = case Col of - null -> - undefined; - _ -> - convert_type(Col, element(4, Field)) - end, - get_row(OtherFields, Rest, [This | Res]). - -get_with_length(<<251:8, Rest/binary>>) -> - {null, Rest}; -get_with_length(<<252:8, Length:16/little, Rest/binary>>) -> - split_binary(Rest, Length); -get_with_length(<<253:8, Length:24/little, Rest/binary>>) -> - split_binary(Rest, Length); -get_with_length(<<254:8, Length:64/little, Rest/binary>>) -> - split_binary(Rest, Length); -get_with_length(<>) when Length < 251 -> - split_binary(Rest, Length). - - -%%-------------------------------------------------------------------- -%% Function: do_send(Sock, Packet, SeqNum) -%% Sock = term(), gen_tcp socket -%% Packet = binary() -%% SeqNum = integer(), packet sequence number -%% Descrip.: Send a packet to the MySQL server. -%% Returns : result of gen_tcp:send/2 -%%-------------------------------------------------------------------- -do_send(Sock, Packet, SeqNum) when is_binary(Packet), is_integer(SeqNum) -> - Data = <<(size(Packet)):24/little, SeqNum:8, Packet/binary>>, - gen_tcp:send(Sock, Data). - -%%-------------------------------------------------------------------- -%% Function: normalize_version(Version) -%% Version = string() -%% Descrip.: Return a flag corresponding to the MySQL version used. -%% The protocol used depends on this flag. -%% Returns : Version = string() -%%-------------------------------------------------------------------- -normalize_version([$4,$.,$0|_T]) -> - %?DEBUG("switching to MySQL 4.0.x protocol.", []), - ?MYSQL_4_0; -normalize_version([$4,$.,$1|_T]) -> - ?MYSQL_4_1; -normalize_version([$5|_T]) -> - %% MySQL version 5.x protocol is compliant with MySQL 4.1.x: - ?MYSQL_4_1; -normalize_version([$6|_T]) -> - %% MySQL version 6.x protocol is compliant with MySQL 4.1.x: - ?MYSQL_4_1; -normalize_version(_Other) -> - %?ERROR("MySQL version '~p' not supported: MySQL Erlang module " - % "might not work correctly.", [Other]), - %% Error, but trying the oldest protocol anyway: - ?MYSQL_4_0. - -%%-------------------------------------------------------------------- -%% Function: get_field_datatype(DataType) -%% DataType = integer(), MySQL datatype -%% Descrip.: Return MySQL field datatype as description string -%% Returns : String, MySQL datatype -%%-------------------------------------------------------------------- -get_field_datatype(0) -> 'DECIMAL'; -get_field_datatype(1) -> 'TINY'; -get_field_datatype(2) -> 'SHORT'; -get_field_datatype(3) -> 'LONG'; -get_field_datatype(4) -> 'FLOAT'; -get_field_datatype(5) -> 'DOUBLE'; -get_field_datatype(6) -> 'NULL'; -get_field_datatype(7) -> 'TIMESTAMP'; -get_field_datatype(8) -> 'LONGLONG'; -get_field_datatype(9) -> 'INT24'; -get_field_datatype(10) -> 'DATE'; -get_field_datatype(11) -> 'TIME'; -get_field_datatype(12) -> 'DATETIME'; -get_field_datatype(13) -> 'YEAR'; -get_field_datatype(14) -> 'NEWDATE'; -get_field_datatype(246) -> 'NEWDECIMAL'; -get_field_datatype(247) -> 'ENUM'; -get_field_datatype(248) -> 'SET'; -get_field_datatype(249) -> 'TINYBLOB'; -get_field_datatype(250) -> 'MEDIUM_BLOG'; -get_field_datatype(251) -> 'LONG_BLOG'; -get_field_datatype(252) -> 'BLOB'; -get_field_datatype(253) -> 'VAR_STRING'; -get_field_datatype(254) -> 'STRING'; -get_field_datatype(255) -> 'GEOMETRY'. - -convert_type(Val, ColType) -> - case ColType of - T when T == 'TINY'; - T == 'SHORT'; - T == 'LONG'; - T == 'LONGLONG'; - T == 'INT24'; - T == 'YEAR' -> - list_to_integer(binary_to_list(Val)); - T when T == 'TIMESTAMP'; - T == 'DATETIME' -> - {ok, [Year, Month, Day, Hour, Minute, Second], _Leftovers} = - io_lib:fread("~d-~d-~d ~d:~d:~d", binary_to_list(Val)), - {datetime, {{Year, Month, Day}, {Hour, Minute, Second}}}; - 'TIME' -> - {ok, [Hour, Minute, Second], _Leftovers} = - io_lib:fread("~d:~d:~d", binary_to_list(Val)), - {time, {Hour, Minute, Second}}; - 'DATE' -> - {ok, [Year, Month, Day], _Leftovers} = - io_lib:fread("~d-~d-~d", binary_to_list(Val)), - {date, {Year, Month, Day}}; - T when T == 'DECIMAL'; - T == 'NEWDECIMAL'; - T == 'FLOAT'; - T == 'DOUBLE' -> - {ok, [Num], _Leftovers} = - case io_lib:fread("~f", binary_to_list(Val)) of - {error, _} -> - io_lib:fread("~d", binary_to_list(Val)); - Res -> - Res - end, - Num; - _Other -> - Val - end. diff --git a/plugins/emysql/src/emysql_recv.erl b/plugins/emysql/src/emysql_recv.erl deleted file mode 100644 index 5577c2e9a..000000000 --- a/plugins/emysql/src/emysql_recv.erl +++ /dev/null @@ -1,130 +0,0 @@ -%%%------------------------------------------------------------------- -%%% File : emysql_recv.erl -%%% Author : Fredrik Thulin -%%% Descrip.: Handles data being received on a MySQL socket. Decodes -%%% per-row framing and sends each row to parent. -%%% -%%% Created : 4 Aug 2005 by Fredrik Thulin -%%% -%%% Note : All MySQL code was written by Magnus Ahltorp, originally -%%% in the file mysql.erl - I just moved it here. -%%% -%%% Copyright (c) 2001-2004 Kungliga Tekniska -%%% See the file COPYING -%%% -%%% Signals this receiver process can send to it's parent -%%% (the parent is a mysql_conn connection handler) : -%%% -%%% {mysql_recv, self(), data, Packet, Num} -%%% {mysql_recv, self(), closed, {error, Reason}} -%%% {mysql_recv, self(), closed, normal} -%%% -%%% Internally (from inside init/4 to start_link/4) the -%%% following signals may be sent to the parent process : -%%% -%%% {mysql_recv, self(), init, {ok, Sock}} -%%% {mysql_recv, self(), init, {error, E}} -%%% -%%%------------------------------------------------------------------- --module(emysql_recv). - -%%-------------------------------------------------------------------- -%% External exports (should only be used by the 'mysql_conn' module) -%%-------------------------------------------------------------------- --export([start_link/2]). - -%callback --export([init/3]). - --record(state, { - socket, - parent, - log_fun, - data}). - --define(SECURE_CONNECTION, 32768). - --define(CONNECT_TIMEOUT, 10000). - -%%-------------------------------------------------------------------- -%% Function: start_link(Host, Port, Parent) -%% Host = string() -%% Port = integer() -%% Parent = pid(), process that should get received frames -%% Descrip.: Start a process that connects to Host:Port and waits for -%% data. When it has received a MySQL frame, it sends it to -%% Parent and waits for the next frame. -%% Returns : {ok, RecvPid, Socket} | -%% {error, Reason} -%% RecvPid = pid(), receiver process pid -%% Socket = term(), gen_tcp socket -%% Reason = atom() | string() -%%-------------------------------------------------------------------- -start_link(Host, Port) -> - proc_lib:start_link(?MODULE, init, [self(), Host, Port]). - -%%-------------------------------------------------------------------- -%% Function: init((Host, Port, Parent) -%% Host = string() -%% Port = integer() -%% Parent = pid(), process that should get received frames -%% Descrip.: Connect to Host:Port and then enter receive-loop. -%% Returns : error | never returns -%%-------------------------------------------------------------------- -init(Parent, Host, Port) -> - case gen_tcp:connect(Host, Port, [binary, {packet, 0}]) of - {ok, Sock} -> - proc_lib:init_ack(Parent, {ok, self(), Sock}), - loop(#state{socket = Sock, parent = Parent, data = <<>>}); - {error, Reason} -> - proc_lib:init_ack(Parent, {error, Reason}) - end. - -%%-------------------------------------------------------------------- -%% Function: loop(State) -%% State = state record() -%% Descrip.: The main loop. Wait for data from our TCP socket and act -%% on received data or signals that our socket was closed. -%% Returns : error | never returns -%%-------------------------------------------------------------------- -loop(State) -> - Sock = State#state.socket, - receive - {tcp, Sock, InData} -> - NewData = list_to_binary([State#state.data, InData]), - %% send data to parent if we have enough data - Rest = sendpacket(State#state.parent, NewData), - loop(State#state{data = Rest}); - {tcp_error, Sock, Reason} -> - State#state.parent ! {mysql_recv, self(), closed, {error, Reason}}, - error; - {tcp_closed, Sock} -> - State#state.parent ! {mysql_recv, self(), closed, normal}, - error; - _Other -> %maybe system message - loop(State) - end. - -%%-------------------------------------------------------------------- -%% Function: sendpacket(Parent, Data) -%% Parent = pid() -%% Data = binary() -%% Descrip.: Check if we have received one or more complete frames by -%% now, and if so - send them to Parent. -%% Returns : Rest = binary() -%%-------------------------------------------------------------------- -%% send data to parent if we have enough data -sendpacket(Parent, Data) -> - case Data of - <> -> - if - Length =< size(D) -> - {Packet, Rest} = split_binary(D, Length), - Parent ! {mysql_recv, self(), data, Packet, Num}, - sendpacket(Parent, Rest); - true -> - Data - end; - _ -> - Data - end. diff --git a/plugins/emysql/src/emysql_sup.erl b/plugins/emysql/src/emysql_sup.erl deleted file mode 100644 index b915f3593..000000000 --- a/plugins/emysql/src/emysql_sup.erl +++ /dev/null @@ -1,34 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : emysql_sup.erl -%%% Author : Ery Lee -%%% Purpose : Mysql driver supervisor -%%% Created : 21 May 2009 -%%% Updated : 11 Jan 2010 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2012, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql_sup). - --author('ery.lee@gmail.com'). - --behavior(supervisor). - -%% API --export([start_link/1, init/1]). - -start_link(Opts) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, Opts). - -init(Opts) -> - PoolSize = proplists:get_value(pool, Opts, - erlang:system_info(schedulers)), - {ok, {{one_for_one, 10, 10}, - [{emysql, {emysql, start_link, [PoolSize]}, transient, - 16#ffffffff, worker, [emysql]} | - [{I, {emysql_conn, start_link, [I, Opts]}, transient, 16#ffffffff, - worker, [emysql_conn, emysql_recv]} || I <- lists:seq(1, PoolSize)]] - } - }. - - diff --git a/apps/emqttd/priv/www/index.html b/priv/www/index.html similarity index 100% rename from apps/emqttd/priv/www/index.html rename to priv/www/index.html diff --git a/apps/emqttd/priv/www/mqttws31.js b/priv/www/mqttws31.js similarity index 100% rename from apps/emqttd/priv/www/mqttws31.js rename to priv/www/mqttws31.js diff --git a/apps/emqttd/priv/www/websocket.html b/priv/www/websocket.html similarity index 100% rename from apps/emqttd/priv/www/websocket.html rename to priv/www/websocket.html diff --git a/apps/emqttd/src/emqttd.app.src b/src/emqttd.app.src similarity index 100% rename from apps/emqttd/src/emqttd.app.src rename to src/emqttd.app.src diff --git a/apps/emqttd/src/emqttd.erl b/src/emqttd.erl similarity index 100% rename from apps/emqttd/src/emqttd.erl rename to src/emqttd.erl diff --git a/apps/emqttd/src/emqttd_access_control.erl b/src/emqttd_access_control.erl similarity index 100% rename from apps/emqttd/src/emqttd_access_control.erl rename to src/emqttd_access_control.erl diff --git a/apps/emqttd/src/emqttd_access_rule.erl b/src/emqttd_access_rule.erl similarity index 100% rename from apps/emqttd/src/emqttd_access_rule.erl rename to src/emqttd_access_rule.erl diff --git a/apps/emqttd/src/emqttd_acl_internal.erl b/src/emqttd_acl_internal.erl similarity index 100% rename from apps/emqttd/src/emqttd_acl_internal.erl rename to src/emqttd_acl_internal.erl diff --git a/apps/emqttd/src/emqttd_acl_mod.erl b/src/emqttd_acl_mod.erl similarity index 100% rename from apps/emqttd/src/emqttd_acl_mod.erl rename to src/emqttd_acl_mod.erl diff --git a/apps/emqttd/src/emqttd_alarm.erl b/src/emqttd_alarm.erl similarity index 100% rename from apps/emqttd/src/emqttd_alarm.erl rename to src/emqttd_alarm.erl diff --git a/apps/emqttd/src/emqttd_app.erl b/src/emqttd_app.erl similarity index 100% rename from apps/emqttd/src/emqttd_app.erl rename to src/emqttd_app.erl diff --git a/apps/emqttd/src/emqttd_auth_anonymous.erl b/src/emqttd_auth_anonymous.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_anonymous.erl rename to src/emqttd_auth_anonymous.erl diff --git a/apps/emqttd/src/emqttd_auth_clientid.erl b/src/emqttd_auth_clientid.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_clientid.erl rename to src/emqttd_auth_clientid.erl diff --git a/apps/emqttd/src/emqttd_auth_ldap.erl b/src/emqttd_auth_ldap.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_ldap.erl rename to src/emqttd_auth_ldap.erl diff --git a/apps/emqttd/src/emqttd_auth_mod.erl b/src/emqttd_auth_mod.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_mod.erl rename to src/emqttd_auth_mod.erl diff --git a/apps/emqttd/src/emqttd_auth_username.erl b/src/emqttd_auth_username.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_username.erl rename to src/emqttd_auth_username.erl diff --git a/apps/emqttd/src/emqttd_bridge.erl b/src/emqttd_bridge.erl similarity index 100% rename from apps/emqttd/src/emqttd_bridge.erl rename to src/emqttd_bridge.erl diff --git a/apps/emqttd/src/emqttd_bridge_sup.erl b/src/emqttd_bridge_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_bridge_sup.erl rename to src/emqttd_bridge_sup.erl diff --git a/apps/emqttd/src/emqttd_broker.erl b/src/emqttd_broker.erl similarity index 100% rename from apps/emqttd/src/emqttd_broker.erl rename to src/emqttd_broker.erl diff --git a/apps/emqttd/src/emqttd_client.erl b/src/emqttd_client.erl similarity index 100% rename from apps/emqttd/src/emqttd_client.erl rename to src/emqttd_client.erl diff --git a/apps/emqttd/src/emqttd_cm.erl b/src/emqttd_cm.erl similarity index 100% rename from apps/emqttd/src/emqttd_cm.erl rename to src/emqttd_cm.erl diff --git a/apps/emqttd/src/emqttd_cm_sup.erl b/src/emqttd_cm_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_cm_sup.erl rename to src/emqttd_cm_sup.erl diff --git a/apps/emqttd/src/emqttd_ctl.erl b/src/emqttd_ctl.erl similarity index 100% rename from apps/emqttd/src/emqttd_ctl.erl rename to src/emqttd_ctl.erl diff --git a/apps/emqttd/src/emqttd_gen_mod.erl b/src/emqttd_gen_mod.erl similarity index 100% rename from apps/emqttd/src/emqttd_gen_mod.erl rename to src/emqttd_gen_mod.erl diff --git a/apps/emqttd/src/emqttd_http.erl b/src/emqttd_http.erl similarity index 100% rename from apps/emqttd/src/emqttd_http.erl rename to src/emqttd_http.erl diff --git a/apps/emqttd/src/emqttd_keepalive.erl b/src/emqttd_keepalive.erl similarity index 100% rename from apps/emqttd/src/emqttd_keepalive.erl rename to src/emqttd_keepalive.erl diff --git a/apps/emqttd/src/emqttd_message.erl b/src/emqttd_message.erl similarity index 100% rename from apps/emqttd/src/emqttd_message.erl rename to src/emqttd_message.erl diff --git a/apps/emqttd/src/emqttd_metrics.erl b/src/emqttd_metrics.erl similarity index 100% rename from apps/emqttd/src/emqttd_metrics.erl rename to src/emqttd_metrics.erl diff --git a/apps/emqttd/src/emqttd_mnesia.erl b/src/emqttd_mnesia.erl similarity index 100% rename from apps/emqttd/src/emqttd_mnesia.erl rename to src/emqttd_mnesia.erl diff --git a/apps/emqttd/src/emqttd_mod_autosub.erl b/src/emqttd_mod_autosub.erl similarity index 100% rename from apps/emqttd/src/emqttd_mod_autosub.erl rename to src/emqttd_mod_autosub.erl diff --git a/apps/emqttd/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl similarity index 100% rename from apps/emqttd/src/emqttd_mod_presence.erl rename to src/emqttd_mod_presence.erl diff --git a/apps/emqttd/src/emqttd_mod_rewrite.erl b/src/emqttd_mod_rewrite.erl similarity index 100% rename from apps/emqttd/src/emqttd_mod_rewrite.erl rename to src/emqttd_mod_rewrite.erl diff --git a/apps/emqttd/src/emqttd_mod_sup.erl b/src/emqttd_mod_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_mod_sup.erl rename to src/emqttd_mod_sup.erl diff --git a/apps/emqttd/src/emqttd_mqueue.erl b/src/emqttd_mqueue.erl similarity index 100% rename from apps/emqttd/src/emqttd_mqueue.erl rename to src/emqttd_mqueue.erl diff --git a/apps/emqttd/src/emqttd_msg_store.erl b/src/emqttd_msg_store.erl similarity index 100% rename from apps/emqttd/src/emqttd_msg_store.erl rename to src/emqttd_msg_store.erl diff --git a/apps/emqttd/src/emqttd_net.erl b/src/emqttd_net.erl similarity index 100% rename from apps/emqttd/src/emqttd_net.erl rename to src/emqttd_net.erl diff --git a/apps/emqttd/src/emqttd_opts.erl b/src/emqttd_opts.erl similarity index 100% rename from apps/emqttd/src/emqttd_opts.erl rename to src/emqttd_opts.erl diff --git a/apps/emqttd/src/emqttd_packet.erl b/src/emqttd_packet.erl similarity index 100% rename from apps/emqttd/src/emqttd_packet.erl rename to src/emqttd_packet.erl diff --git a/apps/emqttd/src/emqttd_parser.erl b/src/emqttd_parser.erl similarity index 100% rename from apps/emqttd/src/emqttd_parser.erl rename to src/emqttd_parser.erl diff --git a/apps/emqttd/src/emqttd_pooler.erl b/src/emqttd_pooler.erl similarity index 100% rename from apps/emqttd/src/emqttd_pooler.erl rename to src/emqttd_pooler.erl diff --git a/apps/emqttd/src/emqttd_pooler_sup.erl b/src/emqttd_pooler_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_pooler_sup.erl rename to src/emqttd_pooler_sup.erl diff --git a/apps/emqttd/src/emqttd_protocol.erl b/src/emqttd_protocol.erl similarity index 100% rename from apps/emqttd/src/emqttd_protocol.erl rename to src/emqttd_protocol.erl diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl similarity index 100% rename from apps/emqttd/src/emqttd_pubsub.erl rename to src/emqttd_pubsub.erl diff --git a/apps/emqttd/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_pubsub_sup.erl rename to src/emqttd_pubsub_sup.erl diff --git a/apps/emqttd/src/emqttd_serialiser.erl b/src/emqttd_serialiser.erl similarity index 100% rename from apps/emqttd/src/emqttd_serialiser.erl rename to src/emqttd_serialiser.erl diff --git a/apps/emqttd/src/emqttd_session.erl b/src/emqttd_session.erl similarity index 100% rename from apps/emqttd/src/emqttd_session.erl rename to src/emqttd_session.erl diff --git a/apps/emqttd/src/emqttd_session_sup.erl b/src/emqttd_session_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_session_sup.erl rename to src/emqttd_session_sup.erl diff --git a/apps/emqttd/src/emqttd_sm.erl b/src/emqttd_sm.erl similarity index 100% rename from apps/emqttd/src/emqttd_sm.erl rename to src/emqttd_sm.erl diff --git a/apps/emqttd/src/emqttd_sm_sup.erl b/src/emqttd_sm_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_sm_sup.erl rename to src/emqttd_sm_sup.erl diff --git a/apps/emqttd/src/emqttd_stats.erl b/src/emqttd_stats.erl similarity index 100% rename from apps/emqttd/src/emqttd_stats.erl rename to src/emqttd_stats.erl diff --git a/apps/emqttd/src/emqttd_sup.erl b/src/emqttd_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_sup.erl rename to src/emqttd_sup.erl diff --git a/apps/emqttd/src/emqttd_sysmon.erl b/src/emqttd_sysmon.erl similarity index 100% rename from apps/emqttd/src/emqttd_sysmon.erl rename to src/emqttd_sysmon.erl diff --git a/apps/emqttd/src/emqttd_throttle.erl b/src/emqttd_throttle.erl similarity index 100% rename from apps/emqttd/src/emqttd_throttle.erl rename to src/emqttd_throttle.erl diff --git a/apps/emqttd/src/emqttd_topic.erl b/src/emqttd_topic.erl similarity index 100% rename from apps/emqttd/src/emqttd_topic.erl rename to src/emqttd_topic.erl diff --git a/apps/emqttd/src/emqttd_trace.erl b/src/emqttd_trace.erl similarity index 100% rename from apps/emqttd/src/emqttd_trace.erl rename to src/emqttd_trace.erl diff --git a/apps/emqttd/src/emqttd_trie.erl b/src/emqttd_trie.erl similarity index 100% rename from apps/emqttd/src/emqttd_trie.erl rename to src/emqttd_trie.erl diff --git a/apps/emqttd/src/emqttd_util.erl b/src/emqttd_util.erl similarity index 100% rename from apps/emqttd/src/emqttd_util.erl rename to src/emqttd_util.erl diff --git a/apps/emqttd/src/emqttd_vm.erl b/src/emqttd_vm.erl similarity index 100% rename from apps/emqttd/src/emqttd_vm.erl rename to src/emqttd_vm.erl diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/src/emqttd_ws_client.erl similarity index 100% rename from apps/emqttd/src/emqttd_ws_client.erl rename to src/emqttd_ws_client.erl diff --git a/apps/emqttd/test/emqttd_access_control_tests.erl b/test/emqttd_access_control_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_access_control_tests.erl rename to test/emqttd_access_control_tests.erl diff --git a/apps/emqttd/test/emqttd_access_rule_tests.erl b/test/emqttd_access_rule_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_access_rule_tests.erl rename to test/emqttd_access_rule_tests.erl diff --git a/apps/emqttd/test/emqttd_acl_test_mod.erl b/test/emqttd_acl_test_mod.erl similarity index 100% rename from apps/emqttd/test/emqttd_acl_test_mod.erl rename to test/emqttd_acl_test_mod.erl diff --git a/apps/emqttd/test/emqttd_acl_tests.erl b/test/emqttd_acl_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_acl_tests.erl rename to test/emqttd_acl_tests.erl diff --git a/apps/emqttd/test/emqttd_auth_anonymous_test_mod.erl b/test/emqttd_auth_anonymous_test_mod.erl similarity index 100% rename from apps/emqttd/test/emqttd_auth_anonymous_test_mod.erl rename to test/emqttd_auth_anonymous_test_mod.erl diff --git a/apps/emqttd/test/emqttd_opts_tests.erl b/test/emqttd_opts_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_opts_tests.erl rename to test/emqttd_opts_tests.erl diff --git a/apps/emqttd/test/emqttd_parser_tests.erl b/test/emqttd_parser_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_parser_tests.erl rename to test/emqttd_parser_tests.erl diff --git a/apps/emqttd/test/emqttd_serialiser_tests.erl b/test/emqttd_serialiser_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_serialiser_tests.erl rename to test/emqttd_serialiser_tests.erl diff --git a/apps/emqttd/test/emqttd_topic_tests.erl b/test/emqttd_topic_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_topic_tests.erl rename to test/emqttd_topic_tests.erl diff --git a/apps/emqttd/test/esockd_access.erl b/test/esockd_access.erl similarity index 100% rename from apps/emqttd/test/esockd_access.erl rename to test/esockd_access.erl diff --git a/apps/emqttd/test/test_acl.config b/test/test_acl.config similarity index 100% rename from apps/emqttd/test/test_acl.config rename to test/test_acl.config From 27efd4de264cd4d75c33b7017a7437957d5c8b1a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 22 Jun 2015 16:40:40 +0800 Subject: [PATCH 084/104] rm tests --- .gitmodules | 3 -- tests/benchmarks/.placeholder | 0 tests/benchmarks/high-mqtt.xml | 64 ----------------------------- tests/org.eclipse.paho.mqtt.testing | 1 - 4 files changed, 68 deletions(-) delete mode 100644 tests/benchmarks/.placeholder delete mode 100644 tests/benchmarks/high-mqtt.xml delete mode 160000 tests/org.eclipse.paho.mqtt.testing diff --git a/.gitmodules b/.gitmodules index 2d15edfc9..093a4a897 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "tests/org.eclipse.paho.mqtt.testing"] - path = tests/org.eclipse.paho.mqtt.testing - url = git://git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.testing.git [submodule "plugins/emqttd_dashboard"] path = plugins/emqttd_dashboard url = https://github.com/emqtt/emqttd_dashboard.git diff --git a/tests/benchmarks/.placeholder b/tests/benchmarks/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/benchmarks/high-mqtt.xml b/tests/benchmarks/high-mqtt.xml deleted file mode 100644 index 56907bb2b..000000000 --- a/tests/benchmarks/high-mqtt.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - test_message - - - - - - - - - - diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing deleted file mode 160000 index bdc690db8..000000000 --- a/tests/org.eclipse.paho.mqtt.testing +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bdc690db847cec1c682e604dca571c72ff756305 From af025d5625892b72cb616f26a2c7f00d743d4e9b Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 22 Jun 2015 16:45:10 +0800 Subject: [PATCH 085/104] fix project structure --- rebar.config | 1 - rel/reltool.config | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rebar.config b/rebar.config index 7755b06d7..55093b0f0 100644 --- a/rebar.config +++ b/rebar.config @@ -20,7 +20,6 @@ {sub_dirs, [ "rel", - "apps/*/", "plugins/*/"]}. {deps, [ diff --git a/rel/reltool.config b/rel/reltool.config index 497676d87..42e4755ef 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -1,7 +1,7 @@ %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et {sys, [ - {lib_dirs, ["../apps", "../deps", "../plugins"]}, + {lib_dirs, ["../deps"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, {rel, "emqttd", git, @@ -61,7 +61,7 @@ {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}]} + {app, emqttd, [{mod_cond, app}, {incl_cond, include}, {lib_dir, ".."}]} ]}. {target_dir, "emqttd"}. From c42673ae792a3743395adbf38ce3c271aabeb303 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 22 Jun 2015 21:02:20 +0800 Subject: [PATCH 086/104] rm priv --- priv/www/index.html | 64 -- priv/www/mqttws31.js | 2143 --------------------------------------- priv/www/websocket.html | 59 -- 3 files changed, 2266 deletions(-) delete mode 100644 priv/www/index.html delete mode 100644 priv/www/mqttws31.js delete mode 100644 priv/www/websocket.html diff --git a/priv/www/index.html b/priv/www/index.html deleted file mode 100644 index d84f633cd..000000000 --- a/priv/www/index.html +++ /dev/null @@ -1,64 +0,0 @@ - - - MQTT over WebSocket - - -

MQTT Over WebSocket

-
- -   State: -
- - - - diff --git a/priv/www/mqttws31.js b/priv/www/mqttws31.js deleted file mode 100644 index 91dfc2de5..000000000 --- a/priv/www/mqttws31.js +++ /dev/null @@ -1,2143 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2013 IBM Corp. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * and Eclipse Distribution License v1.0 which accompany this distribution. - * - * The Eclipse Public License is available at - * http://www.eclipse.org/legal/epl-v10.html - * and the Eclipse Distribution License is available at - * http://www.eclipse.org/org/documents/edl-v10.php. - * - * Contributors: - * Andrew Banks - initial API and implementation and initial documentation - *******************************************************************************/ - - -// Only expose a single object name in the global namespace. -// Everything must go through this module. Global Paho.MQTT module -// only has a single public function, client, which returns -// a Paho.MQTT client object given connection details. - -/** - * Send and receive messages using web browsers. - *

- * This programming interface lets a JavaScript client application use the MQTT V3.1 or - * V3.1.1 protocol to connect to an MQTT-supporting messaging server. - * - * The function supported includes: - *

    - *
  1. Connecting to and disconnecting from a server. The server is identified by its host name and port number. - *
  2. Specifying options that relate to the communications link with the server, - * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required. - *
  3. Subscribing to and receiving messages from MQTT Topics. - *
  4. Publishing messages to MQTT Topics. - *
- *

- * The API consists of two main objects: - *

- *
{@link Paho.MQTT.Client}
- *
This contains methods that provide the functionality of the API, - * including provision of callbacks that notify the application when a message - * arrives from or is delivered to the messaging server, - * or when the status of its connection to the messaging server changes.
- *
{@link Paho.MQTT.Message}
- *
This encapsulates the payload of the message along with various attributes - * associated with its delivery, in particular the destination to which it has - * been (or is about to be) sent.
- *
- *

- * The programming interface validates parameters passed to it, and will throw - * an Error containing an error message intended for developer use, if it detects - * an error with any parameter. - *

- * Example: - * - *

-client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
-client.onConnectionLost = onConnectionLost;
-client.onMessageArrived = onMessageArrived;
-client.connect({onSuccess:onConnect});
-
-function onConnect() {
-  // Once a connection has been made, make a subscription and send a message.
-  console.log("onConnect");
-  client.subscribe("/World");
-  message = new Paho.MQTT.Message("Hello");
-  message.destinationName = "/World";
-  client.send(message); 
-};
-function onConnectionLost(responseObject) {
-  if (responseObject.errorCode !== 0)
-	console.log("onConnectionLost:"+responseObject.errorMessage);
-};
-function onMessageArrived(message) {
-  console.log("onMessageArrived:"+message.payloadString);
-  client.disconnect(); 
-};	
- * 
- * @namespace Paho.MQTT - */ - -if (typeof Paho === "undefined") { - Paho = {}; -} - -Paho.MQTT = (function (global) { - - // Private variables below, these are only visible inside the function closure - // which is used to define the module. - - var version = "@VERSION@"; - var buildLevel = "@BUILDLEVEL@"; - - /** - * Unique message type identifiers, with associated - * associated integer values. - * @private - */ - var MESSAGE_TYPE = { - CONNECT: 1, - CONNACK: 2, - PUBLISH: 3, - PUBACK: 4, - PUBREC: 5, - PUBREL: 6, - PUBCOMP: 7, - SUBSCRIBE: 8, - SUBACK: 9, - UNSUBSCRIBE: 10, - UNSUBACK: 11, - PINGREQ: 12, - PINGRESP: 13, - DISCONNECT: 14 - }; - - // Collection of utility methods used to simplify module code - // and promote the DRY pattern. - - /** - * Validate an object's parameter names to ensure they - * match a list of expected variables name for this option - * type. Used to ensure option object passed into the API don't - * contain erroneous parameters. - * @param {Object} obj - User options object - * @param {Object} keys - valid keys and types that may exist in obj. - * @throws {Error} Invalid option parameter found. - * @private - */ - var validate = function(obj, keys) { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - if (keys.hasOwnProperty(key)) { - if (typeof obj[key] !== keys[key]) - throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); - } else { - var errorStr = "Unknown property, " + key + ". Valid properties are:"; - for (var key in keys) - if (keys.hasOwnProperty(key)) - errorStr = errorStr+" "+key; - throw new Error(errorStr); - } - } - } - }; - - /** - * Return a new function which runs the user function bound - * to a fixed scope. - * @param {function} User function - * @param {object} Function scope - * @return {function} User function bound to another scope - * @private - */ - var scope = function (f, scope) { - return function () { - return f.apply(scope, arguments); - }; - }; - - /** - * Unique message type identifiers, with associated - * associated integer values. - * @private - */ - var ERROR = { - OK: {code:0, text:"AMQJSC0000I OK."}, - CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."}, - SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."}, - UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."}, - PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."}, - INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"}, - CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."}, - SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."}, - SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."}, - MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."}, - UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."}, - INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."}, - INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."}, - INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."}, - UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."}, - INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."}, - INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."}, - MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."}, - }; - - /** CONNACK RC Meaning. */ - var CONNACK_RC = { - 0:"Connection Accepted", - 1:"Connection Refused: unacceptable protocol version", - 2:"Connection Refused: identifier rejected", - 3:"Connection Refused: server unavailable", - 4:"Connection Refused: bad user name or password", - 5:"Connection Refused: not authorized" - }; - - /** - * Format an error message text. - * @private - * @param {error} ERROR.KEY value above. - * @param {substitutions} [array] substituted into the text. - * @return the text with the substitutions made. - */ - var format = function(error, substitutions) { - var text = error.text; - if (substitutions) { - var field,start; - for (var i=0; i 0) { - var part1 = text.substring(0,start); - var part2 = text.substring(start+field.length); - text = part1+substitutions[i]+part2; - } - } - } - return text; - }; - - //MQTT protocol and version 6 M Q I s d p 3 - var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03]; - //MQTT proto/version for 311 4 M Q T T 4 - var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04]; - - /** - * Construct an MQTT wire protocol message. - * @param type MQTT packet type. - * @param options optional wire message attributes. - * - * Optional properties - * - * messageIdentifier: message ID in the range [0..65535] - * payloadMessage: Application Message - PUBLISH only - * connectStrings: array of 0 or more Strings to be put into the CONNECT payload - * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) - * requestQoS: array of QoS values [0..2] - * - * "Flag" properties - * cleanSession: true if present / false if absent (CONNECT) - * willMessage: true if present / false if absent (CONNECT) - * isRetained: true if present / false if absent (CONNECT) - * userName: true if present / false if absent (CONNECT) - * password: true if present / false if absent (CONNECT) - * keepAliveInterval: integer [0..65535] (CONNECT) - * - * @private - * @ignore - */ - var WireMessage = function (type, options) { - this.type = type; - for (var name in options) { - if (options.hasOwnProperty(name)) { - this[name] = options[name]; - } - } - }; - - WireMessage.prototype.encode = function() { - // Compute the first byte of the fixed header - var first = ((this.type & 0x0f) << 4); - - /* - * Now calculate the length of the variable header + payload by adding up the lengths - * of all the component parts - */ - - var remLength = 0; - var topicStrLength = new Array(); - var destinationNameLength = 0; - - // if the message contains a messageIdentifier then we need two bytes for that - if (this.messageIdentifier != undefined) - remLength += 2; - - switch(this.type) { - // If this a Connect then we need to include 12 bytes for its header - case MESSAGE_TYPE.CONNECT: - switch(this.mqttVersion) { - case 3: - remLength += MqttProtoIdentifierv3.length + 3; - break; - case 4: - remLength += MqttProtoIdentifierv4.length + 3; - break; - } - - remLength += UTF8Length(this.clientId) + 2; - if (this.willMessage != undefined) { - remLength += UTF8Length(this.willMessage.destinationName) + 2; - // Will message is always a string, sent as UTF-8 characters with a preceding length. - var willMessagePayloadBytes = this.willMessage.payloadBytes; - if (!(willMessagePayloadBytes instanceof Uint8Array)) - willMessagePayloadBytes = new Uint8Array(payloadBytes); - remLength += willMessagePayloadBytes.byteLength +2; - } - if (this.userName != undefined) - remLength += UTF8Length(this.userName) + 2; - if (this.password != undefined) - remLength += UTF8Length(this.password) + 2; - break; - - // Subscribe, Unsubscribe can both contain topic strings - case MESSAGE_TYPE.SUBSCRIBE: - first |= 0x02; // Qos = 1; - for ( var i = 0; i < this.topics.length; i++) { - topicStrLength[i] = UTF8Length(this.topics[i]); - remLength += topicStrLength[i] + 2; - } - remLength += this.requestedQos.length; // 1 byte for each topic's Qos - // QoS on Subscribe only - break; - - case MESSAGE_TYPE.UNSUBSCRIBE: - first |= 0x02; // Qos = 1; - for ( var i = 0; i < this.topics.length; i++) { - topicStrLength[i] = UTF8Length(this.topics[i]); - remLength += topicStrLength[i] + 2; - } - break; - - case MESSAGE_TYPE.PUBREL: - first |= 0x02; // Qos = 1; - break; - - case MESSAGE_TYPE.PUBLISH: - if (this.payloadMessage.duplicate) first |= 0x08; - first = first |= (this.payloadMessage.qos << 1); - if (this.payloadMessage.retained) first |= 0x01; - destinationNameLength = UTF8Length(this.payloadMessage.destinationName); - remLength += destinationNameLength + 2; - var payloadBytes = this.payloadMessage.payloadBytes; - remLength += payloadBytes.byteLength; - if (payloadBytes instanceof ArrayBuffer) - payloadBytes = new Uint8Array(payloadBytes); - else if (!(payloadBytes instanceof Uint8Array)) - payloadBytes = new Uint8Array(payloadBytes.buffer); - break; - - case MESSAGE_TYPE.DISCONNECT: - break; - - default: - ; - } - - // Now we can allocate a buffer for the message - - var mbi = encodeMBI(remLength); // Convert the length to MQTT MBI format - var pos = mbi.length + 1; // Offset of start of variable header - var buffer = new ArrayBuffer(remLength + pos); - var byteStream = new Uint8Array(buffer); // view it as a sequence of bytes - - //Write the fixed header into the buffer - byteStream[0] = first; - byteStream.set(mbi,1); - - // If this is a PUBLISH then the variable header starts with a topic - if (this.type == MESSAGE_TYPE.PUBLISH) - pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos); - // If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time - - else if (this.type == MESSAGE_TYPE.CONNECT) { - switch (this.mqttVersion) { - case 3: - byteStream.set(MqttProtoIdentifierv3, pos); - pos += MqttProtoIdentifierv3.length; - break; - case 4: - byteStream.set(MqttProtoIdentifierv4, pos); - pos += MqttProtoIdentifierv4.length; - break; - } - var connectFlags = 0; - if (this.cleanSession) - connectFlags = 0x02; - if (this.willMessage != undefined ) { - connectFlags |= 0x04; - connectFlags |= (this.willMessage.qos<<3); - if (this.willMessage.retained) { - connectFlags |= 0x20; - } - } - if (this.userName != undefined) - connectFlags |= 0x80; - if (this.password != undefined) - connectFlags |= 0x40; - byteStream[pos++] = connectFlags; - pos = writeUint16 (this.keepAliveInterval, byteStream, pos); - } - - // Output the messageIdentifier - if there is one - if (this.messageIdentifier != undefined) - pos = writeUint16 (this.messageIdentifier, byteStream, pos); - - switch(this.type) { - case MESSAGE_TYPE.CONNECT: - pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos); - if (this.willMessage != undefined) { - pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos); - pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos); - byteStream.set(willMessagePayloadBytes, pos); - pos += willMessagePayloadBytes.byteLength; - - } - if (this.userName != undefined) - pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos); - if (this.password != undefined) - pos = writeString(this.password, UTF8Length(this.password), byteStream, pos); - break; - - case MESSAGE_TYPE.PUBLISH: - // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters. - byteStream.set(payloadBytes, pos); - - break; - -// case MESSAGE_TYPE.PUBREC: -// case MESSAGE_TYPE.PUBREL: -// case MESSAGE_TYPE.PUBCOMP: -// break; - - case MESSAGE_TYPE.SUBSCRIBE: - // SUBSCRIBE has a list of topic strings and request QoS - for (var i=0; i> 4; - var messageInfo = first &= 0x0f; - pos += 1; - - - // Decode the remaining length (MBI format) - - var digit; - var remLength = 0; - var multiplier = 1; - do { - if (pos == input.length) { - return [null,startingPos]; - } - digit = input[pos++]; - remLength += ((digit & 0x7F) * multiplier); - multiplier *= 128; - } while ((digit & 0x80) != 0); - - var endPos = pos+remLength; - if (endPos > input.length) { - return [null,startingPos]; - } - - var wireMessage = new WireMessage(type); - switch(type) { - case MESSAGE_TYPE.CONNACK: - var connectAcknowledgeFlags = input[pos++]; - if (connectAcknowledgeFlags & 0x01) - wireMessage.sessionPresent = true; - wireMessage.returnCode = input[pos++]; - break; - - case MESSAGE_TYPE.PUBLISH: - var qos = (messageInfo >> 1) & 0x03; - - var len = readUint16(input, pos); - pos += 2; - var topicName = parseUTF8(input, pos, len); - pos += len; - // If QoS 1 or 2 there will be a messageIdentifier - if (qos > 0) { - wireMessage.messageIdentifier = readUint16(input, pos); - pos += 2; - } - - var message = new Paho.MQTT.Message(input.subarray(pos, endPos)); - if ((messageInfo & 0x01) == 0x01) - message.retained = true; - if ((messageInfo & 0x08) == 0x08) - message.duplicate = true; - message.qos = qos; - message.destinationName = topicName; - wireMessage.payloadMessage = message; - break; - - case MESSAGE_TYPE.PUBACK: - case MESSAGE_TYPE.PUBREC: - case MESSAGE_TYPE.PUBREL: - case MESSAGE_TYPE.PUBCOMP: - case MESSAGE_TYPE.UNSUBACK: - wireMessage.messageIdentifier = readUint16(input, pos); - break; - - case MESSAGE_TYPE.SUBACK: - wireMessage.messageIdentifier = readUint16(input, pos); - pos += 2; - wireMessage.returnCode = input.subarray(pos, endPos); - break; - - default: - ; - } - - return [wireMessage,endPos]; - } - - function writeUint16(input, buffer, offset) { - buffer[offset++] = input >> 8; //MSB - buffer[offset++] = input % 256; //LSB - return offset; - } - - function writeString(input, utf8Length, buffer, offset) { - offset = writeUint16(utf8Length, buffer, offset); - stringToUTF8(input, buffer, offset); - return offset + utf8Length; - } - - function readUint16(buffer, offset) { - return 256*buffer[offset] + buffer[offset+1]; - } - - /** - * Encodes an MQTT Multi-Byte Integer - * @private - */ - function encodeMBI(number) { - var output = new Array(1); - var numBytes = 0; - - do { - var digit = number % 128; - number = number >> 7; - if (number > 0) { - digit |= 0x80; - } - output[numBytes++] = digit; - } while ( (number > 0) && (numBytes<4) ); - - return output; - } - - /** - * Takes a String and calculates its length in bytes when encoded in UTF8. - * @private - */ - function UTF8Length(input) { - var output = 0; - for (var i = 0; i 0x7FF) - { - // Surrogate pair means its a 4 byte character - if (0xD800 <= charCode && charCode <= 0xDBFF) - { - i++; - output++; - } - output +=3; - } - else if (charCode > 0x7F) - output +=2; - else - output++; - } - return output; - } - - /** - * Takes a String and writes it into an array as UTF8 encoded bytes. - * @private - */ - function stringToUTF8(input, output, start) { - var pos = start; - for (var i = 0; i>6 & 0x1F | 0xC0; - output[pos++] = charCode & 0x3F | 0x80; - } else if (charCode <= 0xFFFF) { - output[pos++] = charCode>>12 & 0x0F | 0xE0; - output[pos++] = charCode>>6 & 0x3F | 0x80; - output[pos++] = charCode & 0x3F | 0x80; - } else { - output[pos++] = charCode>>18 & 0x07 | 0xF0; - output[pos++] = charCode>>12 & 0x3F | 0x80; - output[pos++] = charCode>>6 & 0x3F | 0x80; - output[pos++] = charCode & 0x3F | 0x80; - }; - } - return output; - } - - function parseUTF8(input, offset, length) { - var output = ""; - var utf16; - var pos = offset; - - while (pos < offset+length) - { - var byte1 = input[pos++]; - if (byte1 < 128) - utf16 = byte1; - else - { - var byte2 = input[pos++]-128; - if (byte2 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""])); - if (byte1 < 0xE0) // 2 byte character - utf16 = 64*(byte1-0xC0) + byte2; - else - { - var byte3 = input[pos++]-128; - if (byte3 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)])); - if (byte1 < 0xF0) // 3 byte character - utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3; - else - { - var byte4 = input[pos++]-128; - if (byte4 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); - if (byte1 < 0xF8) // 4 byte character - utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4; - else // longer encodings are not supported - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); - } - } - } - - if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair - { - utf16 -= 0x10000; - output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character - utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character - } - output += String.fromCharCode(utf16); - } - return output; - } - - /** - * Repeat keepalive requests, monitor responses. - * @ignore - */ - var Pinger = function(client, window, keepAliveInterval) { - this._client = client; - this._window = window; - this._keepAliveInterval = keepAliveInterval*1000; - this.isReset = false; - - var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode(); - - var doTimeout = function (pinger) { - return function () { - return doPing.apply(pinger); - }; - }; - - /** @ignore */ - var doPing = function() { - if (!this.isReset) { - this._client._trace("Pinger.doPing", "Timed out"); - this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT)); - } else { - this.isReset = false; - this._client._trace("Pinger.doPing", "send PINGREQ"); - this._client.socket.send(pingReq); - this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval); - } - } - - this.reset = function() { - this.isReset = true; - this._window.clearTimeout(this.timeout); - if (this._keepAliveInterval > 0) - this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); - } - - this.cancel = function() { - this._window.clearTimeout(this.timeout); - } - }; - - /** - * Monitor request completion. - * @ignore - */ - var Timeout = function(client, window, timeoutSeconds, action, args) { - this._window = window; - if (!timeoutSeconds) - timeoutSeconds = 30; - - var doTimeout = function (action, client, args) { - return function () { - return action.apply(client, args); - }; - }; - this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000); - - this.cancel = function() { - this._window.clearTimeout(this.timeout); - } - }; - - /* - * Internal implementation of the Websockets MQTT V3.1 client. - * - * @name Paho.MQTT.ClientImpl @constructor - * @param {String} host the DNS nameof the webSocket host. - * @param {Number} port the port number for that host. - * @param {String} clientId the MQ client identifier. - */ - var ClientImpl = function (uri, host, port, path, clientId) { - // Check dependencies are satisfied in this browser. - if (!("WebSocket" in global && global["WebSocket"] !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"])); - } - if (!("localStorage" in global && global["localStorage"] !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["localStorage"])); - } - if (!("ArrayBuffer" in global && global["ArrayBuffer"] !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"])); - } - this._trace("Paho.MQTT.Client", uri, host, port, path, clientId); - - this.host = host; - this.port = port; - this.path = path; - this.uri = uri; - this.clientId = clientId; - - // Local storagekeys are qualified with the following string. - // The conditional inclusion of path in the key is for backward - // compatibility to when the path was not configurable and assumed to - // be /mqtt - this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":"; - - // Create private instance-only message queue - // Internal queue of messages to be sent, in sending order. - this._msg_queue = []; - - // Messages we have sent and are expecting a response for, indexed by their respective message ids. - this._sentMessages = {}; - - // Messages we have received and acknowleged and are expecting a confirm message for - // indexed by their respective message ids. - this._receivedMessages = {}; - - // Internal list of callbacks to be executed when messages - // have been successfully sent over web socket, e.g. disconnect - // when it doesn't have to wait for ACK, just message is dispatched. - this._notify_msg_sent = {}; - - // Unique identifier for SEND messages, incrementing - // counter as messages are sent. - this._message_identifier = 1; - - // Used to determine the transmission sequence of stored sent messages. - this._sequence = 0; - - - // Load the local state, if any, from the saved version, only restore state relevant to this client. - for (var key in localStorage) - if ( key.indexOf("Sent:"+this._localKey) == 0 - || key.indexOf("Received:"+this._localKey) == 0) - this.restore(key); - }; - - // Messaging Client public instance members. - ClientImpl.prototype.host; - ClientImpl.prototype.port; - ClientImpl.prototype.path; - ClientImpl.prototype.uri; - ClientImpl.prototype.clientId; - - // Messaging Client private instance members. - ClientImpl.prototype.socket; - /* true once we have received an acknowledgement to a CONNECT packet. */ - ClientImpl.prototype.connected = false; - /* The largest message identifier allowed, may not be larger than 2**16 but - * if set smaller reduces the maximum number of outbound messages allowed. - */ - ClientImpl.prototype.maxMessageIdentifier = 65536; - ClientImpl.prototype.connectOptions; - ClientImpl.prototype.hostIndex; - ClientImpl.prototype.onConnectionLost; - ClientImpl.prototype.onMessageDelivered; - ClientImpl.prototype.onMessageArrived; - ClientImpl.prototype.traceFunction; - ClientImpl.prototype._msg_queue = null; - ClientImpl.prototype._connectTimeout; - /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */ - ClientImpl.prototype.sendPinger = null; - /* The receivePinger monitors how long we allow before we require evidence that the server is alive. */ - ClientImpl.prototype.receivePinger = null; - - ClientImpl.prototype.receiveBuffer = null; - - ClientImpl.prototype._traceBuffer = null; - ClientImpl.prototype._MAX_TRACE_ENTRIES = 100; - - ClientImpl.prototype.connect = function (connectOptions) { - var connectOptionsMasked = this._traceMask(connectOptions, "password"); - this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected); - - if (this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); - if (this.socket) - throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); - - this.connectOptions = connectOptions; - - if (connectOptions.uris) { - this.hostIndex = 0; - this._doConnect(connectOptions.uris[0]); - } else { - this._doConnect(this.uri); - } - - }; - - ClientImpl.prototype.subscribe = function (filter, subscribeOptions) { - this._trace("Client.subscribe", filter, subscribeOptions); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE); - wireMessage.topics=[filter]; - if (subscribeOptions.qos != undefined) - wireMessage.requestedQos = [subscribeOptions.qos]; - else - wireMessage.requestedQos = [0]; - - if (subscribeOptions.onSuccess) { - wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});}; - } - - if (subscribeOptions.onFailure) { - wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode});}; - } - - if (subscribeOptions.timeout) { - wireMessage.timeOut = new Timeout(this, window, subscribeOptions.timeout, subscribeOptions.onFailure - , [{invocationContext:subscribeOptions.invocationContext, - errorCode:ERROR.SUBSCRIBE_TIMEOUT.code, - errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]); - } - - // All subscriptions return a SUBACK. - this._requires_ack(wireMessage); - this._schedule_message(wireMessage); - }; - - /** @ignore */ - ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) { - this._trace("Client.unsubscribe", filter, unsubscribeOptions); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE); - wireMessage.topics = [filter]; - - if (unsubscribeOptions.onSuccess) { - wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});}; - } - if (unsubscribeOptions.timeout) { - wireMessage.timeOut = new Timeout(this, window, unsubscribeOptions.timeout, unsubscribeOptions.onFailure - , [{invocationContext:unsubscribeOptions.invocationContext, - errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code, - errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]); - } - - // All unsubscribes return a SUBACK. - this._requires_ack(wireMessage); - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.send = function (message) { - this._trace("Client.send", message); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH); - wireMessage.payloadMessage = message; - - if (message.qos > 0) - this._requires_ack(wireMessage); - else if (this.onMessageDelivered) - this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage); - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.disconnect = function () { - this._trace("Client.disconnect"); - - if (!this.socket) - throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"])); - - wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT); - - // Run the disconnected call back as soon as the message has been sent, - // in case of a failure later on in the disconnect processing. - // as a consequence, the _disconected call back may be run several times. - this._notify_msg_sent[wireMessage] = scope(this._disconnected, this); - - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.getTraceLog = function () { - if ( this._traceBuffer !== null ) { - this._trace("Client.getTraceLog", new Date()); - this._trace("Client.getTraceLog in flight messages", this._sentMessages.length); - for (var key in this._sentMessages) - this._trace("_sentMessages ",key, this._sentMessages[key]); - for (var key in this._receivedMessages) - this._trace("_receivedMessages ",key, this._receivedMessages[key]); - - return this._traceBuffer; - } - }; - - ClientImpl.prototype.startTrace = function () { - if ( this._traceBuffer === null ) { - this._traceBuffer = []; - } - this._trace("Client.startTrace", new Date(), version); - }; - - ClientImpl.prototype.stopTrace = function () { - delete this._traceBuffer; - }; - - ClientImpl.prototype._doConnect = function (wsurl) { - // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters. - if (this.connectOptions.useSSL) { - var uriParts = wsurl.split(":"); - uriParts[0] = "wss"; - wsurl = uriParts.join(":"); - } - this.connected = false; - if (this.connectOptions.mqttVersion < 4) { - this.socket = new WebSocket(wsurl, ["mqttv3.1"]); - } else { - this.socket = new WebSocket(wsurl, ["mqtt"]); - } - this.socket.binaryType = 'arraybuffer'; - - this.socket.onopen = scope(this._on_socket_open, this); - this.socket.onmessage = scope(this._on_socket_message, this); - this.socket.onerror = scope(this._on_socket_error, this); - this.socket.onclose = scope(this._on_socket_close, this); - - this.sendPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); - this.receivePinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); - - this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]); - }; - - - // Schedule a new message to be sent over the WebSockets - // connection. CONNECT messages cause WebSocket connection - // to be started. All other messages are queued internally - // until this has happened. When WS connection starts, process - // all outstanding messages. - ClientImpl.prototype._schedule_message = function (message) { - this._msg_queue.push(message); - // Process outstanding messages in the queue if we have an open socket, and have received CONNACK. - if (this.connected) { - this._process_queue(); - } - }; - - ClientImpl.prototype.store = function(prefix, wireMessage) { - var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1}; - - switch(wireMessage.type) { - case MESSAGE_TYPE.PUBLISH: - if(wireMessage.pubRecReceived) - storedMessage.pubRecReceived = true; - - // Convert the payload to a hex string. - storedMessage.payloadMessage = {}; - var hex = ""; - var messageBytes = wireMessage.payloadMessage.payloadBytes; - for (var i=0; i= 2) { - var x = parseInt(hex.substring(0, 2), 16); - hex = hex.substring(2, hex.length); - byteStream[i++] = x; - } - var payloadMessage = new Paho.MQTT.Message(byteStream); - - payloadMessage.qos = storedMessage.payloadMessage.qos; - payloadMessage.destinationName = storedMessage.payloadMessage.destinationName; - if (storedMessage.payloadMessage.duplicate) - payloadMessage.duplicate = true; - if (storedMessage.payloadMessage.retained) - payloadMessage.retained = true; - wireMessage.payloadMessage = payloadMessage; - - break; - - default: - throw Error(format(ERROR.INVALID_STORED_DATA, [key, value])); - } - - if (key.indexOf("Sent:"+this._localKey) == 0) { - wireMessage.payloadMessage.duplicate = true; - this._sentMessages[wireMessage.messageIdentifier] = wireMessage; - } else if (key.indexOf("Received:"+this._localKey) == 0) { - this._receivedMessages[wireMessage.messageIdentifier] = wireMessage; - } - }; - - ClientImpl.prototype._process_queue = function () { - var message = null; - // Process messages in order they were added - var fifo = this._msg_queue.reverse(); - - // Send all queued messages down socket connection - while ((message = fifo.pop())) { - this._socket_send(message); - // Notify listeners that message was successfully sent - if (this._notify_msg_sent[message]) { - this._notify_msg_sent[message](); - delete this._notify_msg_sent[message]; - } - } - }; - - /** - * Expect an ACK response for this message. Add message to the set of in progress - * messages and set an unused identifier in this message. - * @ignore - */ - ClientImpl.prototype._requires_ack = function (wireMessage) { - var messageCount = Object.keys(this._sentMessages).length; - if (messageCount > this.maxMessageIdentifier) - throw Error ("Too many messages:"+messageCount); - - while(this._sentMessages[this._message_identifier] !== undefined) { - this._message_identifier++; - } - wireMessage.messageIdentifier = this._message_identifier; - this._sentMessages[wireMessage.messageIdentifier] = wireMessage; - if (wireMessage.type === MESSAGE_TYPE.PUBLISH) { - this.store("Sent:", wireMessage); - } - if (this._message_identifier === this.maxMessageIdentifier) { - this._message_identifier = 1; - } - }; - - /** - * Called when the underlying websocket has been opened. - * @ignore - */ - ClientImpl.prototype._on_socket_open = function () { - // Create the CONNECT message object. - var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions); - wireMessage.clientId = this.clientId; - this._socket_send(wireMessage); - }; - - /** - * Called when the underlying websocket has received a complete packet. - * @ignore - */ - ClientImpl.prototype._on_socket_message = function (event) { - this._trace("Client._on_socket_message", event.data); - // Reset the receive ping timer, we now have evidence the server is alive. - this.receivePinger.reset(); - var messages = this._deframeMessages(event.data); - for (var i = 0; i < messages.length; i+=1) { - this._handleMessage(messages[i]); - } - } - - ClientImpl.prototype._deframeMessages = function(data) { - var byteArray = new Uint8Array(data); - if (this.receiveBuffer) { - var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length); - newData.set(this.receiveBuffer); - newData.set(byteArray,this.receiveBuffer.length); - byteArray = newData; - delete this.receiveBuffer; - } - try { - var offset = 0; - var messages = []; - while(offset < byteArray.length) { - var result = decodeMessage(byteArray,offset); - var wireMessage = result[0]; - offset = result[1]; - if (wireMessage !== null) { - messages.push(wireMessage); - } else { - break; - } - } - if (offset < byteArray.length) { - this.receiveBuffer = byteArray.subarray(offset); - } - } catch (error) { - this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,error.stack.toString()])); - return; - } - return messages; - } - - ClientImpl.prototype._handleMessage = function(wireMessage) { - - this._trace("Client._handleMessage", wireMessage); - - try { - switch(wireMessage.type) { - case MESSAGE_TYPE.CONNACK: - this._connectTimeout.cancel(); - - // If we have started using clean session then clear up the local state. - if (this.connectOptions.cleanSession) { - for (var key in this._sentMessages) { - var sentMessage = this._sentMessages[key]; - localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier); - } - this._sentMessages = {}; - - for (var key in this._receivedMessages) { - var receivedMessage = this._receivedMessages[key]; - localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier); - } - this._receivedMessages = {}; - } - // Client connected and ready for business. - if (wireMessage.returnCode === 0) { - this.connected = true; - // Jump to the end of the list of uris and stop looking for a good host. - if (this.connectOptions.uris) - this.hostIndex = this.connectOptions.uris.length; - } else { - this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]])); - break; - } - - // Resend messages. - var sequencedMessages = new Array(); - for (var msgId in this._sentMessages) { - if (this._sentMessages.hasOwnProperty(msgId)) - sequencedMessages.push(this._sentMessages[msgId]); - } - - // Sort sentMessages into the original sent order. - var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} ); - for (var i=0, len=sequencedMessages.length; i - * Most applications will create just one Client object and then call its connect() method, - * however applications can create more than one Client object if they wish. - * In this case the combination of host, port and clientId attributes must be different for each Client object. - *

- * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods - * (even though the underlying protocol exchange might be synchronous in nature). - * This means they signal their completion by calling back to the application, - * via Success or Failure callback functions provided by the application on the method in question. - * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime - * of the script that made the invocation. - *

- * In contrast there are some callback functions, most notably onMessageArrived, - * that are defined on the {@link Paho.MQTT.Client} object. - * These may get called multiple times, and aren't directly related to specific method invocations made by the client. - * - * @name Paho.MQTT.Client - * - * @constructor - * - * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address. - * @param {number} port - the port number to connect to - only required if host is not a URI - * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'. - * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length. - * - * @property {string} host - read only the server's DNS hostname or dotted decimal IP address. - * @property {number} port - read only the server's port. - * @property {string} path - read only the server's path. - * @property {string} clientId - read only used when connecting to the server. - * @property {function} onConnectionLost - called when a connection has been lost. - * after a connect() method has succeeded. - * Establish the call back used when a connection has been lost. The connection may be - * lost because the client initiates a disconnect or because the server or network - * cause the client to be disconnected. The disconnect call back may be called without - * the connectionComplete call back being invoked if, for example the client fails to - * connect. - * A single response object parameter is passed to the onConnectionLost callback containing the following fields: - *

    - *
  1. errorCode - *
  2. errorMessage - *
- * @property {function} onMessageDelivered called when a message has been delivered. - * All processing that this Client will ever do has been completed. So, for example, - * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server - * and the message has been removed from persistent storage before this callback is invoked. - * Parameters passed to the onMessageDelivered callback are: - *
    - *
  1. {@link Paho.MQTT.Message} that was delivered. - *
- * @property {function} onMessageArrived called when a message has arrived in this Paho.MQTT.client. - * Parameters passed to the onMessageArrived callback are: - *
    - *
  1. {@link Paho.MQTT.Message} that has arrived. - *
- */ - var Client = function (host, port, path, clientId) { - - var uri; - - if (typeof host !== "string") - throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"])); - - if (arguments.length == 2) { - // host: must be full ws:// uri - // port: clientId - clientId = port; - uri = host; - var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/); - if (match) { - host = match[4]||match[2]; - port = parseInt(match[7]); - path = match[8]; - } else { - throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"])); - } - } else { - if (arguments.length == 3) { - clientId = path; - path = "/mqtt"; - } - if (typeof port !== "number" || port < 0) - throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"])); - if (typeof path !== "string") - throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"])); - - var ipv6AddSBracket = (host.indexOf(":") != -1 && host.slice(0,1) != "[" && host.slice(-1) != "]"); - uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path; - } - - var clientIdLength = 0; - for (var i = 0; i 65535) - throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); - - var client = new ClientImpl(uri, host, port, path, clientId); - this._getHost = function() { return host; }; - this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getPort = function() { return port; }; - this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getPath = function() { return path; }; - this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getURI = function() { return uri; }; - this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getClientId = function() { return client.clientId; }; - this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getOnConnectionLost = function() { return client.onConnectionLost; }; - this._setOnConnectionLost = function(newOnConnectionLost) { - if (typeof newOnConnectionLost === "function") - client.onConnectionLost = newOnConnectionLost; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"])); - }; - - this._getOnMessageDelivered = function() { return client.onMessageDelivered; }; - this._setOnMessageDelivered = function(newOnMessageDelivered) { - if (typeof newOnMessageDelivered === "function") - client.onMessageDelivered = newOnMessageDelivered; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"])); - }; - - this._getOnMessageArrived = function() { return client.onMessageArrived; }; - this._setOnMessageArrived = function(newOnMessageArrived) { - if (typeof newOnMessageArrived === "function") - client.onMessageArrived = newOnMessageArrived; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"])); - }; - - this._getTrace = function() { return client.traceFunction; }; - this._setTrace = function(trace) { - if(typeof trace === "function"){ - client.traceFunction = trace; - }else{ - throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"])); - } - }; - - /** - * Connect this Messaging client to its server. - * - * @name Paho.MQTT.Client#connect - * @function - * @param {Object} connectOptions - attributes used with the connection. - * @param {number} connectOptions.timeout - If the connect has not succeeded within this - * number of seconds, it is deemed to have failed. - * The default is 30 seconds. - * @param {string} connectOptions.userName - Authentication username for this connection. - * @param {string} connectOptions.password - Authentication password for this connection. - * @param {Paho.MQTT.Message} connectOptions.willMessage - sent by the server when the client - * disconnects abnormally. - * @param {Number} connectOptions.keepAliveInterval - the server disconnects this client if - * there is no activity for this number of seconds. - * The default value of 60 seconds is assumed if not set. - * @param {boolean} connectOptions.cleanSession - if true(default) the client and server - * persistent state is deleted on successful connect. - * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection. - * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback. - * @param {function} connectOptions.onSuccess - called when the connect acknowledgement - * has been received from the server. - * A single response object parameter is passed to the onSuccess callback containing the following fields: - *
    - *
  1. invocationContext as passed in to the onSuccess method in the connectOptions. - *
- * @config {function} [onFailure] called when the connect request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext as passed in to the onFailure method in the connectOptions. - *
  2. errorCode a number indicating the nature of the error. - *
  3. errorMessage text describing the error. - *
- * @config {Array} [hosts] If present this contains either a set of hostnames or fully qualified - * WebSocket URIs (ws://example.com:1883/mqtt), that are tried in order in place - * of the host and port paramater on the construtor. The hosts are tried one at at time in order until - * one of then succeeds. - * @config {Array} [ports] If present the set of ports matching the hosts. If hosts contains URIs, this property - * is not used. - * @throws {InvalidState} if the client is not in disconnected state. The client must have received connectionLost - * or disconnected before calling connect for a second or subsequent time. - */ - this.connect = function (connectOptions) { - connectOptions = connectOptions || {} ; - validate(connectOptions, {timeout:"number", - userName:"string", - password:"string", - willMessage:"object", - keepAliveInterval:"number", - cleanSession:"boolean", - useSSL:"boolean", - invocationContext:"object", - onSuccess:"function", - onFailure:"function", - hosts:"object", - ports:"object", - mqttVersion:"number"}); - - // If no keep alive interval is set, assume 60 seconds. - if (connectOptions.keepAliveInterval === undefined) - connectOptions.keepAliveInterval = 60; - - if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) { - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"])); - } - - if (connectOptions.mqttVersion === undefined) { - connectOptions.mqttVersionExplicit = false; - connectOptions.mqttVersion = 4; - } else { - connectOptions.mqttVersionExplicit = true; - } - - //Check that if password is set, so is username - if (connectOptions.password === undefined && connectOptions.userName !== undefined) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"])) - - if (connectOptions.willMessage) { - if (!(connectOptions.willMessage instanceof Message)) - throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"])); - // The will message must have a payload that can be represented as a string. - // Cause the willMessage to throw an exception if this is not the case. - connectOptions.willMessage.stringPayload; - - if (typeof connectOptions.willMessage.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"])); - } - if (typeof connectOptions.cleanSession === "undefined") - connectOptions.cleanSession = true; - if (connectOptions.hosts) { - - if (!(connectOptions.hosts instanceof Array) ) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); - if (connectOptions.hosts.length <1 ) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); - - var usingURIs = false; - for (var i = 0; i - * @param {object} subscribeOptions - used to control the subscription - * - * @param {number} subscribeOptions.qos - the maiximum qos of any publications sent - * as a result of making this subscription. - * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback - * or onFailure callback. - * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement - * has been received from the server. - * A single response object parameter is passed to the onSuccess callback containing the following fields: - *
    - *
  1. invocationContext if set in the subscribeOptions. - *
- * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext - if set in the subscribeOptions. - *
  2. errorCode - a number indicating the nature of the error. - *
  3. errorMessage - text describing the error. - *
- * @param {number} subscribeOptions.timeout - which, if present, determines the number of - * seconds after which the onFailure calback is called. - * The presence of a timeout does not prevent the onSuccess - * callback from being called when the subscribe completes. - * @throws {InvalidState} if the client is not in connected state. - */ - this.subscribe = function (filter, subscribeOptions) { - if (typeof filter !== "string") - throw new Error("Invalid argument:"+filter); - subscribeOptions = subscribeOptions || {} ; - validate(subscribeOptions, {qos:"number", - invocationContext:"object", - onSuccess:"function", - onFailure:"function", - timeout:"number" - }); - if (subscribeOptions.timeout && !subscribeOptions.onFailure) - throw new Error("subscribeOptions.timeout specified with no onFailure callback."); - if (typeof subscribeOptions.qos !== "undefined" - && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 )) - throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"])); - client.subscribe(filter, subscribeOptions); - }; - - /** - * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. - * - * @name Paho.MQTT.Client#unsubscribe - * @function - * @param {string} filter - describing the destinations to receive messages from. - * @param {object} unsubscribeOptions - used to control the subscription - * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback - or onFailure callback. - * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server. - * A single response object parameter is passed to the - * onSuccess callback containing the following fields: - *
    - *
  1. invocationContext - if set in the unsubscribeOptions. - *
- * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext - if set in the unsubscribeOptions. - *
  2. errorCode - a number indicating the nature of the error. - *
  3. errorMessage - text describing the error. - *
- * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds - * after which the onFailure callback is called. The presence of - * a timeout does not prevent the onSuccess callback from being - * called when the unsubscribe completes - * @throws {InvalidState} if the client is not in connected state. - */ - this.unsubscribe = function (filter, unsubscribeOptions) { - if (typeof filter !== "string") - throw new Error("Invalid argument:"+filter); - unsubscribeOptions = unsubscribeOptions || {} ; - validate(unsubscribeOptions, {invocationContext:"object", - onSuccess:"function", - onFailure:"function", - timeout:"number" - }); - if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure) - throw new Error("unsubscribeOptions.timeout specified with no onFailure callback."); - client.unsubscribe(filter, unsubscribeOptions); - }; - - /** - * Send a message to the consumers of the destination in the Message. - * - * @name Paho.MQTT.Client#send - * @function - * @param {string|Paho.MQTT.Message} topic - mandatory The name of the destination to which the message is to be sent. - * - If it is the only parameter, used as Paho.MQTT.Message object. - * @param {String|ArrayBuffer} payload - The message data to be sent. - * @param {number} qos The Quality of Service used to deliver the message. - *
- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- * @param {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - * @throws {InvalidState} if the client is not connected. - */ - this.send = function (topic,payload,qos,retained) { - var message ; - - if(arguments.length == 0){ - throw new Error("Invalid argument."+"length"); - - }else if(arguments.length == 1) { - - if (!(topic instanceof Message) && (typeof topic !== "string")) - throw new Error("Invalid argument:"+ typeof topic); - - message = topic; - if (typeof message.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); - client.send(message); - - }else { - //parameter checking in Message object - message = new Message(payload); - message.destinationName = topic; - if(arguments.length >= 3) - message.qos = qos; - if(arguments.length >= 4) - message.retained = retained; - client.send(message); - } - }; - - /** - * Normal disconnect of this Messaging client from its server. - * - * @name Paho.MQTT.Client#disconnect - * @function - * @throws {InvalidState} if the client is already disconnected. - */ - this.disconnect = function () { - client.disconnect(); - }; - - /** - * Get the contents of the trace log. - * - * @name Paho.MQTT.Client#getTraceLog - * @function - * @return {Object[]} tracebuffer containing the time ordered trace records. - */ - this.getTraceLog = function () { - return client.getTraceLog(); - } - - /** - * Start tracing. - * - * @name Paho.MQTT.Client#startTrace - * @function - */ - this.startTrace = function () { - client.startTrace(); - }; - - /** - * Stop tracing. - * - * @name Paho.MQTT.Client#stopTrace - * @function - */ - this.stopTrace = function () { - client.stopTrace(); - }; - - this.isConnected = function() { - return client.connected; - }; - }; - - Client.prototype = { - get host() { return this._getHost(); }, - set host(newHost) { this._setHost(newHost); }, - - get port() { return this._getPort(); }, - set port(newPort) { this._setPort(newPort); }, - - get path() { return this._getPath(); }, - set path(newPath) { this._setPath(newPath); }, - - get clientId() { return this._getClientId(); }, - set clientId(newClientId) { this._setClientId(newClientId); }, - - get onConnectionLost() { return this._getOnConnectionLost(); }, - set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); }, - - get onMessageDelivered() { return this._getOnMessageDelivered(); }, - set onMessageDelivered(newOnMessageDelivered) { this._setOnMessageDelivered(newOnMessageDelivered); }, - - get onMessageArrived() { return this._getOnMessageArrived(); }, - set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); }, - - get trace() { return this._getTrace(); }, - set trace(newTraceFunction) { this._setTrace(newTraceFunction); } - - }; - - /** - * An application message, sent or received. - *

- * All attributes may be null, which implies the default values. - * - * @name Paho.MQTT.Message - * @constructor - * @param {String|ArrayBuffer} payload The message data to be sent. - *

- * @property {string} payloadString read only The payload as a string if the payload consists of valid UTF-8 characters. - * @property {ArrayBuffer} payloadBytes read only The payload as an ArrayBuffer. - *

- * @property {string} destinationName mandatory The name of the destination to which the message is to be sent - * (for messages about to be sent) or the name of the destination from which the message has been received. - * (for messages received by the onMessage function). - *

- * @property {number} qos The Quality of Service used to deliver the message. - *

- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- *

- * @property {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - *

- * @property {Boolean} duplicate read only If true, this message might be a duplicate of one which has already been received. - * This is only set on messages received from the server. - * - */ - var Message = function (newPayload) { - var payload; - if ( typeof newPayload === "string" - || newPayload instanceof ArrayBuffer - || newPayload instanceof Int8Array - || newPayload instanceof Uint8Array - || newPayload instanceof Int16Array - || newPayload instanceof Uint16Array - || newPayload instanceof Int32Array - || newPayload instanceof Uint32Array - || newPayload instanceof Float32Array - || newPayload instanceof Float64Array - ) { - payload = newPayload; - } else { - throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"])); - } - - this._getPayloadString = function () { - if (typeof payload === "string") - return payload; - else - return parseUTF8(payload, 0, payload.length); - }; - - this._getPayloadBytes = function() { - if (typeof payload === "string") { - var buffer = new ArrayBuffer(UTF8Length(payload)); - var byteStream = new Uint8Array(buffer); - stringToUTF8(payload, byteStream, 0); - - return byteStream; - } else { - return payload; - }; - }; - - var destinationName = undefined; - this._getDestinationName = function() { return destinationName; }; - this._setDestinationName = function(newDestinationName) { - if (typeof newDestinationName === "string") - destinationName = newDestinationName; - else - throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"])); - }; - - var qos = 0; - this._getQos = function() { return qos; }; - this._setQos = function(newQos) { - if (newQos === 0 || newQos === 1 || newQos === 2 ) - qos = newQos; - else - throw new Error("Invalid argument:"+newQos); - }; - - var retained = false; - this._getRetained = function() { return retained; }; - this._setRetained = function(newRetained) { - if (typeof newRetained === "boolean") - retained = newRetained; - else - throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"])); - }; - - var duplicate = false; - this._getDuplicate = function() { return duplicate; }; - this._setDuplicate = function(newDuplicate) { duplicate = newDuplicate; }; - }; - - Message.prototype = { - get payloadString() { return this._getPayloadString(); }, - get payloadBytes() { return this._getPayloadBytes(); }, - - get destinationName() { return this._getDestinationName(); }, - set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); }, - - get qos() { return this._getQos(); }, - set qos(newQos) { this._setQos(newQos); }, - - get retained() { return this._getRetained(); }, - set retained(newRetained) { this._setRetained(newRetained); }, - - get duplicate() { return this._getDuplicate(); }, - set duplicate(newDuplicate) { this._setDuplicate(newDuplicate); } - }; - - // Module contents. - return { - Client: Client, - Message: Message - }; -})(window); diff --git a/priv/www/websocket.html b/priv/www/websocket.html deleted file mode 100644 index eb7784226..000000000 --- a/priv/www/websocket.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - MQTT Over Mochiweb websocket - - -

MQTT Over Mochiweb websocket

- -
- -   State: -
-
Protip: open your javascript error console, just in case..
-
-
-
- - -
-
-
-
- - - - - From 38ab245b1546d83ca2241263f572b3f7534f2cbb Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 10:29:19 +0800 Subject: [PATCH 087/104] placeholder --- TODO | 71 -------------------------------------------- plugins/.placeholder | 0 2 files changed, 71 deletions(-) delete mode 100644 TODO create mode 100644 plugins/.placeholder diff --git a/TODO b/TODO deleted file mode 100644 index 017073616..000000000 --- a/TODO +++ /dev/null @@ -1,71 +0,0 @@ - - -v0.9.0-alpha (2015-05-30) -------------------------- - -Redis - -MySQL - -v0.9.0-alpha (2015-05-30) -------------------------- - -Presence Management.... - -Dashboard - -Presence Management.... - - -v0.8.0-alpha (2015-05-10) -------------------------- - -Force Subscriptions... - -Documents... - -MySQL Auth - -AMQP - -Bridge Test - - -0.8.0 (2015-05-10) -------------------------- - -Force Subscription... - -Point2Point Queue... - -MySQL Auth - -LDAP Auth - -PG Auth - -MySQL ACL - -Retained Message... - -Tsung MQTT Test - - -0.7.1-alpha (2015-05-05) -------------------------- - -Bugfix - -Admin Dashboard - -one million connections test... - -topic match benchmark tests... - -Dialyzer ... - -full test cases... - -README.md: add "Supports and Contact" include IRC, mailling-list, email. - -Document diff --git a/plugins/.placeholder b/plugins/.placeholder new file mode 100644 index 000000000..e69de29bb From 47d204f39be3f833c8e8d383b450beb5f379cbfe Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 10:36:36 +0800 Subject: [PATCH 088/104] README --- plugins/.placeholder | 0 plugins/README | 9 +++++++++ 2 files changed, 9 insertions(+) delete mode 100644 plugins/.placeholder create mode 100644 plugins/README diff --git a/plugins/.placeholder b/plugins/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/README b/plugins/README new file mode 100644 index 000000000..92fef71f9 --- /dev/null +++ b/plugins/README @@ -0,0 +1,9 @@ + +## Plugin Submodule Folder + +emqttd_dashboard for example: + +``` +git submodule add https://github.com/emqtt/emqttd_dashboard.git plugins/emqttd_dashboard +``` + From 4049bffa639d9e9053a2426c3a6d3c4e87b6a567 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 11:20:25 +0800 Subject: [PATCH 089/104] fix compile warning --- plugins/README | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 plugins/README diff --git a/plugins/README b/plugins/README deleted file mode 100644 index 92fef71f9..000000000 --- a/plugins/README +++ /dev/null @@ -1,9 +0,0 @@ - -## Plugin Submodule Folder - -emqttd_dashboard for example: - -``` -git submodule add https://github.com/emqtt/emqttd_dashboard.git plugins/emqttd_dashboard -``` - From 8b7304c0dd09369ab92d5b8fa5df4215fe48f5c0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 11:29:58 +0800 Subject: [PATCH 090/104] fix contributors --- CONTRIBUTORS | 10 ---------- README.md | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 CONTRIBUTORS diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 8f1f4c344..000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,10 +0,0 @@ - -# CONTRIBUTORS - -* [@callbay](https://github.com/callbay) -* [@hejin1026](https://github.com/hejin1026) -* [@desoulter](https://github.com/desoulter) -* [@turtleDeng](https://github.com/turtleDeng) -* [@Hades32](https://github.com/Hades32) -* [@huangdan](https://github.com/huangdan) - diff --git a/README.md b/README.md index 6ab9acaba..566961f8f 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,12 @@ The MIT License (MIT) ## Contributors +* [@callbay](https://github.com/callbay) * [@hejin1026](https://github.com/hejin1026) * [@desoulter](https://github.com/desoulter) * [@turtleDeng](https://github.com/turtleDeng) * [@Hades32](https://github.com/Hades32) * [@huangdan](https://github.com/huangdan) -* [@callbay](https://github.com/callbay) ## Author From 3e5675cc71bf47100e8979e30275fb81afd459d6 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 12:20:41 +0800 Subject: [PATCH 091/104] ipaddress --- src/emqttd_access_control.erl | 11 ++-- test/emqttd_access_rule_tests.erl | 4 +- test/emqttd_acl_tests.erl | 88 ------------------------------- 3 files changed, 10 insertions(+), 93 deletions(-) delete mode 100644 test/emqttd_acl_tests.erl diff --git a/src/emqttd_access_control.erl b/src/emqttd_access_control.erl index 30efa0ad5..82ac2552d 100644 --- a/src/emqttd_access_control.erl +++ b/src/emqttd_access_control.erl @@ -36,6 +36,7 @@ %% API Function Exports -export([start_link/0, + start_link/1, auth/2, % authentication check_acl/3, % acl check reload_acl/0, % reload acl @@ -60,7 +61,12 @@ %%------------------------------------------------------------------------------ -spec start_link() -> {ok, pid()} | ignore | {error, any()}. start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + {ok, AcOpts} = application:get_env(emqttd, access), + start_link(AcOpts). + +-spec start_link(AcOpts :: list()) -> {ok, pid()} | ignore | {error, any()}. +start_link(AcOpts) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [AcOpts], []). %%------------------------------------------------------------------------------ %% @doc Authenticate MQTT Client @@ -151,8 +157,7 @@ stop() -> %%% gen_server callbacks %%%============================================================================= -init([]) -> - {ok, AcOpts} = application:get_env(emqttd, access), +init([AcOpts]) -> 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, AcOpts))}), ets:insert(?ACCESS_CONTROL_TAB, {acl_modules, init_mods(acl, proplists:get_value(acl, AcOpts))}), diff --git a/test/emqttd_access_rule_tests.erl b/test/emqttd_access_rule_tests.erl index e0a5427df..8f25a787f 100644 --- a/test/emqttd_access_rule_tests.erl +++ b/test/emqttd_access_rule_tests.erl @@ -53,8 +53,8 @@ compile_test() -> ?assertEqual({deny, all}, compile({deny, all})). match_test() -> - User = #mqtt_client{ipaddr = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, - User2 = #mqtt_client{ipaddr = {192,168,0,10}, clientid = <<"testClient">>, username = <<"TestUser">>}, + User = #mqtt_client{ipaddress = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, + User2 = #mqtt_client{ipaddress = {192,168,0,10}, clientid = <<"testClient">>, username = <<"TestUser">>}, ?assertEqual({matched, allow}, match(User, <<"Test/Topic">>, {allow, all})), ?assertEqual({matched, deny}, match(User, <<"Test/Topic">>, {deny, all})), diff --git a/test/emqttd_acl_tests.erl b/test/emqttd_acl_tests.erl deleted file mode 100644 index ba21ecda4..000000000 --- a/test/emqttd_acl_tests.erl +++ /dev/null @@ -1,88 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd_acl tests. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_acl_tests). - --include("emqttd.hrl"). - --ifdef(TEST). - --include_lib("eunit/include/eunit.hrl"). - -all_modules_test() -> - with_acl( - fun() -> - ?assertMatch([{emqttd_acl_internal, _State}], emqttd_acl:all_modules()) - end). - -reload_test() -> - with_acl( - fun() -> - ?assertEqual([ok], emqttd_acl:reload()) - end). - -register_mod_test() -> - with_acl( - fun() -> - emqttd_acl:register_mod(emqttd_acl_test_mod, []), - ?assertMatch([{emqttd_acl_test_mod, _}, {emqttd_acl_internal, _}], - emqttd_acl:all_modules()) - end). - -unregister_mod_test() -> - with_acl( - fun() -> - emqttd_acl:register_mod(emqttd_acl_test_mod, []), - ?assertMatch([{emqttd_acl_test_mod, _}, {emqttd_acl_internal, _}], - emqttd_acl:all_modules()), - emqttd_acl:unregister_mod(emqttd_acl_test_mod), - timer:sleep(5), - ?assertMatch([{emqttd_acl_internal, _}], emqttd_acl:all_modules()) - end). - -check_test() -> - with_acl( - fun() -> - User1 = #mqtt_user{clientid = <<"client1">>, username = <<"testuser">>}, - User2 = #mqtt_user{clientid = <<"client2">>, username = <<"xyz">>}, - ?assertEqual(allow, emqttd_acl:check({User1, subscribe, <<"users/testuser/1">>})), - ?assertEqual(allow, emqttd_acl:check({User1, subscribe, <<"clients/client1">>})), - ?assertEqual(deny, emqttd_acl:check({User1, subscribe, <<"clients/client1/x/y">>})), - ?assertEqual(allow, emqttd_acl:check({User1, publish, <<"users/testuser/1">>})), - ?assertEqual(allow, emqttd_acl:check({User1, subscribe, <<"a/b/c">>})), - ?assertEqual(deny, emqttd_acl:check({User2, subscribe, <<"a/b/c">>})) - end). - -with_acl(Fun) -> - process_flag(trap_exit, true), - AclOpts = [{internal, [{file, "../test/test_acl.config"}, - {nomatch, allow}]}], - {ok, _AclSrv} = emqttd_acl:start_link(AclOpts), - Fun(), - emqttd_acl:stop(). - --endif. - From 7e97e39bd1660c7bb91ac234482b5b0c140e7d27 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 12:46:51 +0800 Subject: [PATCH 092/104] etc/plugins.config --- src/emqttd.erl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/emqttd.erl b/src/emqttd.erl index 30faa3e2d..ce5f1faa5 100644 --- a/src/emqttd.erl +++ b/src/emqttd.erl @@ -114,9 +114,15 @@ close_listener({Protocol, Port, _Options}) -> -spec load_all_plugins() -> [{App :: atom(), ok | {error, any()}}]. load_all_plugins() -> %% save first - {ok, [PluginApps]} = file:consult("etc/plugins.config"), - application:set_env(emqttd, plugins, [App || {App, _Env} <- PluginApps]), - [{App, load_plugin(App)} || {App, _Env} <- PluginApps]. + case file:consult("etc/plugins.config") of + {ok, [PluginApps]} -> + application:set_env(emqttd, plugins, [App || {App, _Env} <- PluginApps]), + [{App, load_plugin(App)} || {App, _Env} <- PluginApps]; + {error, enoent} -> + lager:error("etc/plugins.config not found!"); + {error, Error} -> + lager:error("Load etc/plugins.config error: ~p", [Error]) + end. %%------------------------------------------------------------------------------ %% @doc Load plugin @@ -169,7 +175,6 @@ unload_all_plugins() -> PluginApps = application:get_env(emqttd, plugins, []), [{App, unload_plugin(App)} || App <- PluginApps]. - %%------------------------------------------------------------------------------ %% @doc Unload plugin %% @end From be52625d596a7de7377f9f4f16a9c40f0388df84 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Tue, 23 Jun 2015 12:47:12 +0800 Subject: [PATCH 093/104] AclOpts --- test/emqttd_access_control_tests.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/emqttd_access_control_tests.erl b/test/emqttd_access_control_tests.erl index 5c906804e..6b55a0fae 100644 --- a/test/emqttd_access_control_tests.erl +++ b/test/emqttd_access_control_tests.erl @@ -98,8 +98,8 @@ with_acl(Fun) -> {internal, [{file, "../test/test_acl.config"}, {nomatch, allow}]} ]} ], - application:set_env(emqttd, access, AclOpts), - emqttd_access_control:start_link(), + %application:set_env(emqttd, access, AclOpts), + emqttd_access_control:start_link(AclOpts), Fun(), emqttd_access_control:stop(). From 2ff78d7fc6a8c60246b0124589e5b971fcb45b5a Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 00:37:02 +0800 Subject: [PATCH 094/104] alarm --- include/emqttd.hrl | 17 ++++++++-- src/emqttd_alarm.erl | 75 ++++++++++++++++++++++++++++++++---------- src/emqttd_mqueue.erl | 45 +++++++++++++------------ src/emqttd_session.erl | 2 +- 4 files changed, 97 insertions(+), 42 deletions(-) diff --git a/include/emqttd.hrl b/include/emqttd.hrl index 34aa3b4bd..bf76cb389 100644 --- a/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -111,14 +111,15 @@ -type mqtt_msgid() :: undefined | 1..16#ffff. -record(mqtt_message, { - topic :: binary(), %% The topic published to + topic :: binary(), %% Topic that the message is published to from :: binary() | atom(), %% ClientId of publisher qos = 0 :: 0 | 1 | 2, %% Message QoS retain = false :: boolean(), %% Retain flag dup = false :: boolean(), %% Dup flag sys = false :: boolean(), %% $SYS flag msgid :: mqtt_msgid(), %% Message ID - payload :: binary() %% Payload + payload :: binary(), %% Payload + timestamp :: erlang:timestamp() %% Timestamp }). -type mqtt_message() :: #mqtt_message{}. @@ -135,4 +136,16 @@ -type mqtt_plugin() :: #mqtt_plugin{}. +%%------------------------------------------------------------------------------ +%% MQTT Alarm +%%------------------------------------------------------------------------------ +-record(mqtt_alarm, { + id :: binary(), + severity :: warning | error | critical, + title :: binary(), + summary :: binary(), + timestamp :: erlang:timestamp() %% Timestamp +}). + +-type mqtt_alarm() :: #mqtt_alarm{}. diff --git a/src/emqttd_alarm.erl b/src/emqttd_alarm.erl index 73c432534..0c7061490 100644 --- a/src/emqttd_alarm.erl +++ b/src/emqttd_alarm.erl @@ -27,17 +27,18 @@ -module(emqttd_alarm). --export([start_link/0, set_alarm/1, clear_alarm/1, get_alarms/0, - add_alarm_handler/1, add_alarm_handler/2, - delete_alarm_handler/1]). +-include("emqttd.hrl"). + +-export([start_link/0, alarm_fun/0, get_alarms/0, + set_alarm/1, clear_alarm/1, + add_alarm_handler/1, add_alarm_handler/2, + delete_alarm_handler/1]). -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2]). -define(SERVER, ?MODULE). --type alarm() :: {AlarmId :: any(), AlarmDescription :: string() | binary()}. - start_link() -> case gen_event:start_link({local, ?SERVER}) of {ok, Pid} -> @@ -47,12 +48,22 @@ start_link() -> Error end. --spec set_alarm(alarm()) -> ok. -set_alarm(Alarm) -> +alarm_fun() -> + alarm_fun(false). + +alarm_fun(Bool) -> + fun(alert, _Alarm) when Bool =:= true -> alarm_fun(true); + (alert, Alarm) when Bool =:= false -> set_alarm(Alarm), alarm_fun(true); + (clear, AlarmId) when Bool =:= true -> clear_alarm(AlarmId), alarm_fun(false); + (clear, _AlarmId) when Bool =:= false -> alarm_fun(false) + end. + +-spec set_alarm(mqtt_alarm()) -> ok. +set_alarm(Alarm) when is_record(Alarm, mqtt_alarm) -> gen_event:notify(?SERVER, {set_alarm, Alarm}). -spec clear_alarm(any()) -> ok. -clear_alarm(AlarmId) -> +clear_alarm(AlarmId) when is_binary(AlarmId) -> gen_event:notify(?SERVER, {clear_alarm, AlarmId}). get_alarms() -> @@ -70,25 +81,38 @@ delete_alarm_handler(Module) when is_atom(Module) -> %%----------------------------------------------------------------- %% Default Alarm handler %%----------------------------------------------------------------- - -init(_) -> {ok, []}. +init(_) -> + {ok, []}. -handle_event({set_alarm, Alarm}, Alarms)-> - %%TODO: publish to $SYS - {ok, [Alarm | Alarms]}; +handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, + severity = Severity, + title = Title, + summary = Summary}}, Alarms)-> + Timestamp = os:timestamp(), + Json = mochijson2:encode([{id, AlarmId}, + {severity, Severity}, + {title, iolist_to_binary(Title)}, + {summary, iolist_to_binary(Summary)}, + {ts, emqttd_util:now_to_secs(Timestamp)}]), + emqttd_pubsub:publish(alarm_msg(alert, AlarmId, Json)), + {ok, [Alarm#mqtt_alarm{timestamp = Timestamp} | Alarms]}; handle_event({clear_alarm, AlarmId}, Alarms)-> - %TODO: publish to $SYS - {ok, lists:keydelete(AlarmId, 1, Alarms)}; + Json = mochijson2:encode([{id, AlarmId}, {ts, emqttd_util:now_to_secs()}]), + emqttd_pubsub:publish(alarm_msg(clear, AlarmId, Json)), + {ok, lists:keydelete(AlarmId, 2, Alarms)}; handle_event(_, Alarms)-> {ok, Alarms}. -handle_info(_, Alarms) -> {ok, Alarms}. +handle_info(_, Alarms) -> + {ok, Alarms}. -handle_call(get_alarms, Alarms) -> {ok, Alarms, Alarms}; +handle_call(get_alarms, Alarms) -> + {ok, Alarms, Alarms}; -handle_call(_Query, Alarms) -> {ok, {error, bad_query}, Alarms}. +handle_call(_Query, Alarms) -> + {ok, {error, bad_query}, Alarms}. terminate(swap, Alarms) -> {?MODULE, Alarms}; @@ -96,3 +120,18 @@ terminate(swap, Alarms) -> terminate(_, _) -> ok. +alarm_msg(Type, AlarmId, Json) -> + #mqtt_message{from = alarm, + qos = 1, + sys = true, + topic = topic(Type, AlarmId), + payload = iolist_to_binary(Json), + timestamp = os:timestamp()}. + +topic(alert, AlarmId) -> + emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); + +topic(clear, AlarmId) -> + emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>). + + diff --git a/src/emqttd_mqueue.erl b/src/emqttd_mqueue.erl index d30e755da..956a6dad5 100644 --- a/src/emqttd_mqueue.erl +++ b/src/emqttd_mqueue.erl @@ -55,7 +55,7 @@ -include("emqttd.hrl"). -include("emqttd_protocol.hrl"). --export([new/2, name/1, +-export([new/3, name/1, is_empty/1, is_full/1, len/1, in/2, out/1]). @@ -64,18 +64,17 @@ -define(HIGH_WM, 0.6). -record(mqueue, {name, - q = queue:new(), %% pending queue - len = 0, %% current queue len - low_wm = ?LOW_WM, - high_wm = ?HIGH_WM, - max_len = ?MAX_LEN, - qos0 = false, - alarm = false}). + q = queue:new(), %% pending queue + len = 0, %% current queue len + low_wm = ?LOW_WM, + high_wm = ?HIGH_WM, + max_len = ?MAX_LEN, + qos0 = false, + alarm_fun}). -type mqueue() :: #mqueue{}. -type mqueue_option() :: {max_length, pos_integer()} %% Max queue length - | {inflight_window, pos_integer()} %% Inflight Window | {low_watermark, float()} %% Low watermark | {high_watermark, float()} %% High watermark | {queue_qos0, boolean()}. %% Queue Qos0 @@ -86,14 +85,15 @@ %% @doc New Queue. %% @end %%------------------------------------------------------------------------------ --spec new(binary(), list(mqueue_option())) -> mqueue(). -new(Name, Opts) -> +-spec new(binary(), list(mqueue_option()), fun()) -> mqueue(). +new(Name, Opts, AlarmFun) -> MaxLen = emqttd_opts:g(max_length, Opts, 1000), #mqueue{name = Name, max_len = MaxLen, low_wm = round(MaxLen * emqttd_opts:g(low_watermark, Opts, ?LOW_WM)), high_wm = round(MaxLen * emqttd_opts:g(high_watermark, Opts, ?HIGH_WM)), - qos0 = emqttd_opts:g(queue_qos0, Opts, true)}. + qos0 = emqttd_opts:g(queue_qos0, Opts, true), + alarm_fun = AlarmFun}. name(#mqueue{name = Name}) -> Name. @@ -135,18 +135,21 @@ out(MQ = #mqueue{q = Q, len = Len}) -> {Result, Q2} = queue:out(Q), {Result, maybe_clear_alarm(MQ#mqueue{q = Q2, len = Len - 1})}. -maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm = false}) - when Len >= HighWM -> - AlarmDescr = io_lib:format("len ~p > high_watermark ~p", [Len, HighWM]), - emqttd_alarm:set_alarm({{queue_high_watermark, Name}, AlarmDescr}), - MQ#mqueue{alarm = true}; +maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm_fun = AlarmFun}) + when Len > HighWM -> + Alarm = #mqtt_alarm{id = list_to_binary(["queue_high_watermark.", Name]), + severity = warning, + title = io_lib:format("Queue ~s high-water mark", [Name]), + summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])}, + MQ#mqueue{alarm_fun = AlarmFun(alert, Alarm)}; + maybe_set_alarm(MQ) -> MQ. -maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm = true}) - when Len =< LowWM -> - emqttd_alarm:clear_alarm({queue_high_watermark, Name}), - MQ#mqueue{alarm = false}; +maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm_fun = AlarmFun}) + when Len < LowWM -> + MQ#mqueue{alarm_fun = AlarmFun(clear, list_to_binary(["queue_high_watermark.", Name]))}; + maybe_clear_alarm(MQ) -> MQ. diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index 5cba1619d..e6bc4dce5 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -222,7 +222,7 @@ init([CleanSess, ClientId, ClientPid]) -> subscriptions = [], inflight_queue = [], max_inflight = emqttd_opts:g(max_inflight, SessEnv, 0), - message_queue = emqttd_mqueue:new(ClientId, QEnv), + message_queue = emqttd_mqueue:new(ClientId, QEnv, emqttd_alarm:alarm_fun()), awaiting_rel = #{}, awaiting_ack = #{}, awaiting_comp = #{}, From 34fc0fb1570003e97c8d06321e2a18b5aaab14cc Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 00:37:41 +0800 Subject: [PATCH 095/104] now_to_secs --- src/emqttd_util.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/emqttd_util.erl b/src/emqttd_util.erl index 7ec6459e3..81b1bd54d 100644 --- a/src/emqttd_util.erl +++ b/src/emqttd_util.erl @@ -30,7 +30,8 @@ -export([apply_module_attributes/1, all_module_attributes/1, - cancel_timer/1]). + cancel_timer/1, + now_to_secs/0, now_to_secs/1]). -export([integer_to_binary/1]). @@ -90,4 +91,9 @@ cancel_timer(Ref) -> integer_to_binary(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). +now_to_secs() -> + now_to_secs(os:timestamp()). + +now_to_secs({MegaSecs, Secs, _MicroSecs}) -> + MegaSecs * 1000000 + Secs. From 89c939b321565a2d98615c642a09b5ed2824100e Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 00:37:58 +0800 Subject: [PATCH 096/104] {subscribe, TopicTable} --- src/emqttd_ws_client.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emqttd_ws_client.erl b/src/emqttd_ws_client.erl index 0270d7d38..c72f46511 100644 --- a/src/emqttd_ws_client.erl +++ b/src/emqttd_ws_client.erl @@ -135,8 +135,8 @@ handle_info({redeliver, {?PUBREL, PacketId}}, #client_state{proto_state = ProtoS {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({subscribe, Topic, Qos}, #client_state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:handle({subscribe, Topic, Qos}, ProtoState), +handle_info({subscribe, TopicTable}, #client_state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:handle({subscribe, TopicTable}, ProtoState), {noreply, State#client_state{proto_state = ProtoState1}}; handle_info({stop, duplicate_id, _NewPid}, State=#client_state{proto_state = ProtoState}) -> @@ -145,7 +145,6 @@ handle_info({stop, duplicate_id, _NewPid}, State=#client_state{proto_state = Pro handle_info({keepalive, start, TimeoutSec}, State = #client_state{request = Req}) -> lager:debug("Client(WebSocket) ~s: Start KeepAlive with ~p seconds", [Req:get(peer), TimeoutSec]), - %%TODO: fix esockd_transport... KeepAlive = emqttd_keepalive:new({esockd_transport, Req:get(socket)}, TimeoutSec, {keepalive, timeout}), {noreply, State#client_state{keepalive = KeepAlive}}; From d0429f56e7e71b07be55d58230bf86b5e551b451 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 00:38:15 +0800 Subject: [PATCH 097/104] fix 'loads/0' export --- src/emqttd_vm.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/emqttd_vm.erl b/src/emqttd_vm.erl index 6cc9f7c50..d25e373a2 100644 --- a/src/emqttd_vm.erl +++ b/src/emqttd_vm.erl @@ -107,8 +107,7 @@ --export([loads/0, - get_system_info/0, +-export([get_system_info/0, % get_statistics/0, % get_process_info/0, get_ports_info/0, From ce5ca88cb6766bd5bda81276d3fdb0f2b8b14273 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 01:00:21 +0800 Subject: [PATCH 098/104] comment --- rel/files/emqttd.config | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index e72a06862..eabd4e119 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -107,14 +107,16 @@ {max_awaiting_rel, 0} ]}, + %% Session {queue, [ - %% Max queue length + %% Max queue length. enqueued messages when persistent client disconnected, + %% or inflight window is full. {max_length, 1000}, - %% Low watermark of queued messsages + %% Low-water mark of queued messsages {low_watermark, 0.2}, - %% High watermark of queued messsages + %% High-water mark of queued messsages {high_watermark, 0.6}, %% Queue Qos0 messages? @@ -141,7 +143,8 @@ %% Bridge {bridge, [ %%TODO: bridge queue size - {max_queue_len, 1000}, + {max_queue_len, 10000}, + %% Ping Interval of bridge node {ping_down_interval, 1} %seconds ]} From 82bd645d7ab93e5394a694554e08907d515420da Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 01:56:44 +0800 Subject: [PATCH 099/104] fix session issue --- src/emqttd_session.erl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl index e6bc4dce5..93c9fd54b 100644 --- a/src/emqttd_session.erl +++ b/src/emqttd_session.erl @@ -323,6 +323,8 @@ handle_cast({resume, ClientId, ClientPid}, Session) -> kick(ClientId, ClientPid, OldClientPid), + true = link(ClientPid), + %% Redeliver PUBREL [ClientPid ! {redeliver, {?PUBREL, MsgId}} || MsgId <- maps:keys(AwaitingComp)], @@ -479,12 +481,12 @@ handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, {stop, normal, Session}; handle_info({'EXIT', ClientPid, Reason}, Session = #session{clean_sess = false, - clientid = ClientId, + clientid = ClientId, client_pid = ClientPid, expired_after = Expires}) -> lager:info("Session ~s unlink with client ~p: reason=~p", [ClientId, ClientPid, Reason]), TRef = timer(Expires, session_expired), - {noreply, Session#session{expired_timer = TRef}, hibernate}; + {noreply, Session#session{client_pid = undefined, expired_timer = TRef}, hibernate}; handle_info({'EXIT', Pid, _Reason}, Session = #session{clientid = ClientId, client_pid = ClientPid}) -> From b4080a0adf6fcfd6c2899f06ecdf9fc289964197 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 02:51:56 +0800 Subject: [PATCH 100/104] ts --- src/emqttd_mod_presence.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl index fe5135c1e..83a7e9100 100644 --- a/src/emqttd_mod_presence.erl +++ b/src/emqttd_mod_presence.erl @@ -53,7 +53,7 @@ client_connected(ConnAck, #mqtt_client{clientid = ClientId, {session, Sess}, {protocol, ProtoVer}, {connack, ConnAck}, - {ts, emqttd_vm:timestamp()}]), + {ts, emqttd_util:now_to_secs()}]), Message = #mqtt_message{from = presence, qos = proplists:get_value(qos, Opts, 0), topic = topic(connected, ClientId), @@ -61,7 +61,7 @@ client_connected(ConnAck, #mqtt_client{clientid = ClientId, emqttd_pubsub:publish(Message). client_disconnected(Reason, ClientId, Opts) -> - Json = mochijson2:encode([{reason, reason(Reason)}, {ts, emqttd_vm:timestamp()}]), + Json = mochijson2:encode([{reason, reason(Reason)}, {ts, emqttd_util:now_to_secs()}]), emqttd_pubsub:publish(#mqtt_message{from = presence, qos = proplists:get_value(qos, Opts, 0), topic = topic(disconnected, ClientId), @@ -71,7 +71,6 @@ unload(_Opts) -> emqttd_broker:unhook(client_connected, {?MODULE, client_connected}), emqttd_broker:unhook(client_disconnected, {?MODULE, client_disconnected}). - topic(connected, ClientId) -> emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); topic(disconnected, ClientId) -> From eeee584fefd25275d5a18696e24bf2e397c0e8e0 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 03:07:59 +0800 Subject: [PATCH 101/104] add 'clientid' --- src/emqttd_mod_presence.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl index 83a7e9100..248eb6bcf 100644 --- a/src/emqttd_mod_presence.erl +++ b/src/emqttd_mod_presence.erl @@ -48,7 +48,8 @@ client_connected(ConnAck, #mqtt_client{clientid = ClientId, true -> false; false -> true end, - Json = mochijson2:encode([{username, Username}, + Json = mochijson2:encode([{clientid, ClientId}, + {username, Username}, {ipaddress, list_to_binary(emqttd_net:ntoa(IpAddress))}, {session, Sess}, {protocol, ProtoVer}, @@ -61,7 +62,9 @@ client_connected(ConnAck, #mqtt_client{clientid = ClientId, emqttd_pubsub:publish(Message). client_disconnected(Reason, ClientId, Opts) -> - Json = mochijson2:encode([{reason, reason(Reason)}, {ts, emqttd_util:now_to_secs()}]), + Json = mochijson2:encode([{clientid, ClientId}, + {reason, reason(Reason)}, + {ts, emqttd_util:now_to_secs()}]), emqttd_pubsub:publish(#mqtt_message{from = presence, qos = proplists:get_value(qos, Opts, 0), topic = topic(disconnected, ClientId), From 0f68186472a69dd8c44c92b9703bd0020b7ed739 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 09:26:22 +0800 Subject: [PATCH 102/104] mqueue test --- src/emqttd_mqueue.erl | 6 +-- test/emqttd_mqueue_tests.erl | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 test/emqttd_mqueue_tests.erl diff --git a/src/emqttd_mqueue.erl b/src/emqttd_mqueue.erl index 956a6dad5..d9f3c3c58 100644 --- a/src/emqttd_mqueue.erl +++ b/src/emqttd_mqueue.erl @@ -53,6 +53,7 @@ -author("Feng Lee "). -include("emqttd.hrl"). + -include("emqttd_protocol.hrl"). -export([new/3, name/1, @@ -112,10 +113,9 @@ len(#mqueue{len = Len}) -> Len. %% @end %%------------------------------------------------------------------------------ --spec in({newcome | pending, mqtt_message()}, mqueue()) -> mqueue(). - +-spec in(mqtt_message(), mqueue()) -> mqueue(). %% drop qos0 -in({_, #mqtt_message{qos = ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> +in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> MQ; %% simply drop the oldest one if queue is full, improve later diff --git a/test/emqttd_mqueue_tests.erl b/test/emqttd_mqueue_tests.erl new file mode 100644 index 000000000..8e6c4fb16 --- /dev/null +++ b/test/emqttd_mqueue_tests.erl @@ -0,0 +1,71 @@ +%%%----------------------------------------------------------------------------- +%%% @Copyright (C) 2012-2015, Feng Lee +%%% +%%% Permission is hereby granted, free of charge, to any person obtaining a copy +%%% of this software and associated documentation files (the "Software"), to deal +%%% in the Software without restriction, including without limitation the rights +%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +%%% copies of the Software, and to permit persons to whom the Software is +%%% furnished to do so, subject to the following conditions: +%%% +%%% The above copyright notice and this permission notice shall be included in all +%%% copies or substantial portions of the Software. +%%% +%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +%%% SOFTWARE. +%%%----------------------------------------------------------------------------- + +-module(emqttd_mqueue_tests). + +-include("emqttd.hrl"). + +-define(QM, emqttd_mqueue). + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). + +in_test() -> + Opts = [{max_length, 5}, + {queue_qos0, true}], + Q = ?QM:new(<<"testQ">>, Opts, alarm_fun()), + ?assertEqual(true, ?QM:is_empty(Q)), + Q1 = ?QM:in(#mqtt_message{}, Q), + ?assertEqual(1, ?QM:len(Q1)), + Q2 = ?QM:in(#mqtt_message{qos = 1}, Q1), + ?assertEqual(2, ?QM:len(Q2)), + Q3 = ?QM:in(#mqtt_message{qos = 2}, Q2), + Q4 = ?QM:in(#mqtt_message{}, Q3), + Q5 = ?QM:in(#mqtt_message{}, Q4), + ?assertEqual(true, ?QM:is_full(Q5)). + +in_qos0_test() -> + Opts = [{max_length, 5}, + {queue_qos0, false}], + Q = ?QM:new(<<"testQ">>, Opts, alarm_fun()), + Q1 = ?QM:in(#mqtt_message{}, Q), + ?assertEqual(true, ?QM:is_empty(Q1)), + Q2 = ?QM:in(#mqtt_message{qos = 0}, Q1), + ?assertEqual(true, ?QM:is_empty(Q2)). + +out_test() -> + Opts = [{max_length, 5}, + {queue_qos0, true}], + Q = ?QM:new(<<"testQ">>, Opts, alarm_fun()), + ?assertMatch({empty, Q}, ?QM:out(Q)), + Q1 = ?QM:in(#mqtt_message{}, Q), + {Value, Q2} = ?QM:out(Q1), + ?assertEqual(0, ?QM:len(Q2)), + ?assertMatch({value, #mqtt_message{}}, Value). + +alarm_fun() -> + fun(_, _) -> alarm_fun() end. + +-endif. + + From d09a0787ea2dc6999aec5bba6817d6c79d2b79b4 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Wed, 24 Jun 2015 22:05:00 +0800 Subject: [PATCH 103/104] fix sessions --- src/emqttd_app.erl | 3 +- src/emqttd_cm.erl | 171 ---------------------------------------- src/emqttd_cm_sup.erl | 59 -------------- src/emqttd_protocol.erl | 20 ++--- src/emqttd_sm.erl | 11 +-- src/emqttd_sm_sup.erl | 5 +- 6 files changed, 16 insertions(+), 253 deletions(-) delete mode 100644 src/emqttd_cm.erl delete mode 100644 src/emqttd_cm_sup.erl diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index f9434b385..ac2dd14e8 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -70,13 +70,12 @@ print_vsn() -> start_servers(Sup) -> Servers = [{"emqttd trace", emqttd_trace}, {"emqttd pooler", {supervisor, emqttd_pooler_sup}}, - {"emqttd client manager", {supervisor, emqttd_cm_sup}}, {"emqttd session manager", {supervisor, emqttd_sm_sup}}, {"emqttd session supervisor", {supervisor, emqttd_session_sup}}, {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, + {"emqttd broker", emqttd_broker}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, - {"emqttd broker", emqttd_broker}, {"emqttd alarm", emqttd_alarm}, {"emqttd mode supervisor", emqttd_mod_sup}, {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup}}, diff --git a/src/emqttd_cm.erl b/src/emqttd_cm.erl deleted file mode 100644 index 1c3effa31..000000000 --- a/src/emqttd_cm.erl +++ /dev/null @@ -1,171 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% MQTT Client Manager -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_cm). - --author("Feng Lee "). - --include("emqttd.hrl"). - --behaviour(gen_server). - --define(SERVER, ?MODULE). - -%% API Exports --export([start_link/2, pool/0, table/0]). - --export([lookup/1, register/1, unregister/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {id, tab, statsfun}). - --define(CM_POOL, cm_pool). - --define(CLIENT_TAB, mqtt_client). - -%%%============================================================================= -%%% API -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Start client manager -%% @end -%%------------------------------------------------------------------------------ --spec start_link(Id, StatsFun) -> {ok, pid()} | ignore | {error, any()} when - Id :: pos_integer(), - StatsFun :: fun(). -start_link(Id, StatsFun) -> - gen_server:start_link(?MODULE, [Id, StatsFun], []). - -pool() -> ?CM_POOL. - -table() -> ?CLIENT_TAB. - -%%------------------------------------------------------------------------------ -%% @doc Lookup client pid with clientId -%% @end -%%------------------------------------------------------------------------------ --spec lookup(ClientId :: binary()) -> mqtt_client() | undefined. -lookup(ClientId) when is_binary(ClientId) -> - case ets:lookup(?CLIENT_TAB, ClientId) of - [Client] -> Client; - [] -> undefined - end. - -%%------------------------------------------------------------------------------ -%% @doc Register clientId with pid. -%% @end -%%------------------------------------------------------------------------------ --spec register(Client :: mqtt_client()) -> ok. -register(Client = #mqtt_client{clientid = ClientId}) -> - CmPid = gproc_pool:pick_worker(?CM_POOL, ClientId), - gen_server:call(CmPid, {register, Client}, infinity). - -%%------------------------------------------------------------------------------ -%% @doc Unregister clientId with pid. -%% @end -%%------------------------------------------------------------------------------ --spec unregister(ClientId :: binary()) -> ok. -unregister(ClientId) when is_binary(ClientId) -> - CmPid = gproc_pool:pick_worker(?CM_POOL, ClientId), - gen_server:cast(CmPid, {unregister, ClientId, self()}). - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -init([Id, StatsFun]) -> - gproc_pool:connect_worker(?CM_POOL, {?MODULE, Id}), - {ok, #state{id = Id, statsfun = StatsFun}}. - -handle_call({register, Client = #mqtt_client{clientid = ClientId, client_pid = Pid}}, _From, State) -> - case ets:lookup(?CLIENT_TAB, ClientId) of - [#mqtt_client{client_pid = Pid}] -> - lager:error("clientId '~s' has been registered with ~p", [ClientId, Pid]), - ignore; - [#mqtt_client{client_pid = OldPid, client_mon = MRef}] -> - lager:error("clientId '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, Pid, OldPid]), - OldPid ! {stop, duplicate_id, Pid}, - erlang:demonitor(MRef), - ets:insert(?CLIENT_TAB, Client#mqtt_client{client_mon = erlang:monitor(process, Pid)}); - [] -> - ets:insert(?CLIENT_TAB, Client#mqtt_client{client_mon = erlang:monitor(process, Pid)}) - end, - {reply, ok, setstats(State)}; - -handle_call(Req, _From, State) -> - lager:error("unexpected request: ~p", [Req]), - {reply, {error, badreq}, State}. - -handle_cast({unregister, ClientId, Pid}, State) -> - case ets:lookup(?CLIENT_TAB, ClientId) of - [#mqtt_client{client_pid = Pid, client_mon = MRef}] -> - erlang:demonitor(MRef, [flush]), - ets:delete(?CLIENT_TAB, ClientId); - [_] -> - ignore; - [] -> - lager:error("cannot find clientId '~s' with ~p", [ClientId, Pid]) - end, - {noreply, setstats(State)}; - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({'DOWN', MRef, process, DownPid, Reason}, State) -> - case ets:match_object(?CLIENT_TAB, {mqtt_client, '$1', '_', '_', DownPid, MRef, '_', '_'}) of - [] -> - ignore; - Clients -> - lists:foreach( - fun(Client = #mqtt_client{clientid = ClientId}) -> - ets:delete_object(?CLIENT_TAB, Client), - lager:error("Client ~s is Down: ~p", [ClientId, Reason]), - emqttd_broker:foreach_hooks(client_disconnected, [Reason, ClientId]) - end, Clients) - end, - {noreply, setstats(State)}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, #state{id = Id}) -> - gproc_pool:disconnect_worker(?CM_POOL, {?MODULE, Id}), ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -setstats(State = #state{statsfun = StatsFun}) -> - StatsFun(ets:info(?CLIENT_TAB, size)), State. - - diff --git a/src/emqttd_cm_sup.erl b/src/emqttd_cm_sup.erl deleted file mode 100644 index bf9500630..000000000 --- a/src/emqttd_cm_sup.erl +++ /dev/null @@ -1,59 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd client manager supervisor. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_cm_sup). - --author("Feng Lee "). - --include("emqttd.hrl"). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - ets:new(emqttd_cm:table(), [ordered_set, named_table, public, {keypos, 2}, - {write_concurrency, true}]), - Schedulers = erlang:system_info(schedulers), - gproc_pool:new(emqttd_cm:pool(), hash, [{size, Schedulers}]), - StatsFun = emqttd_stats:statsfun('clients/count', 'clients/max'), - Children = lists:map( - fun(I) -> - Name = {emqttd_cm, I}, - gproc_pool:add_worker(emqttd_cm:pool(), Name, I), - {Name, {emqttd_cm, start_link, [I, StatsFun]}, - permanent, 10000, worker, [emqttd_cm]} - end, lists:seq(1, Schedulers)), - {ok, {{one_for_all, 10, 100}, Children}}. - - diff --git a/src/emqttd_protocol.erl b/src/emqttd_protocol.erl index dbd6bfa72..9be6b41fc 100644 --- a/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -63,11 +63,10 @@ %%------------------------------------------------------------------------------ init(Peername, SendFun, Opts) -> MaxLen = proplists:get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), - #proto_state{ - peername = Peername, - sendfun = SendFun, - max_clientid_len = MaxLen, - client_pid = self()}. + #proto_state{peername = Peername, + sendfun = SendFun, + max_clientid_len = MaxLen, + client_pid = self()}. info(#proto_state{proto_ver = ProtoVer, proto_name = ProtoName, @@ -145,9 +144,6 @@ handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername} %% Generate clientId if null State2 = State1#proto_state{clientid = clientid(ClientId, State1)}, - %% Register the client to cm - emqttd_cm:register(client(State2)), - %%Starting session {ok, Session} = emqttd_sm:start_session(CleanSess, clientid(State2)), @@ -166,7 +162,7 @@ handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername} {ReturnCode, State1} end, %% Run hooks - emqttd_broker:foreach_hooks(client_connected, [ReturnCode1, client(State3)]), + emqttd_broker:foreach_hooks('client.connected', [ReturnCode1, client(State3)]), %% Send connack send(?CONNACK_PACKET(ReturnCode1), State3); @@ -293,8 +289,7 @@ shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg lager:info([{client, ClientId}], "Client ~s@~s: shutdown ~p", [ClientId, emqttd_net:format(Peername), Error]), send_willmsg(ClientId, WillMsg), - try_unregister(ClientId), - emqttd_broker:foreach_hooks(client_disconnected, [Error, ClientId]). + emqttd_broker:foreach_hooks('client.disconnected', [Error, ClientId]). willmsg(Packet) when is_record(Packet, mqtt_packet_connect) -> emqttd_message:from_packet(Packet). @@ -387,9 +382,6 @@ validate_qos(undefined) -> true; validate_qos(Qos) when Qos =< ?QOS_2 -> true; validate_qos(_) -> false. -try_unregister(undefined) -> ok; -try_unregister(ClientId) -> emqttd_cm:unregister(ClientId). - %% publish ACL is cached in process dictionary. check_acl(publish, Topic, State) -> case get({acl, publish, Topic}) of diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index bbebf3db5..fca18aa76 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -53,7 +53,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {id, statsfun}). +-record(state, {id, client_statsfun, sess_statsfun}). -define(SM_POOL, sm_pool). @@ -67,11 +67,12 @@ %% @doc Start a session manager %% @end %%------------------------------------------------------------------------------ --spec start_link(Id, StatsFun) -> {ok, pid()} | ignore | {error, any()} when +-spec start_link(Id, ClientStatsFun, SessStatsFun) -> {ok, pid()} | ignore | {error, any()} when Id :: pos_integer(), - StatsFun :: fun(). -start_link(Id, StatsFun) -> - gen_server:start_link(?MODULE, [Id, StatsFun], []). + ClientStatsFun :: fun(), + SessStatsFun :: fun(). +start_link(Id, ClientStatsFun, SessStatsFun) -> + gen_server:start_link(?MODULE, [Id, ClientStatsFun, SessStatsFun], []). %%------------------------------------------------------------------------------ %% @doc Pool name. diff --git a/src/emqttd_sm_sup.erl b/src/emqttd_sm_sup.erl index fc50956d1..00babaf94 100644 --- a/src/emqttd_sm_sup.erl +++ b/src/emqttd_sm_sup.erl @@ -46,12 +46,13 @@ init([]) -> {write_concurrency, true}]), Schedulers = erlang:system_info(schedulers), gproc_pool:new(emqttd_sm:pool(), hash, [{size, Schedulers}]), - StatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), + ClientStatsFun = emqttd_stats:statsfun('clients/count', 'clients/max'), + SessStatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), Children = lists:map( fun(I) -> Name = {emqttd_sm, I}, gproc_pool:add_worker(emqttd_sm:pool(), Name, I), - {Name, {emqttd_sm, start_link, [I, StatsFun]}, + {Name, {emqttd_sm, start_link, [I, ClientStatsFun, SessStatsFun]}, permanent, 10000, worker, [emqttd_sm]} end, lists:seq(1, Schedulers)), {ok, {{one_for_all, 10, 100}, Children}}. From 8ea07076849a2ec29b1d44995d02b7d13e8f4d0d Mon Sep 17 00:00:00 2001 From: Feng Date: Wed, 24 Jun 2015 22:48:35 +0800 Subject: [PATCH 104/104] fix session --- src/emqttd_app.erl | 2 +- src/emqttd_sm.erl | 10 +++++----- src/emqttd_sm_sup.erl | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/emqttd_app.erl b/src/emqttd_app.erl index ac2dd14e8..c1ffd10e6 100644 --- a/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -73,9 +73,9 @@ start_servers(Sup) -> {"emqttd session manager", {supervisor, emqttd_sm_sup}}, {"emqttd session supervisor", {supervisor, emqttd_session_sup}}, {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, - {"emqttd broker", emqttd_broker}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, + {"emqttd broker", emqttd_broker}, {"emqttd alarm", emqttd_alarm}, {"emqttd mode supervisor", emqttd_mod_sup}, {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup}}, diff --git a/src/emqttd_sm.erl b/src/emqttd_sm.erl index fca18aa76..59e32f2a1 100644 --- a/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -53,7 +53,7 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {id, client_statsfun, sess_statsfun}). +-record(state, {id, statsfun}). -define(SM_POOL, sm_pool). @@ -67,12 +67,12 @@ %% @doc Start a session manager %% @end %%------------------------------------------------------------------------------ --spec start_link(Id, ClientStatsFun, SessStatsFun) -> {ok, pid()} | ignore | {error, any()} when +-spec start_link(Id, SessStatsFun) -> {ok, pid()} | ignore | {error, any()} when Id :: pos_integer(), - ClientStatsFun :: fun(), + %ClientStatsFun :: fun(), SessStatsFun :: fun(). -start_link(Id, ClientStatsFun, SessStatsFun) -> - gen_server:start_link(?MODULE, [Id, ClientStatsFun, SessStatsFun], []). +start_link(Id, SessStatsFun) -> + gen_server:start_link(?MODULE, [Id, SessStatsFun], []). %%------------------------------------------------------------------------------ %% @doc Pool name. diff --git a/src/emqttd_sm_sup.erl b/src/emqttd_sm_sup.erl index 00babaf94..03ee63a47 100644 --- a/src/emqttd_sm_sup.erl +++ b/src/emqttd_sm_sup.erl @@ -46,13 +46,13 @@ init([]) -> {write_concurrency, true}]), Schedulers = erlang:system_info(schedulers), gproc_pool:new(emqttd_sm:pool(), hash, [{size, Schedulers}]), - ClientStatsFun = emqttd_stats:statsfun('clients/count', 'clients/max'), + %%ClientStatsFun = emqttd_stats:statsfun('clients/count', 'clients/max'), SessStatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), Children = lists:map( fun(I) -> Name = {emqttd_sm, I}, gproc_pool:add_worker(emqttd_sm:pool(), Name, I), - {Name, {emqttd_sm, start_link, [I, ClientStatsFun, SessStatsFun]}, + {Name, {emqttd_sm, start_link, [I, SessStatsFun]}, permanent, 10000, worker, [emqttd_sm]} end, lists:seq(1, Schedulers)), {ok, {{one_for_all, 10, 100}, Children}}.