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