%% @author Arjan Scherpenisse <arjan@scherpenisse.net>
%% @copyright 2011-2015 Arjan Scherpenisse <arjan@scherpenisse.net>
%% Date: 2011-10-12
%% @doc Watch for changed files using inotifywait.
%% https://github.com/rvoicilas/inotify-tools/wiki
%% Copyright 2011-2015 Arjan Scherpenisse
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(zotonic_filewatcher_inotify).
-author("Arjan Scherpenisse <arjan@scherpenisse.net>").
-behaviour(gen_server).
%% gen_server exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([start_link/0]).
-record(state, {
pid :: pid() | undefined,
port :: integer() | undefined,
executable :: string()
}).
%% interface functions
-export([
is_installed/0,
restart/0
]).
-include_lib("kernel/include/logger.hrl").
%%====================================================================
%% API
%%====================================================================
%% @doc Starts the server
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
start_link() ->
case os:find_executable("inotifywait") of
false ->
{error, "inotifywait not found"};
Executable ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [Executable], [])
end.
-spec is_installed() -> boolean().
is_installed() ->
os:find_executable("inotifywait") =/= false.
-spec restart() -> ok.
restart() ->
gen_server:cast(?MODULE, restart).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @doc Initiates the server.
init([Executable]) ->
process_flag(trap_exit, true),
State = #state{
executable = Executable,
port = undefined,
pid = undefined
},
timer:send_after(100, start),
{ok, State}.
%% @doc Trap unknown calls
handle_call(Message, _From, State) ->
{stop, {unknown_call, Message}, State}.
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
handle_cast(restart, #state{ pid = undefined } = State) ->
{noreply, State};
handle_cast(restart, #state{ pid = Pid } = State) when is_pid(Pid) ->
?LOG_INFO("[inotify] Stopping inotify file monitor."),
catch exec:stop(Pid),
{noreply, start_inotify(State#state{ port = undefined })};
handle_cast(Message, State) ->
{stop, {unknown_cast, Message}, State}.
%% @doc Reading a line from the inotifywait program. Sets a timer to
%% prevent duplicate file changed message for the same filename
%% (e.g. if a editor saves a file twice for some reason).
handle_info({stdout, _Port, Data}, #state{} = State) ->
Lines = binary:split(
binary:replace(Data, <<"\r\n">>, <<"\n">>, [global]),
<<"\n">>,
[global]),
lists:map(
fun(Line) ->
case re:run(Line, "^(.+) (MODIFY|CREATE|DELETE|MOVED_TO|MOVED_FROM) (.+)", [{capture, all_but_first, binary}]) of
nomatch ->
ok;
{match, [Path, Verb, File]} ->
Filename = filename:join(Path, File),
zotonic_filewatcher_handler:file_changed(verb(Verb), Filename)
end
end,
Lines),
{noreply, State};
handle_info({'DOWN', _Port, process, Pid, Reason}, #state{pid = Pid} = State) ->
?LOG_ERROR(#{
text => <<"[inotify] inotify port closed, restarting in 5 seconds.">>,
result => error,
reason => Reason
}),
State1 = State#state{
pid = undefined,
port = undefined
},
timer:send_after(5000, start),
{noreply, State1};
handle_info({'EXIT', _Pid, _Reason}, State) ->
{noreply, State};
handle_info(start, #state{ port = undefined } = State) ->
{noreply, start_inotify(State)};
handle_info(start, State) ->
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
%% @spec terminate(Reason, State) -> void()
%% @doc 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{pid = undefined}) ->
ok;
terminate(_Reason, #state{pid = Pid}) ->
catch exec:stop(Pid),
ok.
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @doc Convert process state when code is changed
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%====================================================================
%% support functions
%%====================================================================
start_inotify(#state{executable = Executable, port = undefined} = State) ->
?LOG_INFO("[inotify] Starting inotify file monitor."),
Args = [
Executable,
"-q", "-e", "modify,create,delete,moved_to,moved_from", "-m", "-r",
"--exclude", zotonic_filewatcher_handler:re_exclude()
]
++ zotonic_filewatcher_sup:watch_dirs_expanded(),
{ok, Pid, Port} = exec:run_link(Args, [stdout, monitor]),
State#state{
port = Port,
pid = Pid
}.
verb(<<"CREATE">>) -> create;
verb(<<"MODIFY">>) -> modify;
verb(<<"DELETE">>) -> delete;
verb(<<"MOVED_FROM">>) -> delete;
verb(<<"MOVED_TO">>) -> create.