blob: 190a5e109c4b16ad16ad43dc204c874c6689f3b5 [file] [log] [blame]
/* Copyright 2016 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/inotify.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "cras_file_wait.h"
#define CRAS_FILE_WAIT_EVENT_MIN_SIZE sizeof(struct inotify_event)
#define CRAS_FILE_WAIT_EVENT_SIZE (CRAS_FILE_WAIT_EVENT_MIN_SIZE + NAME_MAX + 1)
#define CRAS_FILE_WAIT_FLAG_MOCK_RACE (1u << 31)
struct cras_file_wait {
cras_file_wait_callback_t callback;
void *callback_context;
const char *file_path;
size_t file_path_len;
char *watch_path;
char *watch_dir;
char *watch_file_name;
size_t watch_file_name_len;
int inotify_fd;
int watch_id;
char event_buf[CRAS_FILE_WAIT_EVENT_SIZE];
cras_file_wait_flag_t flags;
};
int cras_file_wait_get_fd(struct cras_file_wait *file_wait)
{
if (!file_wait)
return -EINVAL;
if (file_wait->inotify_fd < 0)
return -EINVAL;
return file_wait->inotify_fd;
}
/* Defined for the unittest. */
void cras_file_wait_mock_race_condition(struct cras_file_wait *file_wait);
void cras_file_wait_mock_race_condition(struct cras_file_wait *file_wait)
{
if (file_wait)
file_wait->flags |= CRAS_FILE_WAIT_FLAG_MOCK_RACE;
}
void cras_file_wait_destroy(struct cras_file_wait *file_wait)
{
if (!file_wait)
return;
if (file_wait->inotify_fd >= 0)
close(file_wait->inotify_fd);
free(file_wait);
}
static int cras_file_wait_rm_watch(struct cras_file_wait *file_wait)
{
int rc;
file_wait->watch_path[0] = 0;
file_wait->watch_dir[0] = 0;
file_wait->watch_file_name[0] = 0;
file_wait->watch_file_name_len = 0;
if (file_wait->inotify_fd >= 0 && file_wait->watch_id >= 0) {
rc = inotify_rm_watch(file_wait->inotify_fd,
file_wait->watch_id);
file_wait->watch_id = -1;
if (rc < 0)
return -errno;
}
return 0;
}
int cras_file_wait_process_event(struct cras_file_wait *file_wait,
struct inotify_event *event)
{
cras_file_wait_event_t file_wait_event;
syslog(LOG_DEBUG,
"file_wait->watch_id: %d, event->wd: %d"
", event->mask: %x, event->name: %s",
file_wait->watch_id, event->wd, event->mask,
event->len ? event->name : "");
if (event->wd != file_wait->watch_id)
return 0;
if (event->mask & IN_IGNORED) {
/* The watch has been removed. */
file_wait->watch_id = -1;
return cras_file_wait_rm_watch(file_wait);
}
if (event->len == 0 ||
memcmp(event->name, file_wait->watch_file_name,
file_wait->watch_file_name_len + 1) != 0) {
/* Some file we don't care about. */
return 0;
}
if ((event->mask & (IN_CREATE | IN_MOVED_TO)) != 0)
file_wait_event = CRAS_FILE_WAIT_EVENT_CREATED;
else if ((event->mask & (IN_DELETE | IN_MOVED_FROM)) != 0)
file_wait_event = CRAS_FILE_WAIT_EVENT_DELETED;
else
return 0;
/* Found the file! */
if (strcmp(file_wait->watch_path, file_wait->file_path) == 0) {
/* Tell the caller about this creation or deletion. */
file_wait->callback(file_wait->callback_context,
file_wait_event, event->name);
} else {
/* Remove the watch for this file, move on. */
return cras_file_wait_rm_watch(file_wait);
}
return 0;
}
int cras_file_wait_dispatch(struct cras_file_wait *file_wait)
{
struct inotify_event *event;
char *watch_dir_end;
size_t watch_dir_len;
char *watch_file_start;
size_t watch_path_len;
int rc = 0;
int flags;
ssize_t read_rc;
ssize_t read_offset;
if (!file_wait)
return -EINVAL;
/* If we have a file-descriptor, then read it and see what's up. */
if (file_wait->inotify_fd >= 0) {
read_offset = 0;
read_rc = read(file_wait->inotify_fd, file_wait->event_buf,
CRAS_FILE_WAIT_EVENT_SIZE);
if (read_rc < 0) {
rc = -errno;
if ((rc == -EAGAIN || rc == -EWOULDBLOCK) &&
file_wait->watch_id < 0) {
/* Really nothing to read yet: we need to
* setup a watch. */
rc = 0;
}
} else if (read_rc < CRAS_FILE_WAIT_EVENT_MIN_SIZE) {
rc = -EIO;
} else if (file_wait->watch_id < 0) {
/* Processing messages related to old watches. */
rc = 0;
} else
while (rc == 0 && read_offset < read_rc) {
event = (struct inotify_event
*)(file_wait->event_buf +
read_offset);
read_offset += sizeof(*event) + event->len;
rc = cras_file_wait_process_event(file_wait,
event);
}
}
/* Report errors from above here. */
if (rc < 0)
return rc;
if (file_wait->watch_id >= 0) {
/* Assume that the watch that we have is the right one. */
return 0;
}
/* Initialize inotify if we haven't already. */
if (file_wait->inotify_fd < 0) {
file_wait->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (file_wait->inotify_fd < 0)
return -errno;
}
/* Figure out what we need to watch next. */
rc = -ENOENT;
strcpy(file_wait->watch_dir, file_wait->file_path);
watch_dir_len = file_wait->file_path_len;
while (rc == -ENOENT || rc == -EACCES) {
strcpy(file_wait->watch_path, file_wait->watch_dir);
watch_path_len = watch_dir_len;
/* Find the end of the parent directory. */
watch_dir_end = file_wait->watch_dir + watch_dir_len - 1;
while (watch_dir_end > file_wait->watch_dir &&
*watch_dir_end != '/')
watch_dir_end--;
watch_file_start = watch_dir_end + 1;
/* Treat consecutive '/' characters as one. */
while (watch_dir_end > file_wait->watch_dir &&
*(watch_dir_end - 1) == '/')
watch_dir_end--;
watch_dir_len = watch_dir_end - file_wait->watch_dir;
if (watch_dir_len == 0) {
/* We're looking for a file in the current directory. */
strcpy(file_wait->watch_file_name,
file_wait->watch_path);
file_wait->watch_file_name_len = watch_path_len;
strcpy(file_wait->watch_dir, ".");
watch_dir_len = 1;
} else {
/* Copy out the file name that we're looking for, and
* mark the end of the directory path. */
strcpy(file_wait->watch_file_name, watch_file_start);
file_wait->watch_file_name_len =
watch_path_len -
(watch_file_start - file_wait->watch_dir);
*watch_dir_end = 0;
}
if (file_wait->flags & CRAS_FILE_WAIT_FLAG_MOCK_RACE) {
/* For testing only. */
mknod(file_wait->watch_path, S_IFREG | 0600, 0);
file_wait->flags &= ~CRAS_FILE_WAIT_FLAG_MOCK_RACE;
}
flags = IN_CREATE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM;
file_wait->watch_id = inotify_add_watch(
file_wait->inotify_fd, file_wait->watch_dir, flags);
if (file_wait->watch_id < 0) {
rc = -errno;
continue;
}
/* Satisfy the race condition between existence of the
* file and creation of the watch. */
rc = access(file_wait->watch_path, F_OK);
if (rc < 0) {
rc = -errno;
if (rc == -ENOENT) {
/* As expected, the file still doesn't exist. */
rc = 0;
}
continue;
}
/* The file we're looking for exists. */
if (strcmp(file_wait->watch_path, file_wait->file_path) == 0) {
file_wait->callback(file_wait->callback_context,
CRAS_FILE_WAIT_EVENT_CREATED,
file_wait->watch_file_name);
return 0;
}
/* Start over again. */
rc = cras_file_wait_rm_watch(file_wait);
if (rc < 0)
return rc;
rc = -ENOENT;
strcpy(file_wait->watch_dir, file_wait->file_path);
watch_dir_len = file_wait->file_path_len;
}
/* Get out for permissions problems for example. */
return rc;
}
int cras_file_wait_create(const char *file_path, cras_file_wait_flag_t flags,
cras_file_wait_callback_t callback,
void *callback_context,
struct cras_file_wait **file_wait_out)
{
struct cras_file_wait *file_wait;
size_t file_path_len;
int rc;
if (!file_path || !*file_path || !callback || !file_wait_out)
return -EINVAL;
*file_wait_out = NULL;
/* Create a struct cras_file_wait to track waiting for this file. */
file_path_len = strlen(file_path);
file_wait = (struct cras_file_wait *)calloc(
1, sizeof(*file_wait) + ((file_path_len + 1) * 5));
if (!file_wait)
return -ENOMEM;
file_wait->callback = callback;
file_wait->callback_context = callback_context;
file_wait->inotify_fd = -1;
file_wait->watch_id = -1;
file_wait->file_path_len = file_path_len;
file_wait->flags = flags;
/* We've allocated memory such that the file_path, watch_path,
* watch_dir, and watch_file_name data are appended to the end of
* our cras_file_wait structure. */
file_wait->file_path = (const char *)file_wait + sizeof(*file_wait);
file_wait->watch_path =
(char *)file_wait->file_path + file_path_len + 1;
file_wait->watch_dir = file_wait->watch_path + file_path_len + 1;
file_wait->watch_file_name = file_wait->watch_dir + file_path_len + 1;
memcpy((void *)file_wait->file_path, file_path, file_path_len + 1);
/* Setup the first watch. If that fails unexpectedly, then we destroy
* the file wait structure immediately. */
rc = cras_file_wait_dispatch(file_wait);
if (rc < 0) {
cras_file_wait_destroy(file_wait);
return rc;
}
*file_wait_out = file_wait;
return 0;
}