| /* 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; |
| } |