bbce92ca46b4807f0fa83b3fd9d536c7d40d6fb1
[erlang-gen_inotify.git] / src / gen_inotify.erl
1 %%%---------------------------------------------------------------------------
2 %%% @doc
3 %%%   `inotify(7)' bindings for Erlang.
4 %%% @end
5 %%%---------------------------------------------------------------------------
6
7 -module(gen_inotify).
8
9 %% public interface
10 -export([open/0, open/1, close/1]).
11 -export([add/3, update/3, remove/2, list/1]).
12 -export([controlling_process/2]).
13 -export([format_error/1]).
14
15 -export_type([handle/0, message/0, posix/0]).
16 -export_type([flag/0, flag_event/0]).
17
18 %%%---------------------------------------------------------------------------
19 %%% types
20
21 -define(DRIVER_NAME, "gen_inotify_drv").
22
23 -type handle() :: port().
24
25 -type cookie() :: non_neg_integer().
26
27 -type message() ::
28     {inotify, handle(), file:filename() | undefined, cookie(),
29       [flag() | flag_event(), ...]}
30   | {inotify_error, handle(), queue_overflow | posix()}.
31 %% Filename is an absolute path. `undefined' should never happen.
32 %%
33 %% Flags `unmount' and `watch_removed' are always sent with no other
34 %% accompanying flag.
35 %%
36 %% If `is_dir' flag is present, it's always the first one.
37
38 -type flag() :: access
39               | modify
40               | attrib
41               | create
42               | delete
43               | open
44               | close_write | close_nowrite
45               | move_from | move_to
46               | delete_self
47               | move_self.
48
49 -type flag_event() :: watch_removed
50                     | is_dir
51                     | unmount.
52 %% See {@type message()} for details about these flags' positions and company.
53
54 -type posix() :: inet:posix().
55
56 %%%---------------------------------------------------------------------------
57 %%% public interface
58 %%%---------------------------------------------------------------------------
59
60 %% @doc Open a new inotify handle.
61
62 -spec open() ->
63   {ok, handle()} | {error, system_limit | posix()}.
64
65 open() ->
66   open([]).
67
68 %% @doc Open a new inotify handle.
69
70 -spec open(Options :: [Option]) ->
71   {ok, handle()} | {error, badarg | system_limit | posix()}
72   when Option :: recursive | real_path.
73
74 open(Options) ->
75   case build_open_options_data(Options) of
76     {ok, OptData} ->
77       try open_port({spawn_driver, ?DRIVER_NAME}, [binary]) of
78         Handle ->
79           port_control(Handle, 0, OptData),
80           {ok, Handle}
81       catch
82         error:Reason ->
83           {error, Reason}
84       end;
85     {error, badarg} ->
86       {error, badarg}
87   end.
88
89 %%----------------------------------------------------------
90 %% build_open_options_data() {{{
91
92 %% @doc Translate list of options to a `port_control(Port,0,_)' request
93 %%   payload.
94
95 -spec build_open_options_data(Options :: [Option]) ->
96   {ok, {Recursive :: boolean(), UseRealPath :: boolean()}} | {error, badarg}
97   when Option :: recursive | real_path.
98
99 build_open_options_data(Options) ->
100   DefaultOptions = {false, false}, % `{Recursive, UseRealPath}'
101   try lists:foldr(fun add_open_option/2, DefaultOptions, Options) of
102     {false, false} -> {ok, <<0:8, 0:8>>};
103     {true,  false} -> {ok, <<1:8, 0:8>>};
104     {false, true}  -> {ok, <<0:8, 1:8>>};
105     {true,  true}  -> {ok, <<1:8, 1:8>>}
106   catch
107     _:_ -> {error, badarg}
108   end.
109
110 %% @doc Workhorse for {@link build_open_options_data/1}.
111
112 -spec add_open_option(Option, {boolean(), boolean()}) ->
113   {boolean(), boolean()}
114   when Option :: recursive | real_path.
115
116 add_open_option(recursive, {_Recursive, UseRealPath}) -> {true, UseRealPath};
117 add_open_option(real_path, {Recursive, _UseRealPath}) -> {Recursive, true}.
118
119 %% }}}
120 %%----------------------------------------------------------
121
122 %% @doc Close a handle.
123
124 -spec close(handle()) ->
125   ok.
126
127 close(Handle) ->
128   try
129     unlink(Handle),
130     port_close(Handle)
131   catch
132     % this could be caused by port already being closed, which is expected in
133     % some cases
134     error:badarg -> ignore
135   end,
136   ok.
137
138 %% @doc Assign a new owner to a handle.
139
140 -spec controlling_process(handle(), pid()) ->
141   ok | {error, not_owner | closed | badarg}.
142
143 controlling_process(Handle, Pid) ->
144   try erlang:port_info(Handle, connected) of
145     {connected, Pid} ->
146       ok; % already the owner
147     {connected, Owner} when Owner /= self() ->
148       {error, not_owner};
149     {connected, _OldOwner} ->
150       try
151         port_connect(Handle, Pid),
152         unlink(Handle),
153         ok
154       catch
155         _:_ ->
156           {error, closed}
157       end;
158     undefined ->
159       {error, closed}
160   catch
161     _:_ ->
162       {error, badarg}
163   end.
164
165 %% @doc Monitor a file or directory.
166 %%
167 %%   If the path was already watched, any previous flags will be replaced.
168 %%
169 %%   `close' and `move' flags are shorthands for `close_write'
170 %%   + `close_nowrite' and for `move_from' + `move_to', respectively.
171
172 -spec add(handle(), file:filename(), [flag() | close | move | Option]) ->
173   ok | {error, badarg | posix()}
174   when Option :: follow_symlink | unwatch_on_unlink | once | if_dir.
175
176 add(Handle, Path, Flags) ->
177   case build_flags(Flags, add) of
178     {ok, Value} ->
179       try port_control(Handle, 1, [<<Value:32>>, Path]) of
180         <<>> -> ok;
181         ErrorName -> {error, binary_to_atom(ErrorName, latin1)}
182       catch
183         _:_ -> {error, badarg}
184       end;
185     {error, badarg} ->
186       {error, badarg}
187   end.
188
189 %% @doc Add events to watch for for a file or directory.
190 %%
191 %%   If the path was not watched yet, it's added.
192 %%
193 %%   `close' and `move' flags are shorthands for `close_write'
194 %%   + `close_nowrite' and for `move_from' + `move_to', respectively.
195
196 -spec update(handle(), file:filename(), [flag() | close | move | Option]) ->
197   ok | {error, badarg | posix()}
198   when Option :: follow_symlink | unwatch_on_unlink | once | if_dir.
199
200 update(Handle, Path, Flags) ->
201   case build_flags(Flags, update) of
202     {ok, Value} ->
203       try port_control(Handle, 1, [<<Value:32>>, Path]) of
204         <<>> -> ok;
205         ErrorName -> {error, binary_to_atom(ErrorName, latin1)}
206       catch
207         _:_ -> {error, badarg}
208       end;
209     {error, badarg} ->
210       {error, badarg}
211   end.
212
213 %%----------------------------------------------------------
214 %% build_flags() {{{
215
216 %% @doc Translate list of flags to an integer to be sent to port.
217
218 -spec build_flags(Flags :: [Flag], add | update) ->
219   {ok, non_neg_integer()} | {error, badarg}
220   when Flag :: flag() | follow_symlink | unwatch_on_unlink | once | if_dir.
221
222 build_flags(Flags, Op) ->
223   % NOTE: start with "don't follow symlinks" set and subtract it if
224   % `follow_symlink' flag is present
225   try lists:foldl(fun add_flag/2, 16#1000, Flags) of
226     Result when Op == add    -> {ok, Result};
227     Result when Op == update -> {ok, Result bor 16#010000}
228   catch
229     _:_ -> {error, badarg}
230   end.
231
232 %% @doc Workhorse for {@link build_flags/1}.
233
234 -spec add_flag(Flag, non_neg_integer()) ->
235   non_neg_integer()
236   when Flag :: flag() | follow_symlink | unwatch_on_unlink | once | if_dir.
237
238 add_flag(access,            Flags) -> Flags bor 16#0001;
239 add_flag(modify,            Flags) -> Flags bor 16#0002;
240 add_flag(attrib,            Flags) -> Flags bor 16#0004;
241 add_flag(create,            Flags) -> Flags bor 16#0008;
242 add_flag(delete,            Flags) -> Flags bor 16#0010;
243 add_flag(open,              Flags) -> Flags bor 16#0020;
244 add_flag(close,             Flags) -> Flags bor 16#0040 bor 16#0080;
245 add_flag(close_write,       Flags) -> Flags bor 16#0040;
246 add_flag(close_nowrite,     Flags) -> Flags bor 16#0080;
247 add_flag(move,              Flags) -> Flags bor 16#0100 bor 16#0200;
248 add_flag(move_from,         Flags) -> Flags bor 16#0100;
249 add_flag(move_to,           Flags) -> Flags bor 16#0200;
250 add_flag(delete_self,       Flags) -> Flags bor 16#0400;
251 add_flag(move_self,         Flags) -> Flags bor 16#0800;
252 add_flag(follow_symlink,    Flags) -> Flags band bnot 16#1000; % reverse flag
253 add_flag(unwatch_on_unlink, Flags) -> Flags bor 16#2000;
254 add_flag(once,              Flags) -> Flags bor 16#4000;
255 add_flag(if_dir,            Flags) -> Flags bor 16#8000.
256
257 %% }}}
258 %%----------------------------------------------------------
259
260 %% @doc Stop watching a file or directory.
261 %%
262 %%   After this function is called, the owner will receive {@type message()}
263 %%   with `watch_removed' flag.
264
265 -spec remove(handle(), file:filename()) ->
266   ok | {error, badarg}.
267
268 remove(Handle, Path) ->
269   try port_control(Handle, 2, Path) of
270     <<>> -> ok;
271     ErrorName -> {error, binary_to_atom(ErrorName, latin1)}
272   catch
273     _:_ -> {error, badarg}
274   end.
275
276 %% @doc List watched paths.
277
278 -spec list(handle()) ->
279   {ok, [Entry]} | {error, badarg}
280   when Entry :: {file:filename(), [flag()]}.
281
282 list(Handle) ->
283   try port_control(Handle, 3, <<>>) of
284     <<Count:32>> ->
285       Entries = receive_listing(Handle, Count, []),
286       {ok, Entries}
287   catch
288     _:_ -> {error, badarg}
289   end.
290
291 %% @doc Workhorse for {@link list/1}.
292
293 -spec receive_listing(handle(), non_neg_integer(), [Entry]) ->
294   [Entry]
295   when Entry :: {file:filename(), [flag()]}.
296
297 receive_listing(_Handle, 0 = _Count, Entries) ->
298   Entries;
299 receive_listing(Handle, Count, Entries) when Count > 0 ->
300   receive
301     {inotify_listing, Handle, _WD, Path, Flags} ->
302       receive_listing(Handle, Count - 1, [{Path, Flags} | Entries])
303   end.
304
305 %%%---------------------------------------------------------------------------
306
307 %% @doc Format a reason from error tuple as a usable error message.
308
309 -spec format_error(Reason :: term()) ->
310   string().
311
312 format_error(queue_overflow) ->
313   "overflow in inotify event queue";
314 format_error(badarg) ->
315   "bad argument";
316 format_error(closed) ->
317   "file descriptor closed";
318 format_error(Errno) ->
319   inet:format_error(Errno).
320
321 %%%---------------------------------------------------------------------------
322 %%% vim:ft=erlang:foldmethod=marker