Commits

Fred T-H committed 20f98c0

Moving my chat system prototype out of my private repository

Contains an ugly demo web-server with some javascript that needs to be
changed because it works bad, suffers stack overflows, etc.
The Erlang core seems pretty solid, although more testing needs to be done.

  • Participants

Comments (0)

Files changed (12)

+#use glob syntax.
+syntax: glob
+
+*.beam
+
+#switch to refexp syntax.
+syntax: regexp
+^\.beam/

File src/client.erl

+-module(client).
+-export([connect/1, disconnect/2, message/3, listen/1]).
+
+-define(SESSION_LIFE, 15000).
+-define(LISTEN_TIMEOUT, 40000).
+
+connect(Id) ->
+    usr:start(Id, ?SESSION_LIFE),
+    usr:subscribe(Id).
+
+disconnect(Id, Handler) ->
+    usr:unsubscribe(Id, Handler).
+
+message(From, To, Message) ->
+    usr:message(From, To, Message).
+
+listen(Id) ->
+    receive
+        {Id,Msg} -> [Msg | listen1(Id)]
+    after ?LISTEN_TIMEOUT ->
+        []
+    end.
+
+%% used to catch all messages currently lined up
+listen1(Id) ->
+    receive
+        {Id,Msg} -> [Msg | listen1(Id)]
+    after 0 ->
+        []
+    end.

File src/client.js

+// html content
+is_listening = false;
+var html = {
+    nickBlock: '<div id="nickblock">nick: <input type="text" id="nickname" /><button id="regnick">OK</button>',
+    text: '<div id="text"></div>',
+    send_msg: '<div id="msgform">à: <input type="text" id="to"/><br />msg: <input type="text" id="send_msg" /><button id="submit_msg">OK</button>'
+};
+
+function get_nick() {
+    return $('#nick').text();
+}
+
+// call suivi d'un listen server-side
+function send_msg() {
+    var msg = $('#send_msg').val();
+    $('#send_msg').val('');
+    var to = $('#to').val();
+    $.getJSON('message?nick='+escape(get_nick())+'&to='+escape(to)+'&msg='+escape(msg)+'&rand='+Math.random());
+}
+function send_emsg(e){
+    if(e.which == 13){ // enter key pressed
+        send_msg();
+    }
+}
+function putmsgs(data){
+    $.map(data, function(e){
+        var from;
+        var to;
+        if(e.action == 'sent'){
+            from = get_nick();
+            to = e.to;
+        }else if (e.action == 'received') {
+            from = e.to;
+            to = get_nick();
+        }
+        var msg = '<p class="'+e.action+'">'+from+' -> '+to+'<br />'+e.message+'</p>';
+        $('#text').append(msg);
+    });
+    is_listening = false;
+}
+// mode passif!
+function listen(){
+    if(is_listening === false){
+        is_listening = true;
+        $.getJSON('listen?nick='+escape(get_nick())+'&rand='+Math.random(), function(data){ putmsgs(data); listen();} );
+    }
+}
+
+// listen checker!
+
+// functions and whatnot
+// registration pas necessaire, faite à chaque call
+function main(){
+    var nick = $('#nickname').val();
+    $('#content').prepend('<p>Connect&eacute; en tant que <span id="nick">'+nick+'</p>');
+    $('#nickblock').remove();
+    $('#content').append(html.text);
+    $('#content').append(html.send_msg);
+    $('#submit_msg').click(send_msg);
+    $('#submit_msg').keypress(send_emsg);
+    listen();
+}
+
+// main
+$(document).ready(function(){
+    // register user
+    $('#content').append(html.nickBlock);
+    $('#regnick').click(main);
+});
+%% used to replace common_test's broken includes
+-include_lib("test_server/include/test_server.hrl").
+-compile({parse_transform,ct_line}).
+
+%% must deal with crashing listener!
+-module(usr).
+
+%% API
+-export([start/2, terminate/2, subscribe/1, unsubscribe/2, subscribers/1,
+         message/3, relay/3]).
+
+start(UserId, TimeOut) ->
+    init(UserId, TimeOut),
+    ok.
+
+%% kills the supervisor and it should trickle down to the children.
+terminate(UserId, Reason) ->
+    Pid = global:whereis_name(UserId),
+    io:format("Killing user ~p. Reason: ~p~n",[UserId, Reason]),
+    erlang:exit(Pid, kill).
+
+%% in a global lock. Make no mistake. Race condition-free.
+init(UserId, TimeOut) ->
+    case global:whereis_name(UserId) of
+        undefined ->
+            %% start it here
+            usr_sup:start_link(UserId, TimeOut),
+            HandlerId = usr_dispatch_handler,
+            usr_dispatch_manager:add_handler(UserId, HandlerId, UserId),
+            io:format("started user process for id ~p~n",[UserId]),
+            started;
+        _Pid ->
+            io:format("connected to user ~p~n",[UserId]),
+            ok
+    end.
+
+subscribe(UserId) ->
+    HandlerId = {usr_listen_handler, make_ref()},
+    usr_dispatch_manager:add_sup_handler(UserId, HandlerId, {UserId, self(), HandlerId}),
+    usr_monitor:add_handler(UserId, HandlerId),
+    {ok, HandlerId}.
+
+subscribers(UserId) ->
+    usr_dispatch_manager:get_handlers(UserId).
+
+unsubscribe(UserId, HandlerId) ->
+    usr_dispatch_manager:delete_handler(UserId, HandlerId, []),
+    usr_monitor:delete_handler(UserId, HandlerId),
+    ok.
+
+message(From, To, Message) ->
+    usr_dispatch_manager:notify(From, {send, To, Message}),
+    ok.
+
+relay(From, To, Message) ->
+    usr_dispatch_manager:notify(To, {received, From, Message}),
+    ok.

