aboutsummaryrefslogtreecommitdiff
path: root/learn-you-some-erlang/reminder/src/evserv.erl
diff options
context:
space:
mode:
Diffstat (limited to 'learn-you-some-erlang/reminder/src/evserv.erl')
-rw-r--r--learn-you-some-erlang/reminder/src/evserv.erl168
1 files changed, 168 insertions, 0 deletions
diff --git a/learn-you-some-erlang/reminder/src/evserv.erl b/learn-you-some-erlang/reminder/src/evserv.erl
new file mode 100644
index 0000000..1087376
--- /dev/null
+++ b/learn-you-some-erlang/reminder/src/evserv.erl
@@ -0,0 +1,168 @@
+%% Event server
+-module(evserv).
+-compile(export_all).
+
+-record(state, {events, %% list of #event{} records
+ clients}). %% list of Pids
+
+-record(event, {name="",
+ description="",
+ pid,
+ timeout={{1970,1,1},{0,0,0}}}).
+
+%%% User Interface
+
+start() ->
+ register(?MODULE, Pid=spawn(?MODULE, init, [])),
+ Pid.
+
+start_link() ->
+ register(?MODULE, Pid=spawn_link(?MODULE, init, [])),
+ Pid.
+
+terminate() ->
+ ?MODULE ! shutdown.
+
+init() ->
+ %% Loading events from a static file could be done here.
+ %% You would need to pass an argument to init (maybe change the functions
+ %% start/0 and start_link/0 to start/1 and start_link/1) telling where the
+ %% resource to find the events is. Then load it from here.
+ %% Another option is to just pass the event straight to the server
+ %% through this function.
+ loop(#state{events=orddict:new(),
+ clients=orddict:new()}).
+
+subscribe(Pid) ->
+ Ref = erlang:monitor(process, whereis(?MODULE)),
+ ?MODULE ! {self(), Ref, {subscribe, Pid}},
+ receive
+ {Ref, ok} ->
+ {ok, Ref};
+ {'DOWN', Ref, process, _Pid, Reason} ->
+ {error, Reason}
+ after 5000 ->
+ {error, timeout}
+ end.
+
+add_event(Name, Description, TimeOut) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
+ receive
+ {Ref, Msg} -> Msg
+ after 5000 ->
+ {error, timeout}
+ end.
+
+add_event2(Name, Description, TimeOut) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {add, Name, Description, TimeOut}},
+ receive
+ {Ref, {error, Reason}} -> erlang:error(Reason);
+ {Ref, Msg} -> Msg
+ after 5000 ->
+ {error, timeout}
+ end.
+
+cancel(Name) ->
+ Ref = make_ref(),
+ ?MODULE ! {self(), Ref, {cancel, Name}},
+ receive
+ {Ref, ok} -> ok
+ after 5000 ->
+ {error, timeout}
+ end.
+
+listen(Delay) ->
+ receive
+ M = {done, _Name, _Description} ->
+ [M | listen(0)]
+ after Delay*1000 ->
+ []
+ end.
+
+%%% The Server itself
+
+loop(S=#state{}) ->
+ receive
+ {Pid, MsgRef, {subscribe, Client}} ->
+ Ref = erlang:monitor(process, Client),
+ NewClients = orddict:store(Ref, Client, S#state.clients),
+ Pid ! {MsgRef, ok},
+ loop(S#state{clients=NewClients});
+ {Pid, MsgRef, {add, Name, Description, TimeOut}} ->
+ case valid_datetime(TimeOut) of
+ true ->
+ EventPid = event:start_link(Name, TimeOut),
+ NewEvents = orddict:store(Name,
+ #event{name=Name,
+ description=Description,
+ pid=EventPid,
+ timeout=TimeOut},
+ S#state.events),
+ Pid ! {MsgRef, ok},
+ loop(S#state{events=NewEvents});
+ false ->
+ Pid ! {MsgRef, {error, bad_timeout}},
+ loop(S)
+ end;
+ {Pid, MsgRef, {cancel, Name}} ->
+ Events = case orddict:find(Name, S#state.events) of
+ {ok, E} ->
+ event:cancel(E#event.pid),
+ orddict:erase(Name, S#state.events);
+ error ->
+ S#state.events
+ end,
+ Pid ! {MsgRef, ok},
+ loop(S#state{events=Events});
+ {done, Name} ->
+ case orddict:find(Name, S#state.events) of
+ {ok, E} ->
+ send_to_clients({done, E#event.name, E#event.description},
+ S#state.clients),
+ NewEvents = orddict:erase(Name, S#state.events),
+ loop(S#state{events=NewEvents});
+ error ->
+ %% This may happen if we cancel an event and
+ %% it fires at the same time
+ loop(S)
+ end;
+ shutdown ->
+ exit(shutdown);
+ {'DOWN', Ref, process, _Pid, _Reason} ->
+ loop(S#state{clients=orddict:erase(Ref, S#state.clients)});
+ code_change ->
+ ?MODULE:loop(S);
+ {Pid, debug} -> %% used as a hack to let me do some unit testing
+ Pid ! S,
+ loop(S);
+ Unknown ->
+ io:format("Unknown message: ~p~n",[Unknown]),
+ loop(S)
+ end.
+
+
+%%% Internal Functions
+send_to_clients(Msg, ClientDict) ->
+ orddict:map(fun(_Ref, Pid) -> Pid ! Msg end, ClientDict).
+
+valid_datetime({Date,Time}) ->
+ try
+ calendar:valid_date(Date) andalso valid_time(Time)
+ catch
+ error:function_clause -> %% not in {{Y,M,D},{H,Min,S}} format
+ false
+ end;
+valid_datetime(_) ->
+ false.
+
+%% calendar has valid_date, but nothing for days.
+%% This function is based on its interface.
+%% Ugly, but ugh.
+valid_time({H,M,S}) -> valid_time(H,M,S).
+
+valid_time(H,M,S) when H >= 0, H < 24,
+ M >= 0, M < 60,
+ S >= 0, S < 60 -> true;
+valid_time(_,_,_) -> false.