summaryrefslogtreecommitdiff
path: root/daemon/trashlib/dirwatch.c
blob: 6a2f1f6f4dfbb4807d662a84b1184d1977096e8f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/*
 * Copyright © 2008 Ryan Lortie
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of version 3 of the GNU General Public License as
 * published by the Free Software Foundation.   
 */

#include <sys/stat.h>

#include "dirwatch.h"

/* DirWatch
 *
 * a directory watcher utility for use by the trash:/ backend.
 *
 * A DirWatch monitors a given directory for existence under a very
 * specific set of circumstances.  When the directory comes into
 * existence, the create() callback is invoked.  When the directory
 * stops existing the destroy() callback is invoked.  If the directory
 * initially exists, then create() is invoked before the call to
 * dir_watch_new() returns.
 *
 * The directory to watch is considered to exist only if it is a
 * directory (and not a symlink) and its parent directory also exists.
 * A topdir must be given, which is always assumed to "exist".
 *
 * For example, if '/mnt/disk/.Trash/1000/files/' is monitored with
 * '/mnt/disk/' as a topdir then the following conditions must be true
 * in order for the directory to be reported as existing:
 *
 *   /mnt/disk/ is blindly assumed to exist
 *   /mnt/disk/.Trash must be a directory (not a symlink)
 *   /mnt/disk/.Trash/1000 must be a directory (not a symlink)
 *   /mnt/disk/.Trash/1000/files must be a directory (not a symlink)
 *
 * If any of these ceases to be true (even momentarily), the directory
 * will be reported as having been destroyed.  create() and destroy()
 * callbacks are never issued spuriously (ie: two calls to one
 * callback will never occur in a row).  Events where the directory
 * exists momentarily might be missed, but events where the directory
 * stops existing momentarily will (hopefully) always be reported.
 * The first call (if it happens) will always be to create().
 *
 * check() is only ever called in response to a call to
 * dir_watch_check() in which case it will be called only if the
 * watched directory was marked as having existed before the check and
 * is found to still exist.  This facilitates the checking that has to
 * occur in that case (ie: check the contents of the directory to make
 * sure that they are also unchanged).
 *
 * This implementation is currently tweaked a bit for how GFileMonitor
 * currently works with inotify.  If GFileMonitor's implementation is
 * changed it might be a good idea to take another look at this code.
 */

struct OPAQUE_TYPE__DirWatch
{
  GFile *directory;
  GFile *topdir;

  DirWatchFunc create;
  DirWatchFunc check;
  DirWatchFunc destroy;
  gpointer user_data;
  gboolean state;

  DirWatch *parent;

  GFileMonitor *parent_monitor;
};

#ifdef DIR_WATCH_DEBUG
# define dir_watch_created(watch) \
    G_STMT_START {                                              \
      char *path = g_file_get_path ((watch)->directory);        \
      g_print (">> created '%s'\n", path);                      \
      g_free (path);                                            \
      (watch)->create ((watch)->user_data);                     \
    } G_STMT_END

# define dir_watch_destroyed(watch) \
    G_STMT_START {                                              \
      char *path = g_file_get_path ((watch)->directory);        \
      g_print (">> destroyed '%s'\n", path);                    \
      g_free (path);                                            \
      (watch)->destroy ((watch)->user_data);                    \
    } G_STMT_END
#else
# define dir_watch_created(watch) (watch)->create ((watch)->user_data)
# define dir_watch_destroyed(watch) (watch)->destroy ((watch)->user_data)
#endif

#ifdef DIR_WATCH_DEBUG
#include <errno.h>
#endif

static gboolean
dir_exists (GFile *file)
{
  gboolean result;
  struct stat buf;
  char *path;

  path = g_file_get_path (file);
#ifdef DIR_WATCH_DEBUG
  errno = 0;
#endif
  result = !lstat (path, &buf) && S_ISDIR (buf.st_mode);

#ifdef DIR_WATCH_DEBUG
  g_print ("    lstat ('%s') -> is%s a directory (%s)\n",
           path, result ? "" : " not", g_strerror (errno));
#endif

  g_free (path);

  return result;
}

