1 | /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */ |
2 | |
3 | /* inotify-path.c - GVFS Monitor based on inotify. |
4 | |
5 | Copyright (C) 2006 John McCutchan |
6 | Copyright (C) 2009 Codethink Limited |
7 | |
8 | This library is free software; you can redistribute it and/or |
9 | modify it under the terms of the GNU Lesser General Public |
10 | License as published by the Free Software Foundation; either |
11 | version 2.1 of the License, or (at your option) any later version. |
12 | |
13 | This library is distributed in the hope that it will be useful, |
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
16 | Lesser General Public License for more details. |
17 | |
18 | You should have received a copy of the GNU Lesser General Public License |
19 | along with this library; if not, see <http://www.gnu.org/licenses/>. |
20 | |
21 | Authors: |
22 | John McCutchan <john@johnmccutchan.com> |
23 | Ryan Lortie <desrt@desrt.ca> |
24 | */ |
25 | |
26 | #include "config.h" |
27 | |
28 | /* Don't put conflicting kernel types in the global namespace: */ |
29 | #define __KERNEL_STRICT_NAMES |
30 | |
31 | #include <sys/inotify.h> |
32 | #include <string.h> |
33 | #include <glib.h> |
34 | #include "inotify-kernel.h" |
35 | #include "inotify-path.h" |
36 | #include "inotify-missing.h" |
37 | |
38 | #define IP_INOTIFY_DIR_MASK (IN_MODIFY|IN_ATTRIB|IN_MOVED_FROM|IN_MOVED_TO|IN_DELETE|IN_CREATE|IN_DELETE_SELF|IN_UNMOUNT|IN_MOVE_SELF|IN_CLOSE_WRITE) |
39 | |
40 | #define IP_INOTIFY_FILE_MASK (IN_MODIFY|IN_ATTRIB|IN_CLOSE_WRITE) |
41 | |
42 | /* Older libcs don't have this */ |
43 | #ifndef IN_ONLYDIR |
44 | #define IN_ONLYDIR 0 |
45 | #endif |
46 | |
47 | typedef struct ip_watched_file_s { |
48 | gchar *filename; |
49 | gchar *path; |
50 | gint32 wd; |
51 | |
52 | GList *subs; |
53 | } ip_watched_file_t; |
54 | |
55 | typedef struct ip_watched_dir_s { |
56 | char *path; |
57 | /* TODO: We need to maintain a tree of watched directories |
58 | * so that we can deliver move/delete events to sub folders. |
59 | * Or the application could do it... |
60 | */ |
61 | struct ip_watched_dir_s* parent; |
62 | GList* children; |
63 | |
64 | /* basename -> ip_watched_file_t |
65 | * Maps basename to a ip_watched_file_t if the file is currently |
66 | * being directly watched for changes (ie: 'hardlinks' mode). |
67 | */ |
68 | GHashTable *files_hash; |
69 | |
70 | /* Inotify state */ |
71 | gint32 wd; |
72 | |
73 | /* List of inotify subscriptions */ |
74 | GList *subs; |
75 | } ip_watched_dir_t; |
76 | |
77 | static gboolean ip_debug_enabled = FALSE; |
78 | #define IP_W if (ip_debug_enabled) g_warning |
79 | |
80 | /* path -> ip_watched_dir */ |
81 | static GHashTable * path_dir_hash = NULL; |
82 | /* inotify_sub * -> ip_watched_dir * |
83 | * |
84 | * Each subscription is attached to a watched directory or it is on |
85 | * the missing list |
86 | */ |
87 | static GHashTable * sub_dir_hash = NULL; |
88 | /* This hash holds GLists of ip_watched_dir_t *'s |
89 | * We need to hold a list because symbolic links can share |
90 | * the same wd |
91 | */ |
92 | static GHashTable * wd_dir_hash = NULL; |
93 | /* This hash holds GLists of ip_watched_file_t *'s |
94 | * We need to hold a list because links can share |
95 | * the same wd |
96 | */ |
97 | static GHashTable * wd_file_hash = NULL; |
98 | |
99 | static ip_watched_dir_t *ip_watched_dir_new (const char *path, |
100 | int wd); |
101 | static void ip_watched_dir_free (ip_watched_dir_t *dir); |
102 | static gboolean ip_event_callback (ik_event_t *event); |
103 | |
104 | |
105 | static gboolean (*event_callback)(ik_event_t *event, inotify_sub *sub, gboolean file_event); |
106 | |
107 | gboolean |
108 | _ip_startup (gboolean (*cb)(ik_event_t *event, inotify_sub *sub, gboolean file_event)) |
109 | { |
110 | static gboolean initialized = FALSE; |
111 | static gboolean result = FALSE; |
112 | |
113 | if (initialized == TRUE) |
114 | return result; |
115 | |
116 | event_callback = cb; |
117 | result = _ik_startup (cb: ip_event_callback); |
118 | |
119 | if (!result) |
120 | return FALSE; |
121 | |
122 | path_dir_hash = g_hash_table_new (hash_func: g_str_hash, key_equal_func: g_str_equal); |
123 | sub_dir_hash = g_hash_table_new (hash_func: g_direct_hash, key_equal_func: g_direct_equal); |
124 | wd_dir_hash = g_hash_table_new (hash_func: g_direct_hash, key_equal_func: g_direct_equal); |
125 | wd_file_hash = g_hash_table_new (hash_func: g_direct_hash, key_equal_func: g_direct_equal); |
126 | |
127 | initialized = TRUE; |
128 | return TRUE; |
129 | } |
130 | |
131 | static void |
132 | ip_map_path_dir (const char *path, |
133 | ip_watched_dir_t *dir) |
134 | { |
135 | g_assert (path && dir); |
136 | g_hash_table_insert (hash_table: path_dir_hash, key: dir->path, value: dir); |
137 | } |
138 | |
139 | static void |
140 | ip_map_sub_dir (inotify_sub *sub, |
141 | ip_watched_dir_t *dir) |
142 | { |
143 | /* Associate subscription and directory */ |
144 | g_assert (dir && sub); |
145 | g_hash_table_insert (hash_table: sub_dir_hash, key: sub, value: dir); |
146 | dir->subs = g_list_prepend (list: dir->subs, data: sub); |
147 | } |
148 | |
149 | static void |
150 | ip_map_wd_dir (gint32 wd, |
151 | ip_watched_dir_t *dir) |
152 | { |
153 | GList *dir_list; |
154 | |
155 | g_assert (wd >= 0 && dir); |
156 | dir_list = g_hash_table_lookup (hash_table: wd_dir_hash, GINT_TO_POINTER (wd)); |
157 | dir_list = g_list_prepend (list: dir_list, data: dir); |
158 | g_hash_table_replace (hash_table: wd_dir_hash, GINT_TO_POINTER (dir->wd), value: dir_list); |
159 | } |
160 | |
161 | static void |
162 | ip_map_wd_file (gint32 wd, |
163 | ip_watched_file_t *file) |
164 | { |
165 | GList *file_list; |
166 | |
167 | g_assert (wd >= 0 && file); |
168 | file_list = g_hash_table_lookup (hash_table: wd_file_hash, GINT_TO_POINTER (wd)); |
169 | file_list = g_list_prepend (list: file_list, data: file); |
170 | g_hash_table_replace (hash_table: wd_file_hash, GINT_TO_POINTER (wd), value: file_list); |
171 | } |
172 | |
173 | static void |
174 | ip_unmap_wd_file (gint32 wd, |
175 | ip_watched_file_t *file) |
176 | { |
177 | GList *file_list = g_hash_table_lookup (hash_table: wd_file_hash, GINT_TO_POINTER (wd)); |
178 | |
179 | if (!file_list) |
180 | return; |
181 | |
182 | g_assert (wd >= 0 && file); |
183 | file_list = g_list_remove (list: file_list, data: file); |
184 | if (file_list == NULL) |
185 | g_hash_table_remove (hash_table: wd_file_hash, GINT_TO_POINTER (wd)); |
186 | else |
187 | g_hash_table_replace (hash_table: wd_file_hash, GINT_TO_POINTER (wd), value: file_list); |
188 | } |
189 | |
190 | |
191 | static ip_watched_file_t * |
192 | ip_watched_file_new (const gchar *dirname, |
193 | const gchar *filename) |
194 | { |
195 | ip_watched_file_t *file; |
196 | |
197 | file = g_new0 (ip_watched_file_t, 1); |
198 | file->path = g_strjoin (separator: "/" , dirname, filename, NULL); |
199 | file->filename = g_strdup (str: filename); |
200 | file->wd = -1; |
201 | |
202 | return file; |
203 | } |
204 | |
205 | static void |
206 | ip_watched_file_free (ip_watched_file_t *file) |
207 | { |
208 | g_assert (file->subs == NULL); |
209 | g_free (mem: file->filename); |
210 | g_free (mem: file->path); |
211 | g_free (mem: file); |
212 | } |
213 | |
214 | static void |
215 | ip_watched_file_add_sub (ip_watched_file_t *file, |
216 | inotify_sub *sub) |
217 | { |
218 | file->subs = g_list_prepend (list: file->subs, data: sub); |
219 | } |
220 | |
221 | static void |
222 | ip_watched_file_start (ip_watched_file_t *file) |
223 | { |
224 | if (file->wd < 0) |
225 | { |
226 | gint err; |
227 | |
228 | file->wd = _ik_watch (path: file->path, |
229 | IP_INOTIFY_FILE_MASK, |
230 | err: &err); |
231 | |
232 | if (file->wd >= 0) |
233 | ip_map_wd_file (wd: file->wd, file); |
234 | } |
235 | } |
236 | |
237 | static void |
238 | ip_watched_file_stop (ip_watched_file_t *file) |
239 | { |
240 | if (file->wd >= 0) |
241 | { |
242 | _ik_ignore (path: file->path, wd: file->wd); |
243 | ip_unmap_wd_file (wd: file->wd, file); |
244 | file->wd = -1; |
245 | } |
246 | } |
247 | |
248 | gboolean |
249 | _ip_start_watching (inotify_sub *sub) |
250 | { |
251 | gint32 wd; |
252 | int err; |
253 | ip_watched_dir_t *dir; |
254 | |
255 | g_assert (sub); |
256 | g_assert (!sub->cancelled); |
257 | g_assert (sub->dirname); |
258 | |
259 | IP_W ("Starting to watch %s\n" , sub->dirname); |
260 | dir = g_hash_table_lookup (hash_table: path_dir_hash, key: sub->dirname); |
261 | |
262 | if (dir == NULL) |
263 | { |
264 | IP_W ("Trying to add inotify watch " ); |
265 | wd = _ik_watch (path: sub->dirname, IP_INOTIFY_DIR_MASK|IN_ONLYDIR, err: &err); |
266 | if (wd < 0) |
267 | { |
268 | IP_W ("Failed\n" ); |
269 | return FALSE; |
270 | } |
271 | else |
272 | { |
273 | /* Create new watched directory and associate it with the |
274 | * wd hash and path hash |
275 | */ |
276 | IP_W ("Success\n" ); |
277 | dir = ip_watched_dir_new (path: sub->dirname, wd); |
278 | ip_map_wd_dir (wd, dir); |
279 | ip_map_path_dir (path: sub->dirname, dir); |
280 | } |
281 | } |
282 | else |
283 | IP_W ("Already watching\n" ); |
284 | |
285 | if (sub->hardlinks) |
286 | { |
287 | ip_watched_file_t *file; |
288 | |
289 | file = g_hash_table_lookup (hash_table: dir->files_hash, key: sub->filename); |
290 | |
291 | if (file == NULL) |
292 | { |
293 | file = ip_watched_file_new (dirname: sub->dirname, filename: sub->filename); |
294 | g_hash_table_insert (hash_table: dir->files_hash, key: file->filename, value: file); |
295 | } |
296 | |
297 | ip_watched_file_add_sub (file, sub); |
298 | ip_watched_file_start (file); |
299 | } |
300 | |
301 | ip_map_sub_dir (sub, dir); |
302 | |
303 | return TRUE; |
304 | } |
305 | |
306 | static void |
307 | ip_unmap_path_dir (const char *path, |
308 | ip_watched_dir_t *dir) |
309 | { |
310 | g_assert (path && dir); |
311 | g_hash_table_remove (hash_table: path_dir_hash, key: dir->path); |
312 | } |
313 | |
314 | static void |
315 | ip_unmap_wd_dir (gint32 wd, |
316 | ip_watched_dir_t *dir) |
317 | { |
318 | GList *dir_list = g_hash_table_lookup (hash_table: wd_dir_hash, GINT_TO_POINTER (wd)); |
319 | |
320 | if (!dir_list) |
321 | return; |
322 | |
323 | g_assert (wd >= 0 && dir); |
324 | dir_list = g_list_remove (list: dir_list, data: dir); |
325 | if (dir_list == NULL) |
326 | g_hash_table_remove (hash_table: wd_dir_hash, GINT_TO_POINTER (dir->wd)); |
327 | else |
328 | g_hash_table_replace (hash_table: wd_dir_hash, GINT_TO_POINTER (dir->wd), value: dir_list); |
329 | } |
330 | |
331 | static void |
332 | ip_unmap_wd (gint32 wd) |
333 | { |
334 | GList *dir_list = g_hash_table_lookup (hash_table: wd_dir_hash, GINT_TO_POINTER (wd)); |
335 | if (!dir_list) |
336 | return; |
337 | g_assert (wd >= 0); |
338 | g_hash_table_remove (hash_table: wd_dir_hash, GINT_TO_POINTER (wd)); |
339 | g_list_free (list: dir_list); |
340 | } |
341 | |
342 | static void |
343 | ip_unmap_sub_dir (inotify_sub *sub, |
344 | ip_watched_dir_t *dir) |
345 | { |
346 | g_assert (sub && dir); |
347 | g_hash_table_remove (hash_table: sub_dir_hash, key: sub); |
348 | dir->subs = g_list_remove (list: dir->subs, data: sub); |
349 | |
350 | if (sub->hardlinks) |
351 | { |
352 | ip_watched_file_t *file; |
353 | |
354 | file = g_hash_table_lookup (hash_table: dir->files_hash, key: sub->filename); |
355 | file->subs = g_list_remove (list: file->subs, data: sub); |
356 | |
357 | if (file->subs == NULL) |
358 | { |
359 | g_hash_table_remove (hash_table: dir->files_hash, key: sub->filename); |
360 | ip_watched_file_stop (file); |
361 | ip_watched_file_free (file); |
362 | } |
363 | } |
364 | } |
365 | |
366 | static void |
367 | ip_unmap_all_subs (ip_watched_dir_t *dir) |
368 | { |
369 | while (dir->subs != NULL) |
370 | ip_unmap_sub_dir (sub: dir->subs->data, dir); |
371 | } |
372 | |
373 | gboolean |
374 | _ip_stop_watching (inotify_sub *sub) |
375 | { |
376 | ip_watched_dir_t *dir = NULL; |
377 | |
378 | dir = g_hash_table_lookup (hash_table: sub_dir_hash, key: sub); |
379 | if (!dir) |
380 | return TRUE; |
381 | |
382 | ip_unmap_sub_dir (sub, dir); |
383 | |
384 | /* No one is subscribing to this directory any more */ |
385 | if (dir->subs == NULL) |
386 | { |
387 | _ik_ignore (path: dir->path, wd: dir->wd); |
388 | ip_unmap_wd_dir (wd: dir->wd, dir); |
389 | ip_unmap_path_dir (path: dir->path, dir); |
390 | ip_watched_dir_free (dir); |
391 | } |
392 | |
393 | return TRUE; |
394 | } |
395 | |
396 | |
397 | static ip_watched_dir_t * |
398 | ip_watched_dir_new (const char *path, |
399 | gint32 wd) |
400 | { |
401 | ip_watched_dir_t *dir = g_new0 (ip_watched_dir_t, 1); |
402 | |
403 | dir->path = g_strdup (str: path); |
404 | dir->files_hash = g_hash_table_new (hash_func: g_str_hash, key_equal_func: g_str_equal); |
405 | dir->wd = wd; |
406 | |
407 | return dir; |
408 | } |
409 | |
410 | static void |
411 | ip_watched_dir_free (ip_watched_dir_t *dir) |
412 | { |
413 | g_assert_cmpint (g_hash_table_size (dir->files_hash), ==, 0); |
414 | g_assert (dir->subs == NULL); |
415 | g_free (mem: dir->path); |
416 | g_hash_table_unref (hash_table: dir->files_hash); |
417 | g_free (mem: dir); |
418 | } |
419 | |
420 | static void |
421 | ip_wd_delete (gpointer data, |
422 | gpointer user_data) |
423 | { |
424 | ip_watched_dir_t *dir = data; |
425 | GList *l = NULL; |
426 | |
427 | for (l = dir->subs; l; l = l->next) |
428 | { |
429 | inotify_sub *sub = l->data; |
430 | /* Add subscription to missing list */ |
431 | _im_add (sub); |
432 | } |
433 | ip_unmap_all_subs (dir); |
434 | /* Unassociate the path and the directory */ |
435 | ip_unmap_path_dir (path: dir->path, dir); |
436 | ip_watched_dir_free (dir); |
437 | } |
438 | |
439 | static gboolean |
440 | ip_event_dispatch (GList *dir_list, |
441 | GList *file_list, |
442 | ik_event_t *event) |
443 | { |
444 | gboolean interesting = FALSE; |
445 | |
446 | GList *l; |
447 | |
448 | if (!event) |
449 | return FALSE; |
450 | |
451 | for (l = dir_list; l; l = l->next) |
452 | { |
453 | GList *subl; |
454 | ip_watched_dir_t *dir = l->data; |
455 | |
456 | for (subl = dir->subs; subl; subl = subl->next) |
457 | { |
458 | inotify_sub *sub = subl->data; |
459 | |
460 | /* If the subscription and the event |
461 | * contain a filename and they don't |
462 | * match, we don't deliver this event. |
463 | */ |
464 | if (sub->filename && |
465 | event->name && |
466 | strcmp (s1: sub->filename, s2: event->name) && |
467 | (!event->pair || !event->pair->name || strcmp (s1: sub->filename, s2: event->pair->name))) |
468 | continue; |
469 | |
470 | /* If the subscription has a filename |
471 | * but this event doesn't, we don't |
472 | * deliver this event. |
473 | */ |
474 | if (sub->filename && !event->name) |
475 | continue; |
476 | |
477 | /* If we're also watching the file directly |
478 | * don't report events that will also be |
479 | * reported on the file itself. |
480 | */ |
481 | if (sub->hardlinks) |
482 | { |
483 | event->mask &= ~IP_INOTIFY_FILE_MASK; |
484 | if (!event->mask) |
485 | continue; |
486 | } |
487 | |
488 | /* FIXME: We might need to synthesize |
489 | * DELETE/UNMOUNT events when |
490 | * the filename doesn't match |
491 | */ |
492 | |
493 | interesting |= event_callback (event, sub, FALSE); |
494 | |
495 | if (sub->hardlinks) |
496 | { |
497 | ip_watched_file_t *file; |
498 | |
499 | file = g_hash_table_lookup (hash_table: dir->files_hash, key: sub->filename); |
500 | |
501 | if (file != NULL) |
502 | { |
503 | if (event->mask & (IN_MOVED_FROM | IN_DELETE)) |
504 | ip_watched_file_stop (file); |
505 | |
506 | if (event->mask & (IN_MOVED_TO | IN_CREATE)) |
507 | ip_watched_file_start (file); |
508 | } |
509 | } |
510 | } |
511 | } |
512 | |
513 | for (l = file_list; l; l = l->next) |
514 | { |
515 | ip_watched_file_t *file = l->data; |
516 | GList *subl; |
517 | |
518 | for (subl = file->subs; subl; subl = subl->next) |
519 | { |
520 | inotify_sub *sub = subl->data; |
521 | |
522 | interesting |= event_callback (event, sub, TRUE); |
523 | } |
524 | } |
525 | |
526 | return interesting; |
527 | } |
528 | |
529 | static gboolean |
530 | ip_event_callback (ik_event_t *event) |
531 | { |
532 | gboolean interesting = FALSE; |
533 | GList* dir_list = NULL; |
534 | GList *file_list = NULL; |
535 | |
536 | /* We can ignore the IGNORED events. Likewise, if the event queue overflowed, |
537 | * there is not much we can do to recover. */ |
538 | if (event->mask & (IN_IGNORED | IN_Q_OVERFLOW)) |
539 | { |
540 | _ik_event_free (event); |
541 | return TRUE; |
542 | } |
543 | |
544 | dir_list = g_hash_table_lookup (hash_table: wd_dir_hash, GINT_TO_POINTER (event->wd)); |
545 | file_list = g_hash_table_lookup (hash_table: wd_file_hash, GINT_TO_POINTER (event->wd)); |
546 | |
547 | if (event->mask & IP_INOTIFY_DIR_MASK) |
548 | interesting |= ip_event_dispatch (dir_list, file_list, event); |
549 | |
550 | /* Only deliver paired events if the wds are separate */ |
551 | if (event->pair && event->pair->wd != event->wd) |
552 | { |
553 | dir_list = g_hash_table_lookup (hash_table: wd_dir_hash, GINT_TO_POINTER (event->pair->wd)); |
554 | file_list = g_hash_table_lookup (hash_table: wd_file_hash, GINT_TO_POINTER (event->pair->wd)); |
555 | |
556 | if (event->pair->mask & IP_INOTIFY_DIR_MASK) |
557 | interesting |= ip_event_dispatch (dir_list, file_list, event: event->pair); |
558 | } |
559 | |
560 | /* We have to manage the missing list |
561 | * when we get an event that means the |
562 | * file has been deleted/moved/unmounted. |
563 | */ |
564 | if (event->mask & IN_DELETE_SELF || |
565 | event->mask & IN_MOVE_SELF || |
566 | event->mask & IN_UNMOUNT) |
567 | { |
568 | /* Add all subscriptions to missing list */ |
569 | g_list_foreach (list: dir_list, func: ip_wd_delete, NULL); |
570 | /* Unmap all directories attached to this wd */ |
571 | ip_unmap_wd (wd: event->wd); |
572 | } |
573 | |
574 | _ik_event_free (event); |
575 | |
576 | return interesting; |
577 | } |
578 | |
579 | const char * |
580 | _ip_get_path_for_wd (gint32 wd) |
581 | { |
582 | GList *dir_list; |
583 | ip_watched_dir_t *dir; |
584 | |
585 | g_assert (wd >= 0); |
586 | dir_list = g_hash_table_lookup (hash_table: wd_dir_hash, GINT_TO_POINTER (wd)); |
587 | if (dir_list) |
588 | { |
589 | dir = dir_list->data; |
590 | if (dir) |
591 | return dir->path; |
592 | } |
593 | |
594 | return NULL; |
595 | } |
596 | |