%%%---------------------------------------------------------------------- %%% File : mod_cron.erl %%% Author : Badlop %%% Purpose : Execute scheduled tasks %%% Created : 12 July 2007 %%% Id : $Id: mod_cron.erl 1034 2009-11-17 21:44:17Z badlop $ %%%---------------------------------------------------------------------- -module(mod_cron). -author('badlop@ono.com'). -behaviour(gen_mod). -export([ cron_list/1, cron_del/1, run_task/3, web_menu_host/3, web_page_host/3, start/2, apply_interval/3, apply_interval1/3, stop/1]). -include("ejabberd_commands.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("logger.hrl"). -include("xmpp.hrl"). -record(task, {taskid, timerref, host, task}). %% --------------------- %% gen_mod %% --------------------- start(Host, Opts) -> ejabberd_commands:register_commands(commands()), ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50), Tasks = gen_mod:get_opt(tasks, Opts, []), catch ets:new(cron_tasks, [ordered_set, named_table, public, {keypos, 2}]), [add_task(Host, Task) || Task <- Tasks], ok. stop(Host) -> ejabberd_commands:unregister_commands(commands()), ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50), %% Delete tasks of this host [delete_task(Task) || Task <- get_tasks(Host)], ok. %% --------------------- %% Task management %% --------------------- time_to_ms(IntervalUnit, IntervalNum) -> case IntervalUnit of seconds -> timer:seconds(IntervalNum); minutes -> timer:minutes(IntervalNum); hours -> timer:hours(IntervalNum); days -> timer:hours(IntervalNum)*24 end. time_until_event(IntervalMS) -> NowMS = p1_time_compat:system_time(micro_seconds), MSSinceLastEvent = (NowMS rem IntervalMS), (IntervalMS - MSSinceLastEvent). begin_interval_timer(TaskId, TimeUnit, TimeNum, StartParams) -> IntervalMS = time_to_ms(TimeUnit, TimeNum), MSToGo = time_until_event(IntervalMS), {ok, TimerRef} = timer:apply_after(MSToGo, ?MODULE, apply_interval, [TaskId, IntervalMS, StartParams]), TimerRef. begin_fixed_timer(TaskId, TimeUnit, TimeNum, StartParams) -> %% A fixed second timer happens minutely, minute timer happens hourly, a fixed hour timer happens daily. IntervalMS = case TimeUnit of seconds -> timer:minutes(1); minutes -> timer:hours(1); hours -> timer:hours(1) * 24; _ -> undefined end, FixedTimeMS = time_to_ms(TimeUnit, TimeNum), %% Calculate time until the next IntervalUnit, then add FixedTimeMS %% e.g. now is 00:00:32 wait until the next minute (28s), then keep waiting %% 5 more seconds to get 00:01:05 (= wait 33s). %% We then fire the event at 00:01:05 and wait a minute to fire again %% at 00:02:05 etc. MSToGo1 = time_until_event(IntervalMS) + FixedTimeMS, %% If we were, for example, at 1:03PM and the event is hourly at %% 1:05PM then we dont want to wait 57+5 minutes, we want to wait %% 2 minutes. MSToGo2 = if MSToGo1 > IntervalMS -> MSToGo1 - IntervalMS; true -> MSToGo1 end, ?DEBUG("MS To Go Fixed: ~p ~p", [MSToGo1, MSToGo2]), {ok, TimerRef} = timer:apply_after(MSToGo2, ?MODULE, apply_interval, [TaskId, IntervalMS, StartParams]), TimerRef. apply_interval(TaskId, IntervalMS, StartParams) -> %% apply_after doesnt belong to a pid (which is needed for apply_after to stay alive), so make one spawn(?MODULE, apply_interval1, [TaskId, IntervalMS, StartParams]). apply_interval1(TaskId, IntervalMS, [M, F, A]=StartParams) -> % we've already waited for the interval to expire once to get here, % and apply_interval doesn't apply first, so run the task once then start the timer run_task(M, F, A), {ok, TimerRef} = timer:apply_interval(IntervalMS, ?MODULE, run_task, StartParams), update_timer_ref(TaskId, TimerRef), %% Wait forever so the timer process stays alive receive _ -> ok end. update_timer_ref(TaskId, NewTimerRef) -> [Task] = ets:lookup(cron_tasks, TaskId), NewTask = Task#task{timerref=NewTimerRef}, ets:insert(cron_tasks, NewTask). %% Method to add new task add_task(Host, Task) -> [TimeNum, TimeUnit, Mod, Fun, Args, InTimerType] = [proplists:get_value(Key, Task) || Key <- [time, units, module, function, arguments, timer_type]], TimerType = case InTimerType of <<"fixed">> -> fixed; fixed -> fixed; _ -> interval end, %% Get new task identifier TaskId = get_new_taskid(), TimerRef = case TimerType of interval -> begin_interval_timer(TaskId, TimeUnit, TimeNum, [Mod, Fun, Args]); fixed -> begin_fixed_timer(TaskId, TimeUnit, TimeNum, [Mod, Fun, Args]) end, %% Store TRef Taskr = #task{ taskid = TaskId, timerref = TimerRef, host = Host, task = Task }, ets:insert(cron_tasks, Taskr). get_new_taskid() -> case ets:last(cron_tasks) of '$end_of_table' -> 0; Id -> Id + 1 end. %% Method to run existing task run_task(Mod, Fun, Args) -> case catch apply(Mod, Fun, Args) of {'EXIT', Reason} -> ?ERROR_MSG("Error in scheduled task ~p:~p~p:~n~p", [Mod, Fun, Args, Reason]); {error, Reason} -> ?ERROR_MSG("Error in scheduled task ~p:~p~p:~n~p", [Mod, Fun, Args, Reason]); ok -> ?INFO_MSG("Scheduled task ~p:~p~p finished ok", [Mod, Fun, Args]); Res -> ?INFO_MSG("Scheduled task ~p:~p~p returned:~n~p", [Mod, Fun, Args, Res]) end. %% Method to delete task, given a taskid delete_taskid(TaskId) -> [Task] = ets:lookup(cron_tasks, TaskId), delete_task(Task). %% Method to delete task, given the whole task delete_task(Task) -> timer:cancel(Task#task.timerref), ets:delete(cron_tasks, Task#task.taskid). %% Method to know existing tasks on a given host get_tasks(Host) -> ets:select(cron_tasks, [{#task{host = Host, _ = '_'}, [], ['$_']}]). %% Method to know taskids of existing tasks on a given host %%get_tasks_ids(Host) -> %% L = ets:match(cron_tasks, #task{host = Host, taskid = '$1', _ = '_'}), %% [Id || [Id] <- L]. %% --------------------- %% ejabberd commands %% --------------------- commands() -> [ #ejabberd_commands{name = cron_list, tags = [cron], desc = "List tasks scheduled in a host", module = ?MODULE, function = cron_list, args = [{host, binary}], result = {tasks, {list, {task, {tuple, [{id, integer}, {task, string}]}}}}}, #ejabberd_commands{name = cron_del, tags = [cron], desc = "Delete this task from the schedule", module = ?MODULE, function = cron_del, args = [{taskid, integer}], result = {res, rescode}} ]. cron_list(Host) -> Tasks = get_tasks(Host), [{T#task.taskid, io_lib:format("~p", [T#task.task])} || T <- Tasks]. cron_del(TaskId) -> delete_taskid(TaskId), ok. %% --------------------- %% Web Admin %% --------------------- web_menu_host(Acc, _Host, Lang) -> [{<<"cron">>, ?T(<<"Cron Tasks">>)} | Acc]. web_page_host(_, Host, #request{path = [<<"cron">>], lang = Lang} = _Request) -> Tasks = get_tasks(Host), Tasks_table = make_tasks_table(Tasks, Lang), Res = [?XC(<<"h1">>, <<"Cron Tasks">>)] ++ Tasks_table, {stop, Res}; web_page_host(Acc, _, _) -> Acc. make_tasks_table(Tasks, Lang) -> TList = lists:map( fun(T) -> {Time_num, Time_unit, Mod, Fun, Args} = T#task.task, ?XE(<<"tr">>, [?XC(<<"td">>, list_to_binary(integer_to_list(Time_num) ++" " ++ atom_to_list(Time_unit))), ?XC(<<"td">>, list_to_binary(atom_to_list(Mod))), ?XC(<<"td">>, list_to_binary(atom_to_list(Fun))), ?XC(<<"td">>, list_to_binary(io_lib:format("~p", [Args])))]) end, Tasks), [?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, <<"Periodicity">>), ?XCT(<<"td">>, <<"Module">>), ?XCT(<<"td">>, <<"Function">>), ?XCT(<<"td">>, <<"Arguments">>)])]), ?XE(<<"tbody">>, TList)])].