PageRenderTime 23ms CodeModel.GetById 13ms app.highlight 8ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/mod_development/z_filewatcher_inotify.erl

http://github.com/zotonic/zotonic
Erlang | 165 lines | 79 code | 34 blank | 52 comment | 0 complexity | 0d2a231b1b451f36b3ad86425a8184fd MD5 | raw file
  1%% @author Arjan Scherpenisse <arjan@scherpenisse.net>
  2%% @copyright 2011 Arjan Scherpenisse <arjan@scherpenisse.net>
  3%% Date: 2011-10-12
  4
  5%% @doc Watch for changed files using inotifywait.
  6
  7%% Copyright 2011 Arjan Scherpenisse
  8%%
  9%% Licensed under the Apache License, Version 2.0 (the "License");
 10%% you may not use this file except in compliance with the License.
 11%% You may obtain a copy of the License at
 12%% 
 13%%     http://www.apache.org/licenses/LICENSE-2.0
 14%% 
 15%% Unless required by applicable law or agreed to in writing, software
 16%% distributed under the License is distributed on an "AS IS" BASIS,
 17%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 18%% See the License for the specific language governing permissions and
 19%% limitations under the License.
 20
 21-module(z_filewatcher_inotify).
 22-author("Arjan Scherpenisse <arjan@scherpenisse.net>").
 23
 24-include_lib("include/zotonic.hrl").
 25
 26-behaviour(gen_server).
 27
 28%% gen_server exports
 29-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
 30-export([start_link/1]).
 31
 32-record(state, {port, executable, context, timers=[]}).
 33
 34%% interface functions
 35-export([
 36]).
 37
 38
 39%%====================================================================
 40%% API
 41%%====================================================================
 42%% @spec start_link(#context{}) -> {ok,Pid} | ignore | {error,Error}
 43%% @doc Starts the server
 44start_link(Context=#context{}) ->
 45    case os:cmd("which inotifywait 2>/dev/null") of
 46        [] ->
 47            {error, "inotifywait not found"};
 48        Output ->
 49            case whereis(?MODULE) of
 50                undefined ->
 51                    Executable = hd(string:tokens(Output, "\n")),
 52                    gen_server:start_link({local, ?MODULE}, ?MODULE, [Executable, Context], []);
 53                Pid ->
 54                    {ok, Pid}
 55            end
 56    end.
 57
 58
 59%%====================================================================
 60%% gen_server callbacks
 61%%====================================================================
 62
 63%% @spec init(Args) -> {ok, State} |
 64%%                     {ok, State, Timeout} |
 65%%                     ignore               |
 66%%                     {stop, Reason}
 67%% @doc Initiates the server.
 68init([Executable, Context]) ->
 69    process_flag(trap_exit, true),
 70    State = #state{context=Context, executable=Executable},
 71    {ok, State, 0}.
 72
 73
 74%% @doc Trap unknown calls
 75handle_call(Message, _From, State) ->
 76    {stop, {unknown_call, Message}, State}.
 77
 78
 79%% @spec handle_cast(Msg, State) -> {noreply, State} |
 80%%                                  {noreply, State, Timeout} |
 81%%                                  {stop, Reason, State}
 82%% @doc Trap unknown casts
 83handle_cast(Message, State) ->
 84    {stop, {unknown_cast, Message}, State}.
 85
 86
 87%% @doc Reading a line from the inotifywait program. Sets a timer to
 88%% prevent duplicate file changed message for the same filename
 89%% (e.g. if a editor saves a file twice for some reason).
 90handle_info({Port, {data, {eol, Line}}}, State=#state{port=Port, timers=Timers}) ->
 91    case re:run(Line, "^(.+) (MODIFY|CREATE) (.+)", [{capture, all_but_first, list}]) of
 92        nomatch -> 
 93            {noreply, State};
 94        {match, [Path, Verb, File]} ->
 95            Filename = filename:join(Path, File),
 96            Timers1 = case proplists:lookup(Filename, Timers) of
 97                          {Filename, TRef} ->
 98                              erlang:cancel_timer(TRef),
 99                              proplists:delete(Filename, Timers);
100                          none ->
101                              Timers
102                      end,
103            TRef2 = erlang:send_after(300, self(), {filechange, verb(Verb), Filename}),
104            Timers2 = [{Filename, TRef2} | Timers1],
105            {noreply, State#state{timers=Timers2}}
106    end;
107
108%% @doc Launch the actual filechanged notification
109handle_info({filechange, Verb, Filename}, State=#state{timers=Timers}) ->
110    mod_development:file_changed(Verb, Filename),
111    {noreply, State#state{timers=proplists:delete(Filename, Timers)}};
112
113handle_info({'EXIT', Port, _}, State=#state{port=Port}) ->
114    %% restart after 5 seconds
115    {noreply, State, 5000};
116
117handle_info(timeout, State=#state{context=Context}) ->
118    ?zInfo("Starting inotify file monitor.", Context),
119    {noreply, start_inotify(State)};
120
121handle_info(_Info, State) ->
122    ?DEBUG(_Info),
123    {noreply, State}.
124
125%% @spec terminate(Reason, State) -> void()
126%% @doc This function is called by a gen_server when it is about to
127%% terminate. It should be the opposite of Module:init/1 and do any necessary
128%% cleaning up. When it returns, the gen_server terminates with Reason.
129%% The return value is ignored.
130terminate(_Reason, #state{port=Port}) ->
131    true = erlang:port_close(Port),
132    os:cmd("killall inotifywait"),
133    ok.
134
135%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
136%% @doc Convert process state when code is changed
137
138code_change(_OldVsn, State, _Extra) ->
139    {ok, State}.
140
141
142%%====================================================================
143%% support functions
144%%====================================================================
145
146start_inotify(State=#state{executable=Executable}) ->
147    os:cmd("killall inotifywait"),
148    Args = ["-q", "-e", "modify,create", "-m", "-r", 
149            filename:join(os:getenv("ZOTONIC"), "src"),
150            filename:join(os:getenv("ZOTONIC"), "modules"),
151            
152            filename:join(os:getenv("ZOTONIC"), "priv/sites"),
153            filename:join(os:getenv("ZOTONIC"), "priv/modules"),
154            
155            z_path:user_sites_dir(),
156            z_path:user_modules_dir()
157            |
158            string:tokens(os:cmd("find " ++ z_utils:os_escape(os:getenv("ZOTONIC")) ++ " -type l"), "\n")],
159    Port = erlang:open_port({spawn_executable, Executable}, [{args, Args}, {line, 1024}]),
160    State#state{port=Port}.
161
162
163verb("MODIFY") -> modify;
164verb("CREATE") -> create.
165