]> icculus.org git repositories - dana/openbox.git/blob - obt/watch.c
Make the obt_watch functionality work without inotify (via manual refreshes).
[dana/openbox.git] / obt / watch.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    obt/watch.c for the Openbox window manager
4    Copyright (c) 2010        Dana Jansens
5
6    This program is free software; you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation; either version 2 of the License, or
9    (at your option) any later version.
10
11    This program is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14    GNU General Public License for more details.
15
16    See the COPYING file for a copy of the GNU General Public License.
17 */
18
19 #include "obt/watch.h"
20 #include "obt/watch_interface.h"
21
22 #ifdef HAVE_STRING_H
23 #  include <string.h>
24 #endif
25 #ifdef HAVE_UNISTD_H
26 #  include <unistd.h>
27 #endif
28 #ifdef HAVE_SYS_TYPES_H
29 #  include <sys/types.h>
30 #endif
31 #ifdef HAVE_SYS_STAT_H
32 #  include <sys/stat.h>
33 #endif
34
35 #include <glib.h>
36
37 /* Interface for system-specific stuff (e.g. inotify). the functions are
38    defined in in watch_<system>.c
39 */
40
41 /* General system which uses the watch_sys_* stuff
42 */
43
44 struct _ObtWatch {
45     guint ref;
46     GHashTable *targets_by_path;
47     GSource *source;
48 };
49
50 struct _ObtWatchTarget {
51     ObtWatch *w;
52     gchar *base_path;
53     ObtWatchFunc func;
54     gpointer data;
55     gboolean watch_hidden;
56     ObtWatchFile *file;
57     gpointer watch_sys_data;
58 };
59
60 struct _ObtWatchFile {
61     ObtWatchFile *parent;
62     gchar *name;
63     time_t modified;
64     /* If this is a directory, then the hash table contains pointers to other
65        ObtWatchFile objects that are contained in this one. */
66     GHashTable *children_by_name;
67
68     gboolean seen;
69     gpointer watch_sys_data;
70 };
71
72 static void target_free(ObtWatchTarget *t);
73
74 static ObtWatchFile* tree_create(ObtWatchTarget *t,
75                                  const gchar *path, const gchar *last,
76                                  ObtWatchFile *parent);
77 static void tree_destroy(ObtWatchFile *file);
78
79 static void notify(ObtWatchTarget *target, ObtWatchFile *file,
80                    ObtWatchNotifyType type);
81
82 ObtWatch* obt_watch_new()
83 {
84     ObtWatch *w;
85     GSource *source;
86
87     w = NULL;
88     source = watch_sys_create_source();
89     if (source) {
90         w = g_slice_new(ObtWatch);
91         w->ref = 1;
92         w->targets_by_path = g_hash_table_new_full(
93             g_str_hash, g_str_equal, NULL, (GDestroyNotify)target_free);
94         w->source = source;
95
96         g_source_attach(source, NULL);
97     }
98     return w;
99 }
100
101 void obt_watch_ref(ObtWatch *w)
102 {
103     ++w->ref;
104 }
105
106 void obt_watch_unref(ObtWatch *w)
107 {
108     if (--w->ref < 1) {
109         g_hash_table_destroy(w->targets_by_path);
110         g_source_destroy(w->source);
111         g_slice_free(ObtWatch, w);
112     }
113 }
114
115 static void target_free(ObtWatchTarget *t)
116 {
117     if (t->file) tree_destroy(t->file);
118     g_free(t->base_path);
119     g_slice_free(ObtWatchTarget, t);
120 }
121
122 /*! Scans through the filesystem under the target and reports each file/dir
123   that it finds to the watch subsystem.
124   @path Absolute path to the file to scan for.
125   @last The last component (filename) of the file to scan for.
126   @parent The parent directory (if it exists) of the file we are scanning for.
127  */
128 static ObtWatchFile* tree_create(ObtWatchTarget *target,
129                                  const gchar *path, const gchar *last,
130                                  ObtWatchFile *parent)
131 {
132     ObtWatchFile *file;
133     struct stat buf;
134
135     if (stat(path, &buf) < 0)
136         return NULL;
137
138     file = g_slice_new(ObtWatchFile);
139     file->name = g_strdup(last);
140     file->modified = 0;
141     file->parent = parent;
142     file->children_by_name = NULL;
143
144     if (S_ISDIR(buf.st_mode))
145         file->children_by_name =
146             g_hash_table_new_full(g_str_hash, g_str_equal,
147                                   NULL, (GDestroyNotify)tree_destroy);
148
149     if (!parent) {
150         g_assert(target->file == NULL);
151         target->file = file;
152     }
153     else
154         g_hash_table_replace(parent->children_by_name,
155                              file->name,
156                              file);
157
158     if (!S_ISDIR(buf.st_mode))
159         notify(target, file, OBT_WATCH_ADDED);
160
161     file->watch_sys_data =
162         watch_sys_add_file(target->w->source,
163                            target, file, S_ISDIR(buf.st_mode));
164
165     /* recurse on the contents if it's a directory */
166     if (file && watch_main_file_is_dir(file)) {
167         GDir *dir;
168
169         dir = g_dir_open(path, 0, NULL);
170         if (dir) {
171             const gchar *name;
172
173             while ((name = g_dir_read_name(dir))) {
174                 if (name[0] != '.' || target->watch_hidden) {
175                     gchar *subpath;
176                     ObtWatchFile *child;
177
178                     subpath = g_build_filename(path, name, NULL);
179                     child = tree_create(target, subpath, name, file);
180                     g_free(subpath);
181                 }
182             }
183         }
184         g_dir_close(dir);
185     }
186
187     if (file)
188         file->modified = buf.st_mtime;
189
190     return file;
191 }
192
193 static void tree_destroy(ObtWatchFile *file)
194 {
195     g_free(file->name);
196     if (file->children_by_name)
197         g_hash_table_unref(file->children_by_name);
198     g_slice_free(ObtWatchFile, file);
199 }
200
201 gboolean obt_watch_add(ObtWatch *w, const gchar *path, gboolean watch_hidden,
202                        ObtWatchFunc func, gpointer data)
203 {
204     ObtWatchTarget *t;
205
206     g_return_val_if_fail(w != NULL, FALSE);
207     g_return_val_if_fail(path != NULL, FALSE);
208     g_return_val_if_fail(func != NULL, FALSE);
209     g_return_val_if_fail(path[0] == G_DIR_SEPARATOR, FALSE);
210
211     t = g_slice_new(ObtWatchTarget);
212     t->w = w;
213     t->base_path = g_strdup(path);
214     t->watch_hidden = watch_hidden;
215     t->func = func;
216     t->data = data;
217     t->file = NULL;
218     g_hash_table_replace(w->targets_by_path, t->base_path, t);
219
220     watch_main_notify_add(t, NULL, NULL);
221
222     return TRUE;
223 }
224
225 void obt_watch_remove(ObtWatch *w, const gchar *path)
226 {
227     g_return_if_fail(w != NULL);
228     g_return_if_fail(path != NULL);
229     g_return_if_fail(path[0] == G_DIR_SEPARATOR);
230
231     /* this also calls target_free which does notifies */
232     g_hash_table_remove(w->targets_by_path, path);
233 }
234
235 static ObtWatchFile* refresh_file(ObtWatchTarget *target, ObtWatchFile *file,
236                                   ObtWatchFile *parent, const gchar *path)
237 {
238     struct stat buf;
239
240     g_assert(file != NULL);
241
242     if (stat(path, &buf) < 0) {
243         watch_main_notify_remove(target, file);
244         file = NULL;
245     }
246
247     else if (S_ISDIR(buf.st_mode) != watch_main_file_is_dir(file)) {
248         gchar *name = g_strdup(file->name);
249
250         watch_main_notify_remove(target, file);
251         watch_main_notify_add(target, parent, name);
252         g_free(name);
253         file = NULL;
254     }
255
256     /* recurse on the contents if it's a directory */
257     else if (watch_main_file_is_dir(file)) {
258         GDir *dir;
259         GList *children, *it;
260
261         children = watch_main_file_children(file);
262         for (it = children; it; it = g_list_next(it))
263             ((ObtWatchFile*)it->data)->seen = FALSE;
264         g_list_free(children);
265
266         dir = g_dir_open(path, 0, NULL);
267         if (dir) {
268             const gchar *name;
269
270             while ((name = g_dir_read_name(dir))) {
271                 if (name[0] != '.' || target->watch_hidden) {
272                     ObtWatchFile *child = watch_main_file_child(file, name);
273
274                     if (!child)
275                         watch_main_notify_add(target, file, name);
276                     else {
277                         gchar *subpath;
278                         ObtWatchFile *newchild;
279
280                         subpath = g_build_filename(path, name, NULL);
281                         newchild = refresh_file(target, child, file, subpath);
282                         g_free(subpath);
283
284                         if (newchild)
285                             child->seen = TRUE;
286                     }
287                 }
288             }
289         }
290         g_dir_close(dir);
291
292         children = watch_main_file_children(file);
293         for (it = children; it; it = g_list_next(it))
294             if (((ObtWatchFile*)it->data)->seen == FALSE)
295                 watch_main_notify_remove(target, it->data);
296         g_list_free(children);
297     }
298
299     /* check for modifications if it's a file */
300     else {
301         if (file->modified >= 0 && buf.st_mtime > file->modified)
302             watch_main_notify_modify(target, file);
303     }
304
305     if (file)
306         file->modified = buf.st_mtime;
307
308     return file;
309 }
310
311 static void foreach_refresh_target(gpointer k, gpointer v, gpointer u)
312 {
313     ObtWatchTarget *target = v;
314
315     if (!target->file)
316         /* we don't have any files being watched, try look for
317            the target's root again */
318         watch_main_notify_add(target, NULL, NULL);
319     else
320         refresh_file(target, target->file, NULL, target->base_path);
321 }
322
323 void obt_watch_refresh(ObtWatch *w)
324 {
325     g_return_if_fail(w != NULL);
326
327     g_hash_table_foreach(w->targets_by_path, foreach_refresh_target, NULL);
328 }
329
330 static void notify(ObtWatchTarget *target, ObtWatchFile *file,
331                    ObtWatchNotifyType type)
332 {
333     gchar *sub_path = watch_main_file_sub_path(file);
334     target->func(target->w, target->base_path, sub_path, type,
335                  target->data);
336     g_free(sub_path);
337 }
338
339 void watch_main_notify_add(ObtWatchTarget *target,
340                            ObtWatchFile *parent, const gchar *name)
341 {
342     gchar *path;
343
344     if (parent && name[0] == '.' && !target->watch_hidden)
345         return;
346
347     path = g_build_filename(target->base_path,
348                             watch_main_file_sub_path(parent),
349                             name,
350                             NULL);
351     tree_create(target, path, name, parent);
352     g_free(path);
353 }
354
355 static void foreach_child_notify_removed(gpointer k, gpointer v, gpointer u)
356 {
357     ObtWatchTarget *target = u;
358     ObtWatchFile *file = v;
359
360     if (watch_main_file_is_dir(file)) {
361         g_hash_table_foreach(file->children_by_name,
362                              foreach_child_notify_removed,
363                              target);
364         g_hash_table_remove_all(file->children_by_name);
365     }
366     else if (!file->parent)
367         notify(target, file, OBT_WATCH_SELF_REMOVED);
368     else
369         notify(target, file, OBT_WATCH_REMOVED);
370
371     watch_sys_remove_file(target->w->source,
372                           target, file, file->watch_sys_data);
373 }
374
375 void watch_main_notify_remove(ObtWatchTarget *target, ObtWatchFile *file)
376 {
377     foreach_child_notify_removed(NULL, file, target);
378
379     tree_destroy(file);
380     if (!file->parent)
381         target->file = NULL;
382 }
383
384 void watch_main_notify_modify(ObtWatchTarget *target, ObtWatchFile *file)
385 {
386     if (!watch_main_file_is_dir(file)) {
387         notify(target, file, OBT_WATCH_MODIFIED);
388     }
389     file->modified = -1;
390 }
391
392 void build_sub_path(GString *str, ObtWatchFile *file)
393 {
394     if (file->parent) {
395         build_sub_path(str, file->parent);
396         g_string_append(str, G_DIR_SEPARATOR_S);
397         g_string_append(str, file->name);
398     }
399 }
400
401 gboolean watch_main_target_watch_hidden(ObtWatchTarget *target)
402 {
403     return target->watch_hidden;
404 }
405
406 ObtWatchFile* watch_main_target_root(ObtWatchTarget *target)
407 {
408     return target->file;
409 }
410
411 gchar* watch_main_file_sub_path(ObtWatchFile *file)
412 {
413     GString *str;
414     gchar *ret = NULL;
415
416     if (file) {
417         str = g_string_new("");
418         build_sub_path(str, file);
419         ret = str->str;
420         g_string_free(str, FALSE);
421     }
422
423     return ret;
424 }
425
426 gchar* watch_main_target_file_full_path(ObtWatchTarget *target,
427                                         ObtWatchFile *file)
428 {
429     GString *str;
430     gchar *ret = NULL;
431
432     if (file) {
433         str = g_string_new(target->base_path);
434         build_sub_path(str, file);
435         ret = str->str;
436         g_string_free(str, FALSE);
437     }
438
439     return ret;
440 }
441
442 gboolean watch_main_file_is_dir(ObtWatchFile *file)
443 {
444     return file->children_by_name != NULL;
445 }
446
447 ObtWatchFile* watch_main_file_child(ObtWatchFile *file,
448                                     const gchar *name)
449 {
450     return g_hash_table_lookup(file->children_by_name, name);
451 }
452
453 GList* watch_main_file_children(ObtWatchFile *file)
454 {
455     return g_hash_table_get_values(file->children_by_name);
456 }