/**
* @file filewatcher.c
* Mode Control Entity - flag file tracking
*
* Copyright (C) 2013-2019 Jolla Ltd.
*
* @author Simo Piiroinen
*
* mce is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* mce 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 mce. If not, see .
*/
#include "filewatcher.h"
#include "mce-log.h"
#include
#include
#include
#include
#define DEBUG_INOTIFY_EVENTS 0
static inline void *lea(const void *base, int offs)
{
return ((char *)base)+offs;
}
/* ------------------------------------------------------------------------- *
* Inotify event debugging helpers
* ------------------------------------------------------------------------- */
#if DEBUG_INOTIFY_EVENTS
/** Convert inotify event bitmask to human readable string
*
* @param mask event bit mask
* @param buff where to construct the string
* @param size how much space buff has (must be > 0)
*
* @return string with names of set bits separated with '+'
*/
static
const char *
inotify_mask_repr(uint32_t mask, char *buff, size_t size)
{
static const struct
{
uint32_t mask;
const char *name;
} lut[] =
{
# define X(tag) { .mask = IN_##tag, .name = #tag },
X(ACCESS)
X(MODIFY)
X(ATTRIB)
X(CLOSE_WRITE)
X(CLOSE_NOWRITE)
X(OPEN)
X(MOVED_FROM)
X(MOVED_TO)
X(CREATE)
X(DELETE)
X(DELETE_SELF)
X(MOVE_SELF)
X(UNMOUNT)
X(Q_OVERFLOW)
X(IGNORED)
X(ONLYDIR)
X(DONT_FOLLOW)
X(EXCL_UNLINK)
X(MASK_ADD)
X(ISDIR)
X(ONESHOT)
# undef X
{ .mask = 0, .name = 0 }
};
char *pos = buff;
char *end = buff + size - 1;
auto void adds(const char *s)
{
while( *s && pos < end) *pos++ = *s++;
}
for( size_t i = 0; lut[i].mask; ++i )
{
if( mask & lut[i].mask )
{
mask ^= lut[i].mask;
if( pos > buff ) adds("+");
adds(lut[i].name);
}
}
if( mask )
{
char hex[32];
snprintf(hex, sizeof hex, "0x%"PRIx32, mask);
if( pos > buff ) adds("+");
adds(hex);
}
return *pos = 0, buff;
}
/** Emit inotify event details
*
* @param eve inotify_event pointer
*/
static
void
inotify_event_debug(const struct inotify_event *eve)
{
char temp[256];
printf("wd=%d\n", eve->wd);
printf("mask=%s\n", inotify_mask_repr(eve->mask, temp, sizeof temp));
if( eve->len )
{
printf("name=\"%s\"\n", eve->name);
}
printf("\n");
}
#endif /* DEBUG_INOTIFY_EVENTS */
/* ------------------------------------------------------------------------- *
* File content change tracking
* ------------------------------------------------------------------------- */
/** Object for tracking file content in a directory */
struct filewatcher_t
{
/** inotify file descriptor */
int inotify_fd;
/** inotify watch descriptor */
int inotify_wd;
/** glib input watch for inotify_fd */
guint watch_id;
/** the directory to watch over */
char *watch_path;
/** the file in the watch_path to track */
char *watch_file;
/** function to call when watch_path/watch_file changes */
filewatcher_changed_fn changed_cb;
/** user data to pass to changed_cb */
gpointer user_data;
/** how to delete user_data when filewatcher_t is deleted */
GDestroyNotify delete_cb;
};
/* Initialize filewatcher_t object to a sane state
*
* @param self pointer to uninitialized filewatcher_t object
*/
static
void
filewatcher_ctor(filewatcher_t *self)
{
self->inotify_fd = -1;
self->inotify_wd = -1;
self->watch_path = 0;
self->watch_file = 0;
self->watch_id = 0;
self->changed_cb = 0;
self->delete_cb = 0;
self->user_data = 0;
}
/* Release all dynamic data from filewatcher_t object
*
* @param self pointer to initialized filewatcher_t object
*/
static
void
filewatcher_dtor(filewatcher_t *self)
{
/* detach user data */
if( self->delete_cb )
{
self->delete_cb(self->user_data);
}
self->user_data = 0;
/* detach glib io watch */
if( self->watch_id )
{
g_source_remove(self->watch_id), self->watch_id = 0;
}
/* detach inotify fd */
if( self->inotify_fd != -1 )
{
if( self->inotify_wd != -1 )
{
if( inotify_rm_watch(self->inotify_fd, self->inotify_wd) == -1 )
{
mce_log(LL_WARN, "inotify_rm_watch: %m");
}
self->inotify_wd = -1;
}
if( close(self->inotify_fd) == -1 )
{
mce_log(LL_WARN, "close inotify fd: %m");
}
self->inotify_fd = -1;
}
/* release strings */
g_free(self->watch_path), self->watch_path = 0;
g_free(self->watch_file), self->watch_file = 0;
}
/* Delete a filewatcher_t object
*
* @param self pointer to initialized filewatcher_t object, or NULL
*/
void
filewatcher_delete(filewatcher_t *self)
{
if( self != 0 )
{
filewatcher_dtor(self);
g_free(self);
}
}
/** Process inotify events
*
* @param self pointer to filewatcher_t object
*
* @return TRUE on success, or FALSE if further processing is not possible
*/
static
gboolean
filewatcher_process_events(filewatcher_t *self)
{
gboolean res = FALSE;
gboolean flg = FALSE;
char buf[2048];
int todo, size;
struct inotify_event *eve;
if( !self || self->inotify_fd == -1 )
{
goto cleanup;
}
todo = read(self->inotify_fd, buf, sizeof buf);
if( todo < 0 )
{
switch( errno )
{
case EAGAIN:
case EINTR:
res = TRUE;
break;
default:
mce_log(LL_WARN, "read inotify events: %m");
break;
}
goto cleanup;
}
if( todo == 0 )
{
mce_log(LL_WARN, "read inotify events: EOF");
goto cleanup;
}
#if DEBUG_INOTIFY_EVENTS
printf("----\n");
#endif
for( eve = lea(buf, 0); todo; todo -= size, eve = lea(eve, size))
{
if( todo < (int)sizeof *eve )
{
mce_log(LL_WARN, "partial inotify event received");
goto cleanup;
}
size = sizeof *eve + eve->len;
if( todo < size )
{
mce_log(LL_WARN, "oversized inotify event received");
goto cleanup;
}
#if DEBUG_INOTIFY_EVENTS
inotify_event_debug(eve);
#endif
if( eve->len && !strcmp(self->watch_file, eve->name) )
{
flg = TRUE;
}
if( eve->mask & IN_IGNORED )
{
mce_log(LL_ERR, "inotify watch went defunct");
flg = TRUE;
goto cleanup;
}
}
res = TRUE;
cleanup:
if( flg && self && self->changed_cb )
{
self->changed_cb(self->watch_path, self->watch_file, self->user_data);
}
return res;
}
/** Glib io glue for processing inotify event input
*
* @param source (not used)
* @param condition (not used)
* @param data pointer to filewatcher_t object (as void pointer)
*
* @return TRUE to keep the io watch alive, or
* FALSE if the io watch must be released
*/
static
gboolean
filewatcher_input_cb(GIOChannel *source,
GIOCondition condition,
gpointer data)
{
(void)source;
filewatcher_t *self = data;
gboolean keep_going = TRUE;
if( condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL) )
{
keep_going = FALSE;
}
if( !filewatcher_process_events(self) )
{
keep_going = FALSE;
}
if( !keep_going )
{
/* Note: This /should/ never happen, but if it does
* we must not leave the io watch in a state
* where it gets triggered forever. */
mce_log(LL_CRIT, "stopping inotify event io watch");
self->watch_id = 0;
}
return keep_going;
}
/** Helper for setting up inotify file descriptor
*
* @note This function is meant to be called form
* filewatcher_create() function only!
*
* @param self pointer to filewatcher_t object
*
* @return TRUE on success, or FALSE on failure
*/
static
gboolean
filewatcher_setup_inotify(filewatcher_t *self)
{
gboolean success = FALSE;
uint32_t mask = (0
| IN_CREATE
| IN_DELETE
| IN_CLOSE_WRITE
| IN_MOVED_TO
| IN_MOVED_FROM
| IN_DONT_FOLLOW
| IN_ONLYDIR);
self->inotify_fd = inotify_init1(IN_CLOEXEC);
if( self->inotify_fd == -1 )
{
mce_log(LL_WARN, "inotify_init: %m");
goto cleanup;
}
self->inotify_wd = inotify_add_watch(self->inotify_fd,
self->watch_path, mask);
if( self->inotify_wd == -1 )
{
mce_log(LL_WARN, "%s: inotify_add_watch: %m", self->watch_path);
goto cleanup;
}
success = TRUE;
cleanup:
return success;
}
/** Helper for setting up glib io watch for inotify file descriptor
*
* @note This function is meant to be called form
* filewatcher_create() function only!
*
* @param self pointer to filewatcher_t object
*
* @return TRUE on success, or FALSE on failure
*/
static
gboolean
filewatcher_setup_iowatch(filewatcher_t *self)
{
gboolean success = FALSE;
GIOChannel *chan = 0;
GError *err = 0;
if( !(chan = g_io_channel_unix_new(self->inotify_fd)) )
{
mce_log(LL_WARN, "%s: %m", "g_io_channel_unix_new");
goto cleanup;
}
/* the channel does not own the fd */
g_io_channel_set_close_on_unref(chan, FALSE);
/* Set to NULL encoding so that we can turn off the buffering */
if( g_io_channel_set_encoding(chan, NULL, &err) != G_IO_STATUS_NORMAL )
{
mce_log(LL_WARN, "%s: %s", "g_io_channel_set_encoding",
(err && err->message) ? err->message : "unknown");
}
g_io_channel_set_buffered(chan, FALSE);
self->watch_id = g_io_add_watch(chan,
G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
filewatcher_input_cb, self);
if( !self->watch_id )
{
mce_log(LL_WARN, "%s: %m", "g_io_add_watch");
goto cleanup;
}
success = TRUE;
cleanup:
g_clear_error(&err);
if( chan ) g_io_channel_unref(chan);
return success;
}
/** Create an filewatcher_t object
*
* An inotify watcher is started for the given director/file.
* A glib io watch is used to process the inotify events.
* The change_cb is called when contents of the tracked file
* are assumed to have changed.
*
* @note The change_cb function will not be called during the
* initialization. You can make initial state evaluation
* to happen by calling filewatcher_force_trigger() after
* succesfull filewatcher_create().
*
* @param dirpath directory to watch over
* @param filename file to watch in dirpath
* @param change_cb function to call when dirpath/filename changes
* @param user_data extra parameter to pass to change_cb
* @param delete_cb called on user_data when filewatcher_t itself is deleted
*
* @return pointer to filewatcher_t object, or NULL in case of errors
*/
filewatcher_t *
filewatcher_create(const char *dirpath,
const char *filename,
filewatcher_changed_fn change_cb,
gpointer user_data,
GDestroyNotify delete_cb)
{
gboolean success = FALSE;
filewatcher_t *self = g_malloc0(sizeof *self);
filewatcher_ctor(self);
self->watch_path = g_strdup(dirpath);
self->watch_file = g_strdup(filename);
self->changed_cb = change_cb;
self->user_data = user_data;
self->delete_cb = delete_cb;
if( !filewatcher_setup_inotify(self) )
{
goto cleanup;
}
if( !filewatcher_setup_iowatch(self) )
{
goto cleanup;
}
success = TRUE;
cleanup:
if( !success )
{
filewatcher_delete(self), self = 0;
}
return self;
}
/** Force calling the change notification callback
*
* This can be useful for example to feed initial
* state of tracked file via the same mechanism as
* the later changes get reported
*
* @param self pointer to filewatcher_t object
*/
void
filewatcher_force_trigger(filewatcher_t *self)
{
if( self->changed_cb )
{
self->changed_cb(self->watch_path, self->watch_file, self->user_data);
}
}