/*
Copyright (C) 2005 John McCutchan
Copyright © 2015 Canonical Limited
Copyright © 2024 Future Crew LLC
SPDX-License-Identifier: LGPL-2.1-or-later
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this library; if not, see .
Authors:
Ryan Lortie
John McCutchan
Gleb Popov
*/
#include "config.h"
#include
#include
#include
#include
#include
#include
#include "inotify-kernel.h"
#include
#ifdef HAVE_SYS_UIO_H
#include
#endif
#ifdef HAVE_SYS_FILIO_H
#include
#endif
#include
#include "glib-private.h"
/* From inotify(7) */
#define MAX_EVENT_SIZE (sizeof(struct inotify_event) + NAME_MAX + 1)
/* Amount of time to sleep on receipt of uninteresting events */
#define BOREDOM_SLEEP_TIME (100 * G_TIME_SPAN_MILLISECOND)
/* Define limits on the maximum amount of time and maximum amount of
* interceding events between FROM/TO that can be merged.
*/
#define MOVE_PAIR_DELAY (10 * G_TIME_SPAN_MILLISECOND)
#define MOVE_PAIR_DISTANCE (100)
/* We use the lock from inotify-helper.c
*
* We only have to take it on our read callback.
*
* The rest of locking is taken care of in inotify-helper.c
*/
G_LOCK_EXTERN (inotify_lock);
static ik_event_t *
ik_event_new (struct inotify_event *kevent,
gint64 now)
{
ik_event_t *event = g_new0 (ik_event_t, 1);
event->wd = kevent->wd;
event->mask = kevent->mask;
event->cookie = kevent->cookie;
event->len = kevent->len;
event->timestamp = now;
if (event->len)
event->name = g_strdup (kevent->name);
else
event->name = NULL;
return event;
}
void
_ik_event_free (ik_event_t *event)
{
if (event->pair)
{
event->pair->pair = NULL;
_ik_event_free (event->pair);
}
g_free (event->name);
g_free (event);
}
typedef struct
{
GSource source;
GQueue queue; /* (element-type ik_event_t) */
gpointer fd_tag;
gint fd;
GHashTable *unmatched_moves; /* (element-type guint ik_event_t) */
gboolean is_bored;
} InotifyKernelSource;
static InotifyKernelSource *inotify_source;
static gint64
ik_source_get_dispatch_time (InotifyKernelSource *iks)
{
ik_event_t *head;
head = g_queue_peek_head (&iks->queue);
/* nothing in the queue: not ready */
if (!head)
return -1;
/* if it's not an unpaired move, it is ready now */
if (~head->mask & IN_MOVED_FROM || head->pair)
return 0;
/* if the queue is too long then it's ready now */
if (iks->queue.length > MOVE_PAIR_DISTANCE)
return 0;
/* otherwise, it's ready after the delay */
return head->timestamp + MOVE_PAIR_DELAY;
}
static gboolean
ik_source_can_dispatch_now (InotifyKernelSource *iks,
gint64 now)
{
gint64 dispatch_time;
dispatch_time = ik_source_get_dispatch_time (iks);
return 0 <= dispatch_time && dispatch_time <= now;
}
static gsize
ik_source_read_some_events (InotifyKernelSource *iks,
gchar *buffer,
gsize buffer_len)
{
gssize result;
int errsv;
again:
result = read (iks->fd, buffer, buffer_len);
errsv = errno;
if (result < 0)
{
if (errsv == EINTR)
goto again;
if (errsv == EAGAIN)
return 0;
g_error ("inotify read(): %s", g_strerror (errsv));
}
else if (result == 0)
g_error ("inotify unexpectedly hit eof");
return result;
}
static gchar *
ik_source_read_all_the_events (InotifyKernelSource *iks,
gchar *buffer,
gsize buffer_len,
gsize *length_out)
{
gsize n_read;
n_read = ik_source_read_some_events (iks, buffer, buffer_len);
/* Check if we might have gotten another event if we had passed in a
* bigger buffer...
*/
if (n_read + MAX_EVENT_SIZE > buffer_len)
{
gchar *new_buffer;
guint n_readable;
gint result;
int errsv;
/* figure out how many more bytes there are to read */
result = ioctl (iks->fd, FIONREAD, &n_readable);
errsv = errno;
if (result != 0)
g_error ("inotify ioctl(FIONREAD): %s", g_strerror (errsv));
if (n_readable != 0)
{
/* there is in fact more data. allocate a new buffer, copy
* the existing data, and then append the remaining.
*/
new_buffer = g_malloc (n_read + n_readable);
memcpy (new_buffer, buffer, n_read);
n_read += ik_source_read_some_events (iks, new_buffer + n_read, n_readable);
buffer = new_buffer;
/* There may be new events in the buffer that were added after
* the FIONREAD was performed, but we can't risk getting into
* a loop. We'll get them next time.
*/
}
}
*length_out = n_read;
return buffer;
}
static gboolean
ik_source_dispatch (GSource *source,
GSourceFunc func,
gpointer user_data)
{
InotifyKernelSource *iks = (InotifyKernelSource *) source;
gboolean (*user_callback) (ik_event_t *event) = (void *) func;
gboolean interesting = FALSE;
gint64 now;
now = g_source_get_time (source);
if (iks->is_bored || g_source_query_unix_fd (source, iks->fd_tag))
{
#if defined(FILE_MONITOR_BACKEND_INOTIFY)
gchar stack_buffer[4096];
gsize buffer_len;
gchar *buffer;
gsize offset;
/* We want to read all of the available events.
*
* We need to do it in a finite number of steps so that we don't
* get caught in a loop of read() with another process
* continuously adding events each time we drain them.
*
* In the normal case we will have only a few events in the queue,
* so start out by reading into a small stack-allocated buffer.
* Even though we're on a fresh stack frame, there is no need to
* pointlessly blow up with the size of the worker thread stack
* with a huge buffer here.
*
* If the result is large enough to cause us to suspect that
* another event may be pending then we allocate a buffer on the
* heap that can hold all of the events and read (once!) into that
* buffer.
*/
buffer = ik_source_read_all_the_events (iks, stack_buffer, sizeof stack_buffer, &buffer_len);
offset = 0;
while (offset < buffer_len)
{
struct inotify_event *kevent = (struct inotify_event *) (buffer + offset);
ik_event_t *event;
event = ik_event_new (kevent, now);
offset += sizeof (struct inotify_event) + event->len;
if (event->mask & IN_MOVED_TO)
{
ik_event_t *pair;
if (g_hash_table_steal_extended (iks->unmatched_moves, GUINT_TO_POINTER (event->cookie), NULL, (gpointer*)&pair))
{
g_assert (!pair->pair);
event->is_second_in_pair = TRUE;
event->pair = pair;
pair->pair = event;
continue;
}
interesting = TRUE;
}
else if (event->mask & IN_MOVED_FROM)
{
gboolean new;
new = g_hash_table_insert (iks->unmatched_moves, GUINT_TO_POINTER (event->cookie), event);
if G_UNLIKELY (!new)
g_warning ("inotify: got IN_MOVED_FROM event with already-pending cookie %#x", event->cookie);
interesting = TRUE;
}
g_queue_push_tail (&iks->queue, event);
}
if (buffer_len == 0)
{
/* We can end up reading nothing if we arrived here due to a
* boredom timer but the stream of events stopped meanwhile.
*
* In that case, we need to switch back to polling the file
* descriptor in the usual way.
*/
g_assert (iks->is_bored);
interesting = TRUE;
}
if (buffer != stack_buffer)
g_free (buffer);
#elif defined(FILE_MONITOR_BACKEND_LIBINOTIFY_KQUEUE)
struct iovec *received[5];
int num_events = libinotify_direct_readv (iks->fd, received, G_N_ELEMENTS(received), /* no_block=*/ 1);
if (num_events < 0)
{
int errsv = errno;
g_warning ("Failed to read inotify events: %s", g_strerror (errsv));
/* fall through and skip the next few blocks */
}
for (int i = 0; i < num_events; i++)
{
struct iovec *cur_event = received[i];
while (cur_event->iov_base)
{
struct inotify_event *kevent = (struct inotify_event *) cur_event->iov_base;
ik_event_t *event;
event = ik_event_new (kevent, now);
if (event->mask & IN_MOVED_TO)
{
ik_event_t *pair;
if (g_hash_table_steal_extended (iks->unmatched_moves, GUINT_TO_POINTER (event->cookie), NULL, (gpointer*)&pair))
{
g_assert (!pair->pair);
event->is_second_in_pair = TRUE;
event->pair = pair;
pair->pair = event;
cur_event++;
continue;
}
interesting = TRUE;
}
else if (event->mask & IN_MOVED_FROM)
{
gboolean new;
new = g_hash_table_insert (iks->unmatched_moves, GUINT_TO_POINTER (event->cookie), event);
if G_UNLIKELY (!new)
g_warning ("inotify: got IN_MOVED_FROM event with already-pending cookie %#x", event->cookie);
interesting = TRUE;
}
g_queue_push_tail (&iks->queue, event);
cur_event++;
}
libinotify_free_iovec (received[i]);
}
if (num_events == 0)
{
/* We can end up reading nothing if we arrived here due to a
* boredom timer but the stream of events stopped meanwhile.
*
* In that case, we need to switch back to polling the file
* descriptor in the usual way.
*/
g_assert (iks->is_bored);
interesting = TRUE;
}
#endif
}
while (ik_source_can_dispatch_now (iks, now))
{
ik_event_t *event;
/* callback will free the event */
event = g_queue_pop_head (&iks->queue);
if (event->mask & IN_MOVED_FROM && !event->pair)
g_hash_table_remove (iks->unmatched_moves, GUINT_TO_POINTER (event->cookie));
G_LOCK (inotify_lock);
interesting |= (* user_callback) (event);
G_UNLOCK (inotify_lock);
}
/* The queue gets blocked iff we have unmatched moves */
g_assert ((iks->queue.length > 0) == (g_hash_table_size (iks->unmatched_moves) > 0));
/* Here's where we decide what will wake us up next.
*
* If the last event was interesting then we will wake up on the fd or
* when the timeout is reached on an unpaired move (if any).
*
* If the last event was uninteresting then we will wake up after the
* shorter of the boredom sleep or any timeout for an unpaired move.
*/
if (interesting)
{
if (iks->is_bored)
{
g_source_modify_unix_fd (source, iks->fd_tag, G_IO_IN);
iks->is_bored = FALSE;
}
g_source_set_ready_time (source, ik_source_get_dispatch_time (iks));
}
else
{
guint64 dispatch_time = ik_source_get_dispatch_time (iks);
guint64 boredom_time = now + BOREDOM_SLEEP_TIME;
if (!iks->is_bored)
{
g_source_modify_unix_fd (source, iks->fd_tag, 0);
iks->is_bored = TRUE;
}
g_source_set_ready_time (source, MIN (dispatch_time, boredom_time));
}
return TRUE;
}
static void
ik_source_finalize (GSource *source)
{
InotifyKernelSource *iks;
iks = (InotifyKernelSource *) source;
#if defined(FILE_MONITOR_BACKEND_INOTIFY)
close (iks->fd);
#elif defined(FILE_MONITOR_BACKEND_LIBINOTIFY_KQUEUE)
libinotify_direct_close (iks->fd);
#endif
iks->fd = -1;
}
static InotifyKernelSource *
ik_source_new (gboolean (* callback) (ik_event_t *event))
{
static GSourceFuncs source_funcs = {
NULL, NULL,
ik_source_dispatch,
ik_source_finalize,
NULL, NULL
};
InotifyKernelSource *iks;
GSource *source;
gboolean should_set_nonblock = FALSE;
source = g_source_new (&source_funcs, sizeof (InotifyKernelSource));
iks = (InotifyKernelSource *) source;
g_source_set_static_name (source, "inotify kernel source");
iks->unmatched_moves = g_hash_table_new (NULL, NULL);
#if defined(FILE_MONITOR_BACKEND_INOTIFY)
iks->fd = inotify_init1 (IN_CLOEXEC | IN_NONBLOCK);
#elif defined(FILE_MONITOR_BACKEND_LIBINOTIFY_KQUEUE)
iks->fd = inotify_init1 (IN_CLOEXEC | IN_NONBLOCK | IN_DIRECT);
#endif
#ifdef FILE_MONITOR_BACKEND_INOTIFY
if (iks->fd < 0)
{
should_set_nonblock = TRUE;
iks->fd = inotify_init ();
}
#endif
if (iks->fd >= 0)
{
GError *error = NULL;
#ifdef FILE_MONITOR_BACKEND_INOTIFY
if (should_set_nonblock)
{
g_unix_set_fd_nonblocking (iks->fd, TRUE, &error);
g_assert_no_error (error);
}
#endif
iks->fd_tag = g_source_add_unix_fd (source, iks->fd, G_IO_IN);
}
g_source_set_callback (source, (GSourceFunc) callback, NULL, NULL);
g_source_attach (source, GLIB_PRIVATE_CALL (g_get_worker_context) ());
return iks;
}
gboolean
_ik_startup (gboolean (*cb)(ik_event_t *event))
{
if (g_once_init_enter_pointer (&inotify_source))
g_once_init_leave_pointer (&inotify_source, ik_source_new (cb));
return inotify_source->fd >= 0;
}
gint32
_ik_watch (const char *path,
guint32 mask,
int *err)
{
gint32 wd = -1;
g_assert (path != NULL);
g_assert (inotify_source && inotify_source->fd >= 0);
wd = inotify_add_watch (inotify_source->fd, path, mask);
if (wd < 0)
{
int e = errno;
/* FIXME: debug msg failed to add watch */
if (err)
*err = e;
return wd;
}
g_assert (wd >= 0);
return wd;
}
int
_ik_ignore (const char *path,
gint32 wd)
{
g_assert (wd >= 0);
g_assert (inotify_source && inotify_source->fd >= 0);
if (inotify_rm_watch (inotify_source->fd, wd) < 0)
{
/* int e = errno; */
/* failed to rm watch */
return -1;
}
return 0;
}