]> icculus.org git repositories - dana/openbox.git/blob - obt/watch_inotify.c
Move the GSource attach out to the generic watch code, and avoid blocking reads
[dana/openbox.git] / obt / watch_inotify.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 #ifdef HAVE_SYS_INOTIFY_H
20
21 #include "watch.h"
22
23 #include <sys/inotify.h>
24 #ifdef HAVE_UNISTD_H
25 #  include <unistd.h>
26 #endif
27 #ifdef HAVE_STRING_H
28 #  include <string.h>
29 #endif
30 #ifdef HAVE_ERRNO_H
31 #  include <errno.h>
32 #endif
33
34 #include <glib.h>
35
36 typedef struct _InoSource InoSource;
37 typedef struct _InoTarget InoTarget;
38
39 /*! Callback function in the watch general system.
40   Matches definition in watch.c
41 */
42 typedef void (*ObtWatchNotifyFunc)(const gchar *sub_path,
43                                    const gchar *full_path, gpointer target,
44                                    ObtWatchNotifyType type);
45
46 struct _InoSource {
47     GSource source;
48
49     GPollFD pfd;
50     ObtWatchNotifyFunc notify;
51     GHashTable *targets_by_key;
52     GHashTable *targets_by_path;
53 };
54
55 struct _InoTarget {
56     gint key;
57     gchar *path;
58     guint base_len; /* the length of the prefix of path which is the
59                        target's path */
60     gpointer watch_target;
61     gboolean is_dir;
62     gboolean watch_hidden;
63 };
64
65 static gboolean source_check(GSource *source);
66 static gboolean source_prepare(GSource *source, gint *timeout);
67 static gboolean source_read(GSource *source, GSourceFunc cb, gpointer data);
68 static void source_finalize(GSource *source);
69 static gint add_target(GSource *source, InoTarget *parent,
70                        const gchar *path, gboolean watch_hidden,
71                        gpointer target);
72 static void remove_target(GSource *source, InoTarget *target);
73 static void target_free(InoTarget *target);
74 static void notify_target(GSource *source, InoTarget *ino_target,
75                           const gchar *path, ObtWatchNotifyType type);
76
77 static GSourceFuncs source_funcs = {
78     source_prepare,
79     source_check,
80     source_read,
81     source_finalize
82 };
83
84 gint watch_sys_add_target(GSource *source, const char *path,
85                           gboolean watch_hidden, gpointer target)
86 {
87     return add_target(source, NULL, path, watch_hidden, target);
88 }
89
90 void watch_sys_remove_target(GSource *source, gint key)
91 {
92     InoSource *ino_source = (InoSource*)source;
93     InoTarget *t;
94
95     t = g_hash_table_lookup(ino_source->targets_by_key, &key);
96     remove_target(source, t);
97 }
98
99 GSource* watch_sys_create_source(ObtWatchNotifyFunc notify)
100 {
101     gint fd;
102     GSource *source;
103     InoSource *ino_source;
104
105     g_return_val_if_fail(notify != NULL, NULL);
106
107     source = NULL;
108     fd = inotify_init();
109     if (fd < 0) {
110         gchar *s = strerror(errno);
111         g_warning("Failed to initialize inotify: %s", s);
112     }
113     else {
114         g_debug("initialized inotify on fd %d", fd);
115         source = g_source_new(&source_funcs, sizeof(InoSource));
116         ino_source = (InoSource*)source;
117         ino_source->notify = notify;
118         ino_source->targets_by_key = g_hash_table_new_full(
119             g_int_hash, g_int_equal, NULL, NULL);
120         ino_source->targets_by_path = g_hash_table_new_full(
121             g_str_hash, g_str_equal, NULL, (GDestroyNotify)target_free);
122         ino_source->pfd = (GPollFD){ fd, G_IO_IN, G_IO_IN };
123         g_source_add_poll(source, &ino_source->pfd);
124     }
125     return source;
126 }
127
128 static gboolean source_prepare(GSource *source, gint *timeout)
129 {
130     *timeout = -1;
131     return FALSE;
132 }
133
134 static gboolean source_check(GSource *source)
135 {
136     return TRUE;
137 }
138
139 static gboolean source_read(GSource *source, GSourceFunc cb, gpointer data)
140 {
141     const gint BUF_LEN = sizeof(struct inotify_event) + 1024;
142     InoSource *ino_source = (InoSource*)source;
143     gchar buffer[BUF_LEN];
144     gchar *name;
145     guint name_len; /* number of bytes read for the name */
146     guint event_len; /* number of bytes read for the event */
147     gint len; /* number of bytes in the buffer */
148     gint pos; /* position in the buffer */
149     enum {
150         READING_EVENT,
151         READING_NAME_BUFFER,
152         READING_NAME_HEAP
153     } state;
154     struct inotify_event event;
155
156     pos = BUF_LEN;
157     state = READING_EVENT;
158     len = event_len = name_len = 0;
159
160     /* sometimes we can end up here even tho no events have been reported by
161        the kernel.  blame glib?  but we'll block if we read in that case. */
162     while (ino_source->pfd.revents) {
163         if (pos == len || !len || event_len) {
164             /* refill the buffer */
165
166             if (event_len)
167                 pos = event_len;
168             else
169                 pos = 0;
170
171             len = read(ino_source->pfd.fd, &buffer[pos], BUF_LEN-pos);
172
173             if (len < 0) {
174                 if (errno != EINTR) {
175                     gchar *s = strerror(errno);
176                     g_warning("Failed to read from inotify: %s", s);
177                     return FALSE; /* won't read any more */
178                 }
179                 else {
180                     g_warning("Interrupted reading from inotify");
181                     return TRUE;
182                 }
183             }
184             if (len == 0) {
185                 g_debug("Done reading from inotify");
186                 return TRUE;
187             }
188
189             g_debug("read %d bytes", len);
190         }
191
192         if (state == READING_EVENT) {
193             const guint remain = len - pos;
194
195             if (remain < sizeof(struct inotify_event)) {
196                 /* there is more of the event struct to read */
197                 guint i;
198                 for (i = 0; i < remain; ++i)
199                     buffer[i] = buffer[pos+i];
200                 g_debug("leftover %d bytes of event struct", remain);
201             }
202             else {
203                 event = *(struct inotify_event*)&buffer[pos];
204                 pos += sizeof(struct inotify_event);
205
206                 g_debug("read event: wd %d mask %x len %d",
207                         event.wd, event.mask, event.len);
208
209                 if (remain >= event.len) {
210                     g_debug("name fits in buffer");
211                     state = READING_NAME_BUFFER;
212                     name = &buffer[pos];
213                     name_len = event.len;
214                     pos += event.len;
215                 }
216                 else { /* remain < event.len */
217                     g_debug("name doesn't fit in buffer");
218                     state = READING_NAME_HEAP;
219                     name = g_new(gchar, event.len);
220                     memcpy(name, &buffer[pos], remain);
221                     name_len = remain;
222                     pos += remain;
223                 }
224             }
225         }
226         if (state == READING_NAME_HEAP && pos < len) {
227             const guint buf_remain = len - pos;
228             const guint name_remain = event.len - name_len;
229             const guint copy_len = MIN(buf_remain, name_remain);
230             memcpy(name+name_len, &buffer[pos], copy_len);
231             name_len += copy_len;
232             pos += copy_len;
233         }
234         if ((state == READING_NAME_BUFFER || state == READING_NAME_HEAP) &&
235             name_len == event.len)
236         {
237             /* done reading the file name ! */
238             InoTarget *t;
239             gboolean report;
240             ObtWatchNotifyType type;
241             gchar *full_path;
242
243             g_debug("read filename %s mask %x", name, event.mask);
244
245             event.mask &= ~IN_IGNORED;  /* skip this one, we watch for things
246                                            to get removed explicitly so this
247                                            will just be double-reporting */
248             if (event.mask) {
249
250                 t = g_hash_table_lookup(ino_source->targets_by_key, &event.wd);
251                 g_assert(t != NULL);
252
253                 full_path = g_build_filename(t->path, name, NULL);
254                 g_debug("full path to change: %s", full_path);
255
256                 /* don't report hidden stuff inside a directory watch */
257                 report = !t->is_dir || name[0] != '.' || t->watch_hidden;
258                 if (event.mask & IN_MOVE_SELF) {
259                     g_warning("Watched target was moved away: %s", t->path);
260                     type = OBT_WATCH_SELF_REMOVED;
261                 }
262                 else if (event.mask & IN_ISDIR) {
263                     if (event.mask & IN_MOVED_TO ||
264                         event.mask & IN_CREATE)
265                     {
266                         add_target(source, t, full_path, t->watch_hidden,
267                                    t->watch_target);
268                         g_debug("added %s", full_path);
269                     }
270                     else if (event.mask & IN_MOVED_FROM ||
271                              event.mask & IN_DELETE)
272                     {
273                         InoTarget *subt;
274
275                         subt = g_hash_table_lookup(ino_source->targets_by_path,
276                                                    full_path);
277                         g_assert(subt);
278                         remove_target(source, subt);
279                         g_debug("removed %s", full_path);
280                     }
281                     report = FALSE;
282                 }
283                 else {
284                     if (event.mask & IN_MOVED_TO || event.mask & IN_CREATE)
285                         type = OBT_WATCH_ADDED;
286                     else if (event.mask & IN_MOVED_FROM ||
287                              event.mask & IN_DELETE)
288                         type = OBT_WATCH_REMOVED;
289                     else if (event.mask & IN_MODIFY)
290                         type = OBT_WATCH_MODIFIED;
291                     else
292                         g_assert_not_reached();
293                 }
294
295                 if (report) {
296                     /* call the GSource callback if there is one */
297                     if (cb) cb(data);
298
299                     /* call the WatchNotify callback */
300                     notify_target(source, t, full_path, type);
301                 }
302
303                 g_free(full_path);
304             }
305
306             if (state == READING_NAME_HEAP)
307                 g_free(name);
308             state = READING_EVENT;
309         }
310     }
311 }
312
313 static void source_finalize(GSource *source)
314 {
315     InoSource *ino_source = (InoSource*)source;
316     g_debug("source_finalize");
317     close(ino_source->pfd.fd);
318     g_hash_table_destroy(ino_source->targets_by_key);
319 }
320
321 static gint add_target(GSource *source, InoTarget *parent,
322                        const gchar *path,
323                        gboolean watch_hidden, gpointer target)
324 {
325     InoSource *ino_source;
326     InoTarget *ino_target;
327     guint32 mask;
328     gint key;
329     gboolean is_dir;
330
331     ino_source = (InoSource*)source;
332
333     is_dir = g_file_test(path, G_FILE_TEST_IS_DIR);
334     if (is_dir)
335         mask = IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVE;
336     else
337         mask = IN_MODIFY;
338     /* only watch IN_MOVE_SELF on the top-most target of the watch */
339     if (!parent)
340         mask |= IN_MOVE_SELF;
341
342     ino_target = NULL;
343     key = inotify_add_watch(ino_source->pfd.fd, path, mask);
344     g_debug("added watch descriptor %d for fd %d on path %s",
345             key, ino_source->pfd.fd, path);
346     if (key < 0) {
347         gchar *s = strerror(errno);
348         g_warning("Unable to watch path %s: %s", path, s);
349     }
350     else {
351         ino_target = g_slice_new(InoTarget);
352         ino_target->key = key;
353         ino_target->path = g_strdup(path);
354         ino_target->base_len = (parent ? parent->base_len : strlen(path));
355         ino_target->is_dir = is_dir;
356         ino_target->watch_hidden = watch_hidden;
357         ino_target->watch_target = target;
358         g_hash_table_insert(ino_source->targets_by_key, &ino_target->key,
359                             ino_target);
360         g_hash_table_insert(ino_source->targets_by_path, ino_target->path,
361                             ino_target);
362     }
363
364     if (key >= 0 && is_dir) {
365         /* recurse */
366         GDir *dir;
367
368         dir = g_dir_open(path, 0, NULL);
369         if (dir) {
370             const gchar *name;
371
372             while ((name = g_dir_read_name(dir))) {
373                 if (name[0] != '.' || watch_hidden) {
374                     gchar *subpath;
375
376                     subpath = g_build_filename(path, name, NULL);
377                     if (g_file_test(subpath, G_FILE_TEST_IS_DIR))
378                         add_target(source, ino_target, subpath,
379                                    watch_hidden, target);
380                     else
381                         /* notify for each file in the directory on startup */
382                         notify_target(source, ino_target, subpath,
383                                       OBT_WATCH_ADDED);
384                     g_free(subpath);
385                 }
386             }
387         }
388         g_dir_close(dir);
389     }
390
391     return key;
392 }
393
394 static void remove_target(GSource *source, InoTarget *target)
395 {
396     InoSource *ino_source = (InoSource*)source;
397     g_debug("removing wd %d for fd %d", target->key, ino_source->pfd.fd);
398     inotify_rm_watch(ino_source->pfd.fd, (guint32)target->key);
399     g_hash_table_remove(ino_source->targets_by_key, &target->key);
400     g_hash_table_remove(ino_source->targets_by_path, target->path);
401 }
402
403 static void target_free(InoTarget *target)
404 {
405     g_free(target->path);
406     g_slice_free(InoTarget, target);
407 }
408
409 static void notify_target(GSource *source, InoTarget *ino_target,
410                           const gchar *path, ObtWatchNotifyType type)
411 {
412     InoSource *ino_source = (InoSource*)source;
413     ino_source->notify(path + ino_target->base_len,
414                        path,
415                        ino_target->watch_target,
416                        type);
417 }
418
419 #endif