static void
dir_watch_parent_changed (GFileMonitor      *monitor,
                          GFile             *file,
                          GFile             *other_file,
                          GFileMonitorEvent  event_type,
                          gpointer           user_data)
{
  DirWatch *watch = user_data;

  g_assert (watch->parent_monitor == monitor);

  if (!g_file_equal (file, watch->directory))
    return;

  if (event_type == G_FILE_MONITOR_EVENT_CREATED)
    {
      if (watch->state)
        return;

      /* we were just created.  ensure that it's a directory. */
      if (dir_exists (file))
        {
          /* we're official now.  report it. */
          watch->state = TRUE;
          dir_watch_created (watch);
        }
    }
  else if (event_type == G_FILE_MONITOR_EVENT_DELETED)
    {
      if (!watch->state)
        return;

      watch->state = FALSE;
      dir_watch_destroyed (watch);
    }
}

static void
dir_watch_recursive_create (gpointer user_data)
{
  DirWatch *watch = user_data;
  GFile *parent;

  g_assert (watch->parent_monitor == NULL);

  parent = g_file_get_parent (watch->directory);
  watch->parent_monitor = g_file_monitor_directory (parent, 0,
                                                    NULL, NULL);
  g_object_unref (parent);
  g_signal_connect (watch->parent_monitor, "changed",
                    G_CALLBACK (dir_watch_parent_changed), watch);
  
  /* check if directory was created before we started to monitor */
  if (dir_exists (watch->directory))
    {
      watch->state = TRUE;
      dir_watch_created (watch);
    }
}

static void
dir_watch_recursive_check (gpointer user_data)
{
  DirWatch *watch = user_data;
  gboolean exists;
 
  exists = dir_exists (watch->directory);

  if (watch->state && exists)
    watch->check (watch->user_data);

  else if (!watch->state && exists)
    {
      watch->state = TRUE;
      dir_watch_created (watch);
    }
  else if (watch->state && !exists)
    {
      watch->state = FALSE;
      dir_watch_destroyed (watch);
    }
}

static void
dir_watch_recursive_destroy (gpointer user_data)
{
  DirWatch *watch = user_data;

  /* exactly one monitor should be active */
  g_assert (watch->parent_monitor != NULL);

  /* if we were monitoring the directory... */
  if (watch->state)
    {
      dir_watch_destroyed (watch);
      watch->state = FALSE;
    }

  g_file_monitor_cancel (watch->parent_monitor);
  g_object_unref (watch->parent_monitor);
  watch->parent_monitor = NULL;
}

DirWatch *
dir_watch_new (GFile        *directory,
               GFile        *topdir,
               DirWatchFunc  create,
               DirWatchFunc  check,
               DirWatchFunc  destroy,
               gpointer      user_data)
{
  DirWatch *watch;

  watch = g_slice_new0 (DirWatch);
  watch->create = create;
  watch->check = check;
  watch->destroy = destroy;
  watch->user_data = user_data;

  watch->directory = g_object_ref (directory);
  watch->topdir = g_object_ref (topdir);

  /* the top directory always exists */
  if (g_file_equal (directory, topdir))
    {
      dir_watch_created (watch);
      watch->state = TRUE;
    }

  else
    {
      GFile *parent;

      parent = g_file_get_parent (directory);
      g_assert (parent != NULL);

      watch->parent = dir_watch_new (parent, topdir,
                                     dir_watch_recursive_create,
                                     dir_watch_recursive_check,
                                     dir_watch_recursive_destroy,
                                     watch);

      g_object_unref (parent);
    }

  return watch;
}

void
dir_watch_free (DirWatch *watch)
{
  if (watch != NULL)
    {
      if (watch->parent_monitor)
        {
          g_file_monitor_cancel (watch->parent_monitor);
          g_object_unref (watch->parent_monitor);
        }

      g_object_unref (watch->directory);
      g_object_unref (watch->topdir);

      dir_watch_free (watch->parent);

      g_slice_free (DirWatch, watch);
    }
}

/**
 * dir_watch_check:
 * @watch: a #DirWatch
 *
 * Emit missed events.
 *
 * This function is called on a DirWatch that might have missed events
 * (because it is watching on an NFS mount, for example).
 * 
 * This function will manually check if any directories have come into
 * or gone out of existence and will emit created or destroyed callbacks
 * as appropriate.
 *
 * Additionally, if a directory is found to still exist, the checked
 * callback will be emitted.
 **/
void
dir_watch_check (DirWatch *watch)
{
  if (watch->parent == NULL)
    {
      g_assert (watch->state);

      watch->check (watch->user_data);
      return;
    }

  dir_watch_check (watch->parent);
}