1 /*
2 * Copyright © 2008 Ryan Lortie
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of version 3 of the GNU General Public License as
6 * published by the Free Software Foundation.
7 */
8
9 #include <sys/stat.h>
10
11 #include "dirwatch.h"
12
13 /* DirWatch
14 *
15 * a directory watcher utility for use by the trash:/ backend.
16 *
17 * A DirWatch monitors a given directory for existence under a very
18 * specific set of circumstances. When the directory comes into
19 * existence, the create() callback is invoked. When the directory
20 * stops existing the destroy() callback is invoked. If the directory
21 * initially exists, then create() is invoked before the call to
22 * dir_watch_new() returns.
23 *
24 * The directory to watch is considered to exist only if it is a
25 * directory (and not a symlink) and its parent directory also exists.
26 * A topdir must be given, which is always assumed to "exist".
27 *
28 * For example, if '/mnt/disk/.Trash/1000/files/' is monitored with
29 * '/mnt/disk/' as a topdir then the following conditions must be true
30 * in order for the directory to be reported as existing:
31 *
32 * /mnt/disk/ is blindly assumed to exist
33 * /mnt/disk/.Trash must be a directory (not a symlink)
34 * /mnt/disk/.Trash/1000 must be a directory (not a symlink)
35 * /mnt/disk/.Trash/1000/files must be a directory (not a symlink)
36 *
37 * If any of these ceases to be true (even momentarily), the directory
38 * will be reported as having been destroyed. create() and destroy()
39 * callbacks are never issued spuriously (ie: two calls to one
40 * callback will never occur in a row). Events where the directory
41 * exists momentarily might be missed, but events where the directory
42 * stops existing momentarily will (hopefully) always be reported.
43 * The first call (if it happens) will always be to create().
44 *
45 * check() is only ever called in response to a call to
46 * dir_watch_check() in which case it will be called only if the
47 * watched directory was marked as having existed before the check and
48 * is found to still exist. This facilitates the checking that has to
49 * occur in that case (ie: check the contents of the directory to make
50 * sure that they are also unchanged).
51 *
52 * This implementation is currently tweaked a bit for how GFileMonitor
53 * currently works with inotify. If GFileMonitor's implementation is
54 * changed it might be a good idea to take another look at this code.
55 */
56
57 struct OPAQUE_TYPE__DirWatch
58 {
59 GFile *directory;
60 GFile *topdir;
61
62 DirWatchFunc create;
63 DirWatchFunc check;
64 DirWatchFunc destroy;
65 gpointer user_data;
66 gboolean state;
67
68 DirWatch *parent;
69
70 GFileMonitor *parent_monitor;
71 };
72
73 #ifdef DIR_WATCH_DEBUG
74 # define dir_watch_created(watch) \
75 G_STMT_START { \
76 char *path = g_file_get_path ((watch)->directory); \
77 g_print (">> created '%s'\n", path); \
78 g_free (path); \
79 (watch)->create ((watch)->user_data); \
80 } G_STMT_END
81
82 # define dir_watch_destroyed(watch) \
83 G_STMT_START { \
84 char *path = g_file_get_path ((watch)->directory); \
85 g_print (">> destroyed '%s'\n", path); \
86 g_free (path); \
87 (watch)->destroy ((watch)->user_data); \
88 } G_STMT_END
89 #else
90 # define dir_watch_created(watch) (watch)->create ((watch)->user_data)
91 # define dir_watch_destroyed(watch) (watch)->destroy ((watch)->user_data)
92 #endif
93
94 #ifdef DIR_WATCH_DEBUG
95 #include <errno.h>
96 #endif
97
98 static gboolean
dir_exists(GFile * file)99 dir_exists (GFile *file)
100 {
101 gboolean result;
102 struct stat buf;
103 char *path;
104
105 path = g_file_get_path (file);
106 #ifdef DIR_WATCH_DEBUG
107 errno = 0;
108 #endif
109 result = !lstat (path, &buf) && S_ISDIR (buf.st_mode);
110
111 #ifdef DIR_WATCH_DEBUG
112 g_print (" lstat ('%s') -> is%s a directory (%s)\n",
113 path, result ? "" : " not", g_strerror (errno));
114 #endif
115
116 g_free (path);
117
118 return result;
119 }
120
121 static void
dir_watch_parent_changed(GFileMonitor * monitor,GFile * file,GFile * other_file,GFileMonitorEvent event_type,gpointer user_data)122 dir_watch_parent_changed (GFileMonitor *monitor,
123 GFile *file,
124 GFile *other_file,
125 GFileMonitorEvent event_type,
126 gpointer user_data)
127 {
128 DirWatch *watch = user_data;
129
130 g_assert (watch->parent_monitor == monitor);
131
132 if (!g_file_equal (file, watch->directory))
133 return;
134
135 if (event_type == G_FILE_MONITOR_EVENT_CREATED)
136 {
137 if (watch->state)
138 return;
139
140 /* we were just created. ensure that it's a directory. */
141 if (dir_exists (file))
142 {
143 /* we're official now. report it. */
144 watch->state = TRUE;
145 dir_watch_created (watch);
146 }
147 }
148 else if (event_type == G_FILE_MONITOR_EVENT_DELETED)
149 {
150 if (!watch->state)
151 return;
152
153 watch->state = FALSE;
154 dir_watch_destroyed (watch);
155 }
156 }
157
158 static void
dir_watch_recursive_create(gpointer user_data)159 dir_watch_recursive_create (gpointer user_data)
160 {
161 DirWatch *watch = user_data;
162 GFile *parent;
163
164 g_assert (watch->parent_monitor == NULL);
165
166 parent = g_file_get_parent (watch->directory);
167 watch->parent_monitor = g_file_monitor_directory (parent, 0,
168 NULL, NULL);
169 g_object_unref (parent);
170 g_signal_connect (watch->parent_monitor, "changed",
171 G_CALLBACK (dir_watch_parent_changed), watch);
172
173 /* check if directory was created before we started to monitor */
174 if (dir_exists (watch->directory))
175 {
176 watch->state = TRUE;
177 dir_watch_created (watch);
178 }
179 }
180
181 static void
dir_watch_recursive_check(gpointer user_data)182 dir_watch_recursive_check (gpointer user_data)
183 {
184 DirWatch *watch = user_data;
185 gboolean exists;
186
187 exists = dir_exists (watch->directory);
188
189 if (watch->state && exists)
190 watch->check (watch->user_data);
191
192 else if (!watch->state && exists)
193 {
194 watch->state = TRUE;
195 dir_watch_created (watch);
196 }
197 else if (watch->state && !exists)
198 {
199 watch->state = FALSE;
200 dir_watch_destroyed (watch);
201 }
202 }
203
204 static void
dir_watch_recursive_destroy(gpointer user_data)205 dir_watch_recursive_destroy (gpointer user_data)
206 {
207 DirWatch *watch = user_data;
208
209 /* exactly one monitor should be active */
210 g_assert (watch->parent_monitor != NULL);
211
212 /* if we were monitoring the directory... */
213 if (watch->state)
214 {
215 dir_watch_destroyed (watch);
216 watch->state = FALSE;
217 }
218
219 g_file_monitor_cancel (watch->parent_monitor);
220 g_object_unref (watch->parent_monitor);
221 watch->parent_monitor = NULL;
222 }
223
224 DirWatch *
dir_watch_new(GFile * directory,GFile * topdir,DirWatchFunc create,DirWatchFunc check,DirWatchFunc destroy,gpointer user_data)225 dir_watch_new (GFile *directory,
226 GFile *topdir,
227 DirWatchFunc create,
228 DirWatchFunc check,
229 DirWatchFunc destroy,
230 gpointer user_data)
231 {
232 DirWatch *watch;
233
234 watch = g_slice_new0 (DirWatch);
235 watch->create = create;
236 watch->check = check;
237 watch->destroy = destroy;
238 watch->user_data = user_data;
239
240 watch->directory = g_object_ref (directory);
241 watch->topdir = g_object_ref (topdir);
242
243 /* the top directory always exists */
244 if (g_file_equal (directory, topdir))
245 {
246 dir_watch_created (watch);
247 watch->state = TRUE;
248 }
249
250 else
251 {
252 GFile *parent;
253
254 parent = g_file_get_parent (directory);
255 g_assert (parent != NULL);
256
257 watch->parent = dir_watch_new (parent, topdir,
258 dir_watch_recursive_create,
259 dir_watch_recursive_check,
260 dir_watch_recursive_destroy,
261 watch);
262
263 g_object_unref (parent);
264 }
265
266 return watch;
267 }
268
269 void
dir_watch_free(DirWatch * watch)270 dir_watch_free (DirWatch *watch)
271 {
272 if (watch != NULL)
273 {
274 if (watch->parent_monitor)
275 {
276 g_file_monitor_cancel (watch->parent_monitor);
277 g_object_unref (watch->parent_monitor);
278 }
279
280 g_object_unref (watch->directory);
281 g_object_unref (watch->topdir);
282
283 dir_watch_free (watch->parent);
284
285 g_slice_free (DirWatch, watch);
286 }
287 }
288
289 /**
290 * dir_watch_check:
291 * @watch: a #DirWatch
292 *
293 * Emit missed events.
294 *
295 * This function is called on a DirWatch that might have missed events
296 * (because it is watching on an NFS mount, for example).
297 *
298 * This function will manually check if any directories have come into
299 * or gone out of existence and will emit created or destroyed callbacks
300 * as appropriate.
301 *
302 * Additionally, if a directory is found to still exist, the checked
303 * callback will be emitted.
304 **/
305 void
dir_watch_check(DirWatch * watch)306 dir_watch_check (DirWatch *watch)
307 {
308 if (watch->parent == NULL)
309 {
310 g_assert (watch->state);
311
312 watch->check (watch->user_data);
313 return;
314 }
315
316 dir_watch_check (watch->parent);
317 }
318