diff options
Diffstat (limited to 'src/lib/file-lock.c')
-rw-r--r-- | src/lib/file-lock.c | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/src/lib/file-lock.c b/src/lib/file-lock.c new file mode 100644 index 0000000..f17beea --- /dev/null +++ b/src/lib/file-lock.c @@ -0,0 +1,530 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "istream.h" +#include "file-lock.h" +#include "file-dotlock.h" +#include "time-util.h" + +#include <time.h> +#include <sys/stat.h> +#ifdef HAVE_FLOCK +# include <sys/file.h> +#endif + +struct file_lock { + struct file_lock_settings set; + + int fd; + char *path; + struct dotlock *dotlock; + + struct timeval locked_time; + int lock_type; +}; + +static struct timeval lock_wait_start; +static uint64_t file_lock_wait_usecs = 0; +static long long file_lock_slow_warning_usecs = -1; + +static void file_lock_log_warning_if_slow(struct file_lock *lock); + +bool file_lock_method_parse(const char *name, enum file_lock_method *method_r) +{ + if (strcasecmp(name, "fcntl") == 0) + *method_r = FILE_LOCK_METHOD_FCNTL; + else if (strcasecmp(name, "flock") == 0) + *method_r = FILE_LOCK_METHOD_FLOCK; + else if (strcasecmp(name, "dotlock") == 0) + *method_r = FILE_LOCK_METHOD_DOTLOCK; + else + return FALSE; + return TRUE; +} + +const char *file_lock_method_to_str(enum file_lock_method method) +{ + switch (method) { + case FILE_LOCK_METHOD_FCNTL: + return "fcntl"; + case FILE_LOCK_METHOD_FLOCK: + return "flock"; + case FILE_LOCK_METHOD_DOTLOCK: + return "dotlock"; + } + i_unreached(); +} + +int file_try_lock(int fd, const char *path, int lock_type, + const struct file_lock_settings *set, + struct file_lock **lock_r, const char **error_r) +{ + return file_wait_lock(fd, path, lock_type, set, 0, lock_r, error_r); +} + +static const char * +file_lock_find_fcntl(int lock_fd, int lock_type) +{ + struct flock fl; + + i_zero(&fl); + fl.l_type = lock_type; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + + if (fcntl(lock_fd, F_GETLK, &fl) < 0 || + fl.l_type == F_UNLCK || fl.l_pid == -1 || fl.l_pid == 0) + return ""; + return t_strdup_printf(" (%s lock held by pid %ld)", + fl.l_type == F_RDLCK ? "READ" : "WRITE", (long)fl.l_pid); +} + +static const char * +file_lock_find_proc_locks(int lock_fd ATTR_UNUSED) +{ + /* do anything except Linux support this? don't bother trying it for + OSes we don't know about. */ +#ifdef __linux__ + static bool have_proc_locks = TRUE; + struct stat st; + char node_buf[MAX_INT_STRLEN * 3 + 2]; + struct istream *input; + const char *line, *lock_type = ""; + pid_t pid = 0; + int fd; + + if (!have_proc_locks) + return NULL; + + if (fstat(lock_fd, &st) < 0) + return ""; + i_snprintf(node_buf, sizeof(node_buf), "%02x:%02x:%llu", + major(st.st_dev), minor(st.st_dev), + (unsigned long long)st.st_ino); + fd = open("/proc/locks", O_RDONLY); + if (fd == -1) { + have_proc_locks = FALSE; + return ""; + } + input = i_stream_create_fd_autoclose(&fd, 512); + while (pid == 0 && (line = i_stream_read_next_line(input)) != NULL) T_BEGIN { + const char *const *args = t_strsplit_spaces(line, " "); + + /* number: FLOCK/POSIX ADVISORY READ/WRITE pid + major:minor:inode region-start region-end */ + if (str_array_length(args) < 8) { + ; /* don't continue from within a T_BEGIN {...} T_END */ + } else if (strcmp(args[5], node_buf) == 0) { + lock_type = strcmp(args[3], "READ") == 0 ? + "READ" : "WRITE"; + if (str_to_pid(args[4], &pid) < 0) + pid = 0; + } + } T_END; + i_stream_destroy(&input); + if (pid == 0) { + /* not found */ + return ""; + } + if (pid == getpid()) + return " (BUG: lock is held by our own process)"; + return t_strdup_printf(" (%s lock held by pid %ld)", lock_type, (long)pid); +#else + return ""; +#endif +} + +const char *file_lock_find(int lock_fd, enum file_lock_method lock_method, + int lock_type) +{ + const char *ret; + + if (lock_method == FILE_LOCK_METHOD_FCNTL) { + ret = file_lock_find_fcntl(lock_fd, lock_type); + if (ret[0] != '\0') + return ret; + } + return file_lock_find_proc_locks(lock_fd); +} + +static bool err_is_lock_timeout(time_t started, unsigned int timeout_secs) +{ + /* if EINTR took at least timeout_secs-1 number of seconds, + assume it was the alarm. otherwise log EINTR failure. + (We most likely don't want to retry EINTR since a signal + means somebody wants us to stop blocking). */ + return errno == EINTR && + (unsigned long)(time(NULL) - started + 1) >= timeout_secs; +} + +static int file_lock_do(int fd, const char *path, int lock_type, + const struct file_lock_settings *set, + unsigned int timeout_secs, const char **error_r) +{ + const char *lock_type_str; + time_t started = time(NULL); + int ret; + + i_assert(fd != -1); + + if (timeout_secs != 0) { + alarm(timeout_secs); + file_lock_wait_start(); + } + + lock_type_str = lock_type == F_UNLCK ? "unlock" : + (lock_type == F_RDLCK ? "read-lock" : "write-lock"); + + switch (set->lock_method) { + case FILE_LOCK_METHOD_FCNTL: { +#ifndef HAVE_FCNTL + *error_r = t_strdup_printf( + "Can't lock file %s: fcntl() locks not supported", path); + return -1; +#else + struct flock fl; + + fl.l_type = lock_type; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + + ret = fcntl(fd, timeout_secs != 0 ? F_SETLKW : F_SETLK, &fl); + if (timeout_secs != 0) { + alarm(0); + file_lock_wait_end(path); + } + + if (ret == 0) + break; + + if (timeout_secs == 0 && + (errno == EACCES || errno == EAGAIN)) { + /* locked by another process */ + *error_r = t_strdup_printf( + "fcntl(%s, %s, F_SETLK) locking failed: %m " + "(File is already locked)", path, lock_type_str); + return 0; + } + + if (err_is_lock_timeout(started, timeout_secs)) { + errno = EAGAIN; + *error_r = t_strdup_printf( + "fcntl(%s, %s, F_SETLKW) locking failed: " + "Timed out after %u seconds%s", + path, lock_type_str, timeout_secs, + file_lock_find(fd, set->lock_method, + lock_type)); + return 0; + } + *error_r = t_strdup_printf("fcntl(%s, %s, %s) locking failed: %m", + path, lock_type_str, timeout_secs == 0 ? "F_SETLK" : "F_SETLKW"); + if (errno == EDEADLK && !set->allow_deadlock) { + i_panic("%s%s", *error_r, + file_lock_find(fd, set->lock_method, + lock_type)); + } + return -1; +#endif + } + case FILE_LOCK_METHOD_FLOCK: { +#ifndef HAVE_FLOCK + *error_r = t_strdup_printf( + "Can't lock file %s: flock() not supported", path); + return -1; +#else + int operation = timeout_secs != 0 ? 0 : LOCK_NB; + + switch (lock_type) { + case F_RDLCK: + operation |= LOCK_SH; + break; + case F_WRLCK: + operation |= LOCK_EX; + break; + case F_UNLCK: + operation |= LOCK_UN; + break; + } + + ret = flock(fd, operation); + if (timeout_secs != 0) { + alarm(0); + file_lock_wait_end(path); + } + + if (ret == 0) + break; + + if (timeout_secs == 0 && errno == EWOULDBLOCK) { + /* locked by another process */ + *error_r = t_strdup_printf( + "flock(%s, %s) failed: %m " + "(File is already locked)", path, lock_type_str); + return 0; + } + if (err_is_lock_timeout(started, timeout_secs)) { + errno = EAGAIN; + *error_r = t_strdup_printf("flock(%s, %s) failed: " + "Timed out after %u seconds%s", + path, lock_type_str, timeout_secs, + file_lock_find(fd, set->lock_method, + lock_type)); + return 0; + } + *error_r = t_strdup_printf("flock(%s, %s) failed: %m", + path, lock_type_str); + if (errno == EDEADLK && !set->allow_deadlock) { + i_panic("%s%s", *error_r, + file_lock_find(fd, set->lock_method, + lock_type)); + } + return -1; +#endif + } + case FILE_LOCK_METHOD_DOTLOCK: + /* we shouldn't get here */ + i_unreached(); + } + + return 1; +} + +int file_wait_lock(int fd, const char *path, int lock_type, + const struct file_lock_settings *set, + unsigned int timeout_secs, + struct file_lock **lock_r, const char **error_r) +{ + struct file_lock *lock; + int ret; + + ret = file_lock_do(fd, path, lock_type, set, timeout_secs, error_r); + if (ret <= 0) + return ret; + + lock = i_new(struct file_lock, 1); + lock->set = *set; + lock->fd = fd; + lock->path = i_strdup(path); + lock->lock_type = lock_type; + i_gettimeofday(&lock->locked_time); + *lock_r = lock; + return 1; +} + +int file_lock_try_update(struct file_lock *lock, int lock_type) +{ + const char *error; + int ret; + + ret = file_lock_do(lock->fd, lock->path, lock_type, &lock->set, 0, + &error); + if (ret <= 0) + return ret; + file_lock_log_warning_if_slow(lock); + lock->lock_type = lock_type; + return 1; +} + +void file_lock_set_unlink_on_free(struct file_lock *lock, bool set) +{ + lock->set.unlink_on_free = set; +} + +void file_lock_set_close_on_free(struct file_lock *lock, bool set) +{ + lock->set.close_on_free = set; +} + +struct file_lock *file_lock_from_dotlock(struct dotlock **dotlock) +{ + struct file_lock *lock; + + lock = i_new(struct file_lock, 1); + lock->set.lock_method = FILE_LOCK_METHOD_DOTLOCK; + lock->fd = -1; + lock->path = i_strdup(file_dotlock_get_lock_path(*dotlock)); + lock->lock_type = F_WRLCK; + i_gettimeofday(&lock->locked_time); + lock->dotlock = *dotlock; + + *dotlock = NULL; + return lock; +} + +static void file_unlock_real(struct file_lock *lock) +{ + const char *error; + + if (file_lock_do(lock->fd, lock->path, F_UNLCK, &lock->set, 0, + &error) == 0) { + /* this shouldn't happen */ + i_error("file_unlock(%s) failed: %m", lock->path); + } +} + +void file_unlock(struct file_lock **_lock) +{ + struct file_lock *lock = *_lock; + + *_lock = NULL; + + /* unlocking is unnecessary when the file is unlinked. or alternatively + the unlink() must be done before unlocking, because otherwise it + could be deleting the new lock. */ + i_assert(!lock->set.unlink_on_free); + + if (lock->dotlock == NULL) + file_unlock_real(lock); + file_lock_free(&lock); +} + +static void file_try_unlink_locked(struct file_lock *lock) +{ + struct file_lock *temp_lock = NULL; + struct file_lock_settings temp_set = lock->set; + struct stat st1, st2; + const char *error; + int ret; + + temp_set.close_on_free = FALSE; + temp_set.unlink_on_free = FALSE; + + file_unlock_real(lock); + ret = file_try_lock(lock->fd, lock->path, F_WRLCK, &temp_set, + &temp_lock, &error); + if (ret < 0) { + i_error("file_lock_free(): Unexpectedly failed to retry locking %s: %s", + lock->path, error); + } else if (ret == 0) { + /* already locked by someone else */ + } else if (fstat(lock->fd, &st1) < 0) { + /* not expected to happen */ + i_error("file_lock_free(): fstat(%s) failed: %m", lock->path); + } else if (stat(lock->path, &st2) < 0) { + if (errno != ENOENT) + i_error("file_lock_free(): stat(%s) failed: %m", lock->path); + } else if (st1.st_ino != st2.st_ino || + !CMP_DEV_T(st1.st_dev, st2.st_dev)) { + /* lock file was recreated already - don't delete it */ + } else { + /* nobody was waiting on the lock - unlink it */ + i_unlink(lock->path); + } + file_lock_free(&temp_lock); +} + +void file_lock_free(struct file_lock **_lock) +{ + struct file_lock *lock = *_lock; + + if (lock == NULL) + return; + + *_lock = NULL; + + if (lock->dotlock != NULL) + file_dotlock_delete(&lock->dotlock); + if (lock->set.unlink_on_free) + file_try_unlink_locked(lock); + if (lock->set.close_on_free) + i_close_fd(&lock->fd); + + file_lock_log_warning_if_slow(lock); + i_free(lock->path); + i_free(lock); +} + +const char *file_lock_get_path(struct file_lock *lock) +{ + return lock->path; +} + +void file_lock_set_path(struct file_lock *lock, const char *path) +{ + if (path != lock->path) { + i_free(lock->path); + lock->path = i_strdup(path); + } +} + +void file_lock_wait_start(void) +{ + i_assert(lock_wait_start.tv_sec == 0); + + i_gettimeofday(&lock_wait_start); +} + +static void file_lock_wait_init_warning(void) +{ + const char *value; + + i_assert(file_lock_slow_warning_usecs == -1); + + value = getenv("FILE_LOCK_SLOW_WARNING_MSECS"); + if (value == NULL) + file_lock_slow_warning_usecs = LLONG_MAX; + else if (str_to_llong(value, &file_lock_slow_warning_usecs) == 0 && + file_lock_slow_warning_usecs > 0) { + file_lock_slow_warning_usecs *= 1000; + } else { + i_error("FILE_LOCK_SLOW_WARNING_MSECS: " + "Invalid value '%s' - ignoring", value); + file_lock_slow_warning_usecs = LLONG_MAX; + } +} + +static void file_lock_log_warning_if_slow(struct file_lock *lock) +{ + if (file_lock_slow_warning_usecs < 0) + file_lock_wait_init_warning(); + if (file_lock_slow_warning_usecs == LLONG_MAX) { + /* slowness checking is disabled */ + return; + } + if (lock->lock_type != F_WRLCK) { + /* some shared locks can legitimately be kept for a long time. + don't warn about them. */ + return; + } + + struct timeval now; + i_gettimeofday(&now); + + int diff = timeval_diff_msecs(&now, &lock->locked_time); + if (diff > file_lock_slow_warning_usecs/1000) { + i_warning("Lock %s kept for %d.%03d secs", lock->path, + diff / 1000, diff % 1000); + } +} + +void file_lock_wait_end(const char *lock_name) +{ + struct timeval now; + + i_assert(lock_wait_start.tv_sec != 0); + + i_gettimeofday(&now); + long long diff = timeval_diff_usecs(&now, &lock_wait_start); + if (diff < 0) { + /* time moved backwards */ + diff = 0; + } + if (diff > file_lock_slow_warning_usecs) { + if (file_lock_slow_warning_usecs < 0) + file_lock_wait_init_warning(); + if (diff > file_lock_slow_warning_usecs) { + int diff_msecs = (diff + 999) / 1000; + i_warning("Locking %s took %d.%03d secs", lock_name, + diff_msecs / 1000, diff_msecs % 1000); + } + } + file_lock_wait_usecs += diff; + lock_wait_start.tv_sec = 0; +} + +uint64_t file_lock_wait_get_total_usecs(void) +{ + return file_lock_wait_usecs; +} |