%%%------------------------------------------------------------------- %% @doc Watches the XDG application directories for changes. %% %% Starts one `fs' (native inotify) watch per existing directory %% returned by `er_xdg_pipe_menu_scanner:data_dirs/0' and subscribes %% to its file events. On any event, a debounce timer is (re)started; %% once it fires with no further events in the window, the menu cache %% is invalidated so the next request gets a freshly rebuilt menu. %% %% `fs' watches recursively (it drives `inotifywait -r' on Linux), so %% changes in vendor subdirectories (e.g. `applications/kde4/*') are %% picked up without any extra handling here. %% @end %%%------------------------------------------------------------------- -module(er_xdg_pipe_menu_watcher). -behaviour(gen_server). -export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). -define(DEFAULT_DEBOUNCE_MS, 500). -spec start_link() -> {ok, pid()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> warn_if_inotifywait_missing(), Dirs = [D || D <- er_xdg_pipe_menu_scanner:data_dirs(), filelib:is_dir(D)], lists:foreach(fun watch/1, lists:zip(lists:seq(1, length(Dirs)), Dirs)), {ok, #{timer => undefined}}. watch({Index, Dir}) -> Name = list_to_atom("er_xdg_pipe_menu_fs_" ++ integer_to_list(Index)), case fs:start_link(Name, Dir) of {ok, _Pid} -> fs:subscribe(Name); {error, Reason} -> log_watch_failure(Dir, Reason) end. %% `fs' on Linux shells out to the system `inotifywait' binary; if %% it's missing, `fs:start_link/2' still returns `{ok, _}' but simply %% never delivers events, which is otherwise silent. warn_if_inotifywait_missing() -> case os:find_executable("inotifywait") of false -> error_logger:warning_msg( "er_xdg_pipe_menu_watcher: 'inotifywait' not found on PATH " "(install inotify-tools) -- desktop file changes won't be " "picked up until the daemon is restarted~n", [] ); _Path -> ok end. log_watch_failure(Dir, Reason) -> error_logger:warning_msg( "er_xdg_pipe_menu_watcher: failed to watch ~p: ~p~n", [Dir, Reason] ). handle_call(_Request, _From, State) -> {reply, ok, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info({_Pid, {fs, file_event}, {_Path, _Events}}, State) -> {noreply, reset_debounce(State)}; handle_info(debounce_fire, State) -> er_xdg_pipe_menu_cache:invalidate(), {noreply, State#{timer => undefined}}; handle_info(_Info, State) -> {noreply, State}. reset_debounce(#{timer := Timer} = State) -> case Timer of undefined -> ok; Ref -> erlang:cancel_timer(Ref) end, State#{timer => erlang:send_after(debounce_ms(), self(), debounce_fire)}. debounce_ms() -> application:get_env(er_xdg_pipe_menu, debounce_ms, ?DEFAULT_DEBOUNCE_MS).