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