File src/usr_SUITE.erl

+%%%-------------------------------------------------------------------
+%%% File    : usr_SUITE.erl
+%%% Description: Basic tests for a user and its clients
+%%%
+%%% Created : 2009/12/23
+%%%-------------------------------------------------------------------
+-module(usr_SUITE).
+-compile(export_all).
+-include("ct.hrl").
+%-include_lib("common_test/include/ct.hrl").
+%%--------------------------------------------------------------------
+%% Function: suite() -> Info
+%% Info = [tuple()]
+%%--------------------------------------------------------------------
+suite() ->
+    [{timetrap,{seconds,30}}].
+%%--------------------------------------------------------------------
+%% Function: init_per_suite(Config0) ->
+%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+init_per_suite(Config) ->
+    Config.
+%%--------------------------------------------------------------------
+%% Function: end_per_suite(Config0) -> void() | {save_config,Config1}
+%% Config0 = Config1 = [tuple()]
+%%--------------------------------------------------------------------
+end_per_suite(_Config) ->
+    ok.
+%%--------------------------------------------------------------------
+%% Function: init_per_group(GroupName, Config0) ->
+%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% GroupName = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+init_per_group(_GroupName, Config) ->
+    Config.
+%%--------------------------------------------------------------------
+%% Function: end_per_group(GroupName, Config0) ->
+%%               void() | {save_config,Config1}
+%% GroupName = atom()
+%% Config0 = Config1 = [tuple()]
+%%--------------------------------------------------------------------
+end_per_group(_GroupName, _Config) ->
+    ok.
+%%--------------------------------------------------------------------
+%% Function: init_per_testcase(TestCase, Config0) ->
+%%               Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% TestCase = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+init_per_testcase(_TestCase, Config) ->
+    Config.
+%%--------------------------------------------------------------------
+%% Function: end_per_testcase(TestCase, Config0) ->
+%%               void() | {save_config,Config1} | {fail,Reason}
+%% TestCase = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%%--------------------------------------------------------------------
+end_per_testcase(_TestCase, _Config) ->
+    ok.
+%%--------------------------------------------------------------------
+%% Function: groups() -> [Group]
+%% Group = {GroupName,Properties,GroupsAndTestCases}
+%% GroupName = atom()
+%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}]
+%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase]
+%% TestCase = atom()
+%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}}
+%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
+%%              repeat_until_any_ok | repeat_until_any_fail
+%% N = integer() | forever
+%%--------------------------------------------------------------------
+groups() ->
+    [].
+%%--------------------------------------------------------------------
+%% Function: all() -> GroupsAndTestCases | {skip,Reason}
+%% GroupsAndTestCases = [{group,GroupName} | TestCase]
+%% GroupName = atom()
+%% TestCase = atom()
+%% Reason = term()
+%%--------------------------------------------------------------------
+all() -> 
+    [usr_timeout, usr_subscribe, usr_message, usr_relay, usr_conv,
+     client_conv].
+%%--------------------------------------------------------------------
+%% Function: TestCase(Config0) ->
+%%               ok | exit() | {skip,Reason} | {comment,Comment} |
+%%               {save_config,Config1} | {skip_and_save,Reason,Config1}
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%% Comment = term()
+%%--------------------------------------------------------------------
+
+usr_timeout(_Config) ->
+    [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
+    IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
+    usr:start(Id, Sleep),
+    true = lists:all(IsProc, Expected),
+    timer:sleep(Sleep*3),
+    io:format("procs: ~p~n", [global:registered_names()]),
+    false = lists:any(IsProc, Expected),
+    ok.
+
+usr_subscribe(_Config) ->
+    [Id, Sleep, Expected] = [id, 500, [{monitor, id}, id, {manager,id}]],
+    IsProc = fun(Name) -> lists:member(Name, global:registered_names()) end,
+    usr:start(Id, Sleep),
+    {ok, Handler} = usr:subscribe(Id),
+    timer:sleep(Sleep*3),
+    true = lists:all(IsProc, Expected),
+    usr:unsubscribe(Id, Handler),
+    timer:sleep(Sleep*3),
+    false = lists:any(IsProc, Expected),
+    ok.
+
+usr_message(_Config) ->
+    usr:start(send_id, 500),
+    {ok, Handler} = usr:subscribe(send_id),
+    usr:message(send_id, invalid_id, hello),
+    X = receive
+            {send_id, {sent, invalid_id, hello}} -> true
+        after 1000 -> throw("Message not sent")
+        end,
+    usr:unsubscribe(send_id, Handler),
+    true = X.
+
+usr_relay(_Config) ->
+    usr:start(sender, 500),
+    {ok, HandlerS} = usr:subscribe(sender),
+    usr:start(rec, 500),
+    {ok, HandlerR} = usr:subscribe(rec),
+    usr:message(sender, rec, hello),
+    X = receive
+            {rec, {received, sender, hello}} -> true
+        after 1000 -> throw("Message not received")
+        end,
+    usr:unsubscribe(sender, HandlerS),
+    usr:unsubscribe(rec, HandlerR),
+    true = X.
+
+usr_conv(_Config) ->
+    {A,B} = {make_ref(), make_ref()},
+    usr:start(A, 500),
+    {ok, HandlerA} = usr:subscribe(A),
+    usr:start(B, 500),
+    {ok, HandlerB} = usr:subscribe(B),
+    usr:message(A,B,"Hello, B"),
+    usr:message(B,A,"Oh, Hello, A!"),
+    receive {B, {received, A, "Hello, B"}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {A, {sent, B, "Hello, B"}} -> ok
+    after 500 -> throw("Message not sent")
+    end,
+    receive {A, {received, B, "Oh, Hello, A!"}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {B, {sent, A, "Oh, Hello, A!"}} -> ok
+    after 500 -> throw ("Message not sent")
+    end,
+    usr:message(A,B, "Well, goodbye!"),
+    usr:message(B,A, "Have a nice day..."),
+    receive {B, {received, A, "Well, goodbye!"}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {A, {sent, B, "Well, goodbye!"}} -> ok
+    after 500 -> throw("Message not sent")
+    end,
+    receive {A, {received, B, "Have a nice day..."}} -> ok
+    after 500 -> throw("Message not received")
+    end,
+    receive {B, {sent, A, "Have a nice day..."}} -> ok
+    after 500 -> throw ("Message not sent")
+    end,
+    usr:unsubscribe(A, HandlerA),
+    usr:unsubscribe(B, HandlerB).
+
+client_conv(_Config) ->
+    {A,B} = {make_ref(), make_ref()},
+    {ok, ConnA} = client:connect(A),
+    {ok, ConnB} = client:connect(B),
+    client:message(A,B,"hi"),
+    [{sent, B, "hi"}] = client:listen(A),
+    client:message(A,B, "you there?"),
+    timer:sleep(50), %% simulate delays to group messages
+    [{received, A, "hi"},{received, A, "you there?"}] = client:listen(B),
+    client:message(B,A, "stop harassing me!"),
+    timer:sleep(50), %% simulate delays to group messages
+    [{sent, B, "you there?"},
+     {received,B, "stop harassing me!"}] = client:listen(A),
+    [{sent, A, "stop harassing me!"}] = client:listen(B),
+    client:disconnect(A,ConnA),
+    client:disconnect(B,ConnB).

File src/usr_dispatch_handler.erl

+-module(usr_dispatch_handler).
+-compile(export_all).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%% strictly deals with forwarding messages to other channels
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler.
+%%--------------------------------------------------------------------
+init(UserId) ->
+    {ok, UserId}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%%                               {swap_handler, Args1, State1, Mod2, Args2} |
+%%                               remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%--------------------------------------------------------------------
+handle_event({send, To, Msg}, UserId) ->
+    usr:relay(UserId, To, Msg),
+    {ok, UserId};
+handle_event(_Event, State) ->
+    {ok, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%%                                {swap_handler, Reply, Args1, State1,
+%%                                  Mod2, Args2} |
+%%                                {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Request, State) ->
+    Reply = ok,
+    {ok, Reply, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%%                             {swap_handler, Args1, State1, Mod2, Args2} |
+%%                              remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+  {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description:Whenever an event handler is deleted from an event manager,
+%% this function is called. It should be the opposite of Module:init/1 and
+%% do any necessary cleaning up.
+%%--------------------------------------------------------------------
+terminate(_Reason, _State) ->
+  ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.
+

File src/usr_dispatch_manager.erl

+-module(usr_dispatch_manager).
+
+%% API
+-export([start_link/1, add_handler/3, add_sup_handler/3, delete_handler/3,
+         notify/2, get_handlers/1]).
+
+%%--------------------------------------------------------------------
+%% Function: start_link() -> {ok,Pid} | {error,Error}
+%% Description: Creates an event manager.
+%% The user is global by default as we ought to support a multiple
+%% node system and will need more than a single process
+%%--------------------------------------------------------------------
+start_link(UserId) ->
+    gen_event:start_link({global, {manager, UserId}}).
+
+%%--------------------------------------------------------------------
+%% Function: add_handler(Module,Args) -> ok | {'EXIT',Reason} | term()
+%% Description: Adds an event handler
+%%--------------------------------------------------------------------
+add_handler(UserId, Handler = {_Module, _HandlerId}, Params) ->
+    gen_event:add_sup_handler({global, {manager, UserId}}, Handler, Params);
+add_handler(UserId, Module, Params) when is_atom(Module) ->
+    gen_event:add_sup_handler({global, {manager, UserId}}, Module, Params).
+
+%%--------------------------------------------------------------------
+%% Function: add_sup_handler(Module,Args) -> ok | {'EXIT',Reason} | term()
+%% Description: Adds an event handler that watches for the calling process'
+%% exit signals. This avoids having zombie listeners left over. The actual
+%% handling of errors is done in the callback module's terminate/2 function
+%%--------------------------------------------------------------------
+add_sup_handler(UserId, Handler = {_Module, _HandlerId}, Params) ->
+    gen_event:add_sup_handler({global, {manager, UserId}}, Handler, Params);
+add_sup_handler(UserId, Module, Params) when is_atom(Module) ->
+    gen_event:add_sup_handler({global, {manager, UserId}}, Module, Params).
+
+%%--------------------------------------------------------------------
+%% Function: delete_handler(UserId, Handler, Params) ->
+%% Description: Removes an event handler
+%%--------------------------------------------------------------------
+delete_handler(UserId, Handler, Params) ->
+    gen_event:delete_handler({global, {manager,UserId}}, Handler, Params).
+
+%%--------------------------------------------------------------------
+%% Function: get_handlers(UserId) -> [term()]
+%% Description: returns a list of all the handlers registered
+%%--------------------------------------------------------------------
+get_handlers(UserId) ->
+    gen_event:which_handlers({global, {manager, UserId}}).
+
+%%--------------------------------------------------------------------
+%% Function: notify(Event) -> ok | {error, Reason}
+%% Description: Sends the Event through the event manager.
+%%--------------------------------------------------------------------
+notify(UserId, Message) ->
+  gen_event:notify({global, {manager,UserId}}, Message).

File src/usr_listen_handler.erl

+-module(usr_listen_handler).
+-compile(export_all).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+%%====================================================================
+%% gen_event callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, State}
+%% Description: Whenever a new event handler is added to an event manager,
+%% this function is called to initialize the event handler.
+%%--------------------------------------------------------------------
+init({UserId, SubscriberPid, HandlerId}) when is_pid(SubscriberPid) ->
+    {ok, {UserId, SubscriberPid, HandlerId}}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, State) -> {ok, State} |
+%%                               {swap_handler, Args1, State1, Mod2, Args2} |
+%%                               remove_handler
+%% Description:Whenever an event manager receives an event sent using
+%% gen_event:notify/2 or gen_event:sync_notify/2, this function is called for
+%% each installed event handler to handle the event.
+%%--------------------------------------------------------------------
+handle_event({received, From, Msg}, {UserId, SubscriberPid, HandlerId}) ->
+    SubscriberPid ! {UserId, {received, From, Msg}},
+    {ok, {UserId, SubscriberPid, HandlerId}};
+handle_event({send, To, Msg}, {UserId, SubscriberPid, HandlerId}) ->
+    SubscriberPid ! {UserId, {sent, To, Msg}},
+    {ok, {UserId, SubscriberPid, HandlerId}};
+handle_event(_Event, State) ->
+    {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_call(Request, State) -> {ok, Reply, State} |
+%%                                {swap_handler, Reply, Args1, State1,
+%%                                  Mod2, Args2} |
+%%                                {remove_handler, Reply}
+%% Description: Whenever an event manager receives a request sent using
+%% gen_event:call/3,4, this function is called for the specified event
+%% handler to handle the request.
+%%--------------------------------------------------------------------
+handle_call(_Request, State) ->
+    Reply = ok,
+    {ok, Reply, State}.
+
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info, State) -> {ok, State} |
+%%                             {swap_handler, Args1, State1, Mod2, Args2} |
+%%                              remove_handler
+%% Description: This function is called for each installed event handler when
+%% an event manager receives any other message than an event or a synchronous
+%% request (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, State) ->
+  {ok, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, State) -> void()
+%% Description:Whenever an event handler is deleted from an event manager,
+%% this function is called. It should be the opposite of Module:init/1 and
+%% do any necessary cleaning up.
+%%--------------------------------------------------------------------
+terminate({stop,_}, {UserId,_,HandlerId}) ->
+  usr_monitor:delete_handler(UserId, HandlerId),
+  ok;
+terminate(_Reason, _State) ->
+  ok.
+
+%%--------------------------------------------------------------------
+%% Function: code_change(OldVsn, State, Extra) -> {ok, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.

File src/usr_monitor.erl

+-module(usr_monitor).
+%% trap exits to know when a subscriber crashes to unregister it!
+-behaviour(gen_fsm).
+
+%% API
+-export([start_link/3, add_handler/2, delete_handler/2]).
+
+%% gen_fsm callbacks
+-export([init/1, waiting/2, listening/2, handle_event/3,
+         handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
+
+%%====================================================================
+%% API
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: start_link(UserId, Dispatcher, TimeOut) ->
+%%               {ok,Pid} | ignore | {error,Error}
+%% Description:Creates a gen_fsm process which calls Module:init/1 to
+%% initialize. To ensure a synchronized start-up procedure, this function
+%% does not return until Module:init/1 has returned.
+%%--------------------------------------------------------------------
+start_link(UserId, Dispatcher, TimeOut) ->
+  gen_fsm:start_link({global, {monitor,UserId}},
+                     ?MODULE,
+                     [{id, UserId},
+                      {dispatcher, Dispatcher},
+                      {timeout, TimeOut}],
+                     []).
+
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: add_handler(UserId, HandlerId) -> ok
+%% Description: Sends a message to the FSM to start tracking a new
+%% client listener.
+%%--------------------------------------------------------------------
+add_handler(UserId, HandlerId) ->
+    gen_fsm:sync_send_all_state_event({global, {monitor,UserId}}, {add_handler, HandlerId}).
+
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: delete_handler(UserId, HandlerId) -> ok
+%% Description: Orders the fsm to stop tracking the client listener.
+%% If there are no listeners left, the FSM will switch into waiting
+%% mode and timeout if there are no new handlers.
+%%--------------------------------------------------------------------
+delete_handler(UserId, HandlerId) ->
+    gen_fsm:send_event({global, {monitor,UserId}}, {delete_handler, HandlerId}).
+
+%%====================================================================
+%% gen_fsm callbacks
+%%====================================================================
+%%--------------------------------------------------------------------
+%% Function: init(Args) -> {ok, StateName, State} |
+%%                         {ok, StateName, State, Timeout} |
+%%                         ignore                              |
+%%                         {stop, StopReason}
+%% Description:Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
+%% gen_fsm:start_link/3,4, this function is called by the new process to
+%% initialize.
+%%--------------------------------------------------------------------
+init(Opts) ->
+    {timeout, T} = proplists:lookup(timeout,Opts),
+    {ok, waiting, [{handlers, []} | Opts], T}.
+
+
+%%--------------------------------------------------------------------
+%% Function: waiting(timeout, State) -> {stop, normal, State}
+%% Description: the fsm waits until it times out, in which case it
+%% will kill its supervisor. Adding a handler (global event) will stop
+%% the waiting state
+%%--------------------------------------------------------------------
+waiting(timeout, State) ->
+    %% maybe store messages?
+    io:format("timing out~n"),
+    {id, UserId} = proplists:lookup(id, State),
+    usr:terminate(UserId, timeout), % take care, circular dependency!
+    {stop, normal, State};
+waiting(_Event, State) -> % catch-all to avoid crashing
+    io:format("invalid timeout msg:~p~n",[_Event]),
+    {timeout, TimeOut} = proplists:lookup(timeout, State),
+    {next_state, waiting, State, TimeOut}.
+
+%%--------------------------------------------------------------------
+%% function: listening({delete_handler, HandlerId}, State) ->
+%%              {next_state, waiting, NewState, TimeOut} |
+%%              {next_state, listening, NewState}
+%% Description: the fsm just keeps listening for the order to stop
+%% tracking event handlers. When no handler is left, switch to the
+%% waiting state, otherwise, keep listening.
+%%--------------------------------------------------------------------
+listening({delete_handler, HandlerId}, State) ->
+    {handlers, Handlers} = proplists:lookup(handlers, State),
+    {timeout, TimeOut} = proplists:lookup(timeout, State),
+    TempState = proplists:delete(handlers, State),
+    NewHandlers = lists:delete(HandlerId, Handlers),
+    NewState = [{handlers, NewHandlers} | TempState],
+    case NewHandlers of
+        [] ->    {next_state, waiting, NewState, TimeOut};
+        [_|_] -> {next_state, listening, NewState}
+    end;
+listening(_Event, State) -> % avoid crashes!
+    {next_state, listening, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_event(Event, StateName, State) -> {next_state, NextStateName,
+%%                                                NextState} |
+%%                                          {next_state, NextStateName,
+%%                                                NextState, Timeout} |
+%%                                          {stop, Reason, NewState}
+%% Description: Whenever a gen_fsm receives an event sent using
+%% gen_fsm:send_all_state_event/2, this function is called to handle
+%% the event.
+%%--------------------------------------------------------------------
+handle_event(_Event, StateName, State) ->
+  {next_state, StateName, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_sync_event(Event, From, StateName,
+%%                   State) -> {next_state, NextStateName, NextState} |
+%%                             {next_state, NextStateName, NextState,
+%%                              Timeout} |
+%%                             {reply, Reply, NextStateName, NextState}|
+%%                             {reply, Reply, NextStateName, NextState,
+%%                              Timeout} |
+%%                             {stop, Reason, NewState} |
+%%                             {stop, Reason, Reply, NewState}
+%% Description: Whenever a gen_fsm receives an event sent using
+%% gen_fsm:sync_send_all_state_event/2,3, this function is called to handle
+%% the event.
+%%
+%% When receiving {add_handler, HandlerId} as an event, this will
+%% make the fsm start tracking it and switch the state to listening.
+%%--------------------------------------------------------------------
+handle_sync_event({add_handler, HandlerId}, _From, _StateName, State) ->
+    {handlers, Handlers} = proplists:lookup(handlers, State),
+    TempState = proplists:delete(handlers, State),
+    NewState = [{handlers, [HandlerId | Handlers]} | TempState],
+    {reply, ok, listening, NewState};
+handle_sync_event(_Event, _From, StateName, State) ->
+    Reply = ok,
+    {reply, Reply, StateName, State}.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% handle_info(Info,StateName,State)-> {next_state, NextStateName, NextState}|
+%%                                     {next_state, NextStateName, NextState,
+%%                                       Timeout} |
+%%                                     {stop, Reason, NewState}
+%% Description: This function is called by a gen_fsm when it receives any
+%% other message than a synchronous or asynchronous event
+%% (or a system message).
+%%--------------------------------------------------------------------
+handle_info(_Info, StateName, State) ->
+    {next_state, StateName, State}.
+
+%%--------------------------------------------------------------------
+%% Function: terminate(Reason, StateName, State) -> void()
+%% Description:This function is called by a gen_fsm 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_fsm terminates with
+%% Reason. The return value is ignored.
+%%--------------------------------------------------------------------
+terminate(_Reason, _StateName, _State) ->
+    ok.
+
+%%--------------------------------------------------------------------
+%% Function:
+%% code_change(OldVsn, StateName, State, Extra) -> {ok, StateName, NewState}
+%% Description: Convert process state when code is changed
+%%--------------------------------------------------------------------
+code_change(_OldVsn, StateName, State, _Extra) ->
+    {ok, StateName, State}.

File src/usr_sup.erl

+-module(usr_sup).
+-behaviour(supervisor).
+
+%% supervisor callback
+-export([init/1]).
+%% API
+-export([start_link/2]).
+
+start_link(UserId, TimeOut) ->
+    {ok, Pid} = supervisor:start_link({global, UserId}, ?MODULE, [UserId, TimeOut]),
+    %% unlink the process so the one that starts it (usually a connection)
+    %% doesn't bring it down when it ends
+    erlang:unlink(Pid).
+
+%% in a global lock. Make no mistake. Race condition-free.
+init([UserId, TimeOut]) ->
+    MonitorId = {monitor, UserId},
+    ManagerId = {manager, UserId},
+    MonitorSpec = {MonitorId,
+                   {usr_monitor, start_link, [UserId, ManagerId, TimeOut]},
+                   transient,
+                   TimeOut,
+                   worker,
+                   [usr_monitor]},
+    ManagerSpec = {ManagerId,
+                   {usr_dispatch_manager, start_link, [UserId]},
+                   transient,
+                   TimeOut,
+                   worker,
+                   dynamic},
+    {ok, {{one_for_all, 1, TimeOut},
+         [MonitorSpec, ManagerSpec]}}.
+

File src/web_server.erl

+-module(web_server).
+
+-behaviour(gen_server).
+
+-define(SERVER, ?MODULE).
+-define(OK, <<"ok">>).
+
+%% API
+-export([start_link/1, dispatch_requests/1, stop/0]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+     terminate/2, code_change/3]).
+
+start_link(Port) ->
+  gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).
+
+init([Port]) ->
+  process_flag(trap_exit, true),
+  io:format("~p (~p) starting...~n", [?MODULE, self()]),
+  mochiweb_http:start([{port, Port},
+               {loop, fun(Req) ->
+                  dispatch_requests(Req) end}]),
+  {ok, []}.
+
+stop() ->
+  gen_server:cast(?SERVER, stop).
+
+dispatch_requests(Req) ->
+  Path = Req:get(path),
+  Action = clean_path(Path),
+  handle(Action, Req).
+
+handle_call(_Request, _From, State) ->
+  Reply = ok,
+  {reply, Reply, State}.
+
+handle_cast(stop, State) ->
+  {stop, normal, State};
+
+handle_cast(_Msg, State) ->
+  {noreply, State}.
+
+handle_info({'DOWN', _, _, {mochiweb_http, _}, _}, State) ->
+  {stop, normal, State};
+
+handle_info(_Info, State) ->
+  {noreply, State}.
+
+terminate(_Reason, _State) ->
+  mochiweb_http:stop(),
+  ok.
+
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.
+
+%% Internal functions
+handle("/", Req) ->
+    Page = 
+    <<"<html>
+        <head>
+            <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" />
+            <title>Test chat</title>
+            <style type=\"text/css\">
+                .sent { font-style:italic; color:#999; }
+                .received { font-style: italic; color: #333; }
+            </style>
+            <script type=\"text/javascript\" src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js\"></script>
+            <script type=\"text/javascript\" src=\"client.js\"></script>
+        </head>
+        <body>
+            <h1>Chat client demo</h1>
+            <div id=\"content\">
+                <p>Demo par Fred T-H</p>
+            </div>
+        </body>
+    </html>">>,
+    success(Req, Page);
+handle("/client.js", Req) ->
+    Req:serve_file("client.js", ".");
+handle("/favicon.ico", Req) -> %% auto-request from browsers!
+    Req:serve_file("favicon.ico", ".");
+handle("/message", Req) ->
+    Params = Req:parse_qs(),
+    From = proplists:get_value("nick", Params),
+    To = proplists:get_value("to", Params),
+    Message = proplists:get_value("msg", Params),
+    {ok, H} = client:connect(From),
+    client:message(From, To, Message),
+    client:disconnect(From, H),
+    %% we clear the event messages because mochiweb re-uses the same procs
+    %% over and over again in a pool. Otherwise, we leak memory and when the
+    %% listen call gets the right message process, a backlog gets printed to
+    %% the user.
+    lib:flush_receive(),
+    success(Req, ?OK);
+
+handle("/listen", Req) ->
+    Params = Req:parse_qs(),
+    Nick = proplists:get_value("nick", Params),
+    {ok,Handler} = client:connect(Nick),
+    Msgs = client:listen(Nick),
+    Ret = [json_prepare(M) || M <- Msgs],
+    client:disconnect(Nick, Handler),
+    json(Req, Ret);
+
+handle(_, Req) ->
+  error(Req, "").
+
+error(Req, Body) when is_binary(Body) ->
+  Req:respond({500, [{"Content-Type", "text/plain"}], Body}).
+
+success(Req, Body) when is_binary(Body) ->
+  Req:respond({200, [{"Content-Type", "text/html"}], Body}).
+
+json(Req, Body) ->
+  JSON = mochijson2:encode(Body),
+  Req:respond({200, [{"Content-Type", "application/json"}], JSON}).
+
+clean_path(Path) ->
+  case string:str(Path, "?") of
+    0 ->
+      Path;
+    N ->
+      string:substr(Path, 1, string:len(Path) - (N + 1))
+  end.
+
+%% have to convert lists to binaries to be strings in json
+%% also gotta change the internal format of things to make it fit
+json_prepare({Verb=[_|_],To,Msg}) -> json_prepare({unicode:characters_to_binary(Verb),To,Msg});
+json_prepare({Verb,To=[_|_],Msg}) -> json_prepare({Verb,unicode:characters_to_binary(To),Msg});
+json_prepare({Verb,To,Msg=[_|_]}) -> json_prepare({Verb,To,unicode:characters_to_binary(Msg)});
+json_prepare({Verb,To,Msg}) ->
+    {struct, [{action,Verb},{to,To},{message,Msg}]}.