summaryrefslogtreecommitdiffstats
path: root/src/lib/file-dotlock.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/file-dotlock.c')
-rw-r--r--src/lib/file-dotlock.c925
1 files changed, 925 insertions, 0 deletions
diff --git a/src/lib/file-dotlock.c b/src/lib/file-dotlock.c
new file mode 100644
index 0000000..ad14ef0
--- /dev/null
+++ b/src/lib/file-dotlock.c
@@ -0,0 +1,925 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "hex-binary.h"
+#include "hostpid.h"
+#include "file-lock.h"
+#include "eacces-error.h"
+#include "write-full.h"
+#include "safe-mkstemp.h"
+#include "nfs-workarounds.h"
+#include "file-dotlock.h"
+#include "sleep.h"
+
+#include <stdio.h>
+#include <signal.h>
+#include <time.h>
+#include <utime.h>
+#include <sys/stat.h>
+
+#define DEFAULT_LOCK_SUFFIX ".lock"
+
+/* 0.1 .. 0.2msec */
+#define LOCK_RANDOM_USLEEP_TIME (100000 + (unsigned int)i_rand() % 100000)
+/* Maximum 3 second wait between dotlock checks */
+#define LOCK_MAX_WAIT_USECS (1000000 * 3)
+
+/* If the dotlock is newer than this, don't verify that the PID it contains
+ is valid (since it most likely is). */
+#define STALE_PID_CHECK_SECS 2
+
+/* Maximum difference between current time and create file's ctime before
+ logging a warning. Should be less than a second in normal operation. */
+#define MAX_TIME_DIFF 30
+/* NFS may return a cached mtime in stat(). A later non-cached stat() may
+ return a slightly different mtime. Allow the difference to be this much
+ and still consider it to be the same mtime. */
+#define FILE_DOTLOCK_MAX_STAT_MTIME_DIFF 1
+
+struct dotlock {
+ struct dotlock_settings settings;
+
+ dev_t dev;
+ ino_t ino;
+ time_t mtime;
+
+ char *path;
+ char *lock_path;
+ int fd;
+
+ time_t lock_time;
+};
+
+struct file_change_info {
+ dev_t dev;
+ ino_t ino;
+ off_t size;
+ time_t ctime, mtime;
+};
+
+struct lock_info {
+ const struct dotlock_settings *set;
+ const char *path, *lock_path, *temp_path;
+ int fd;
+
+ struct file_change_info lock_info;
+ struct file_change_info file_info;
+
+ time_t last_pid_check;
+ time_t last_change;
+ unsigned int wait_usecs;
+
+ bool have_pid:1;
+ bool pid_read:1;
+ bool use_io_notify:1;
+ bool lock_stated:1;
+};
+
+static struct dotlock *
+file_dotlock_alloc(const struct dotlock_settings *settings, const char *path)
+{
+ struct dotlock *dotlock;
+
+ dotlock = i_new(struct dotlock, 1);
+ dotlock->settings = *settings;
+ if (dotlock->settings.lock_suffix == NULL)
+ dotlock->settings.lock_suffix = DEFAULT_LOCK_SUFFIX;
+ dotlock->path = i_strdup(path);
+ dotlock->fd = -1;
+
+ return dotlock;
+}
+
+static pid_t read_local_pid(const char *lock_path)
+{
+ char buf[512], *host;
+ int fd;
+ ssize_t ret;
+ pid_t pid;
+
+ fd = open(lock_path, O_RDONLY);
+ if (fd == -1)
+ return -1; /* ignore the actual error */
+
+ /* read line */
+ ret = read(fd, buf, sizeof(buf)-1);
+ i_close_fd(&fd);
+ if (ret <= 0)
+ return -1;
+
+ /* fix the string */
+ if (buf[ret-1] == '\n')
+ ret--;
+ buf[ret] = '\0';
+
+ /* it should contain pid:host */
+ host = strchr(buf, ':');
+ if (host == NULL)
+ return -1;
+ *host++ = '\0';
+
+ /* host must be ours */
+ if (strcmp(host, my_hostname) != 0)
+ return -1;
+
+ if (str_to_pid(buf, &pid) < 0)
+ return -1;
+ if (pid <= 0)
+ return -1;
+ return pid;
+}
+
+static bool
+update_change_info(const struct stat *st, struct file_change_info *change,
+ time_t *last_change_r, time_t now, bool check_ctime)
+{
+ /* ctime is checked only if we're not doing NFS attribute cache
+ flushes. it changes them. */
+ if (change->ino != st->st_ino || !CMP_DEV_T(change->dev, st->st_dev) ||
+ (change->ctime != st->st_ctime && check_ctime) ||
+ change->mtime != st->st_mtime || change->size != st->st_size) {
+ time_t change_time = now;
+
+ if (change->ctime == 0) {
+ /* First check, set last_change to file's change time.
+ Use mtime instead if it's higher, but only if it's
+ not higher than current time, because the mtime
+ can also be used for keeping metadata. */
+ change_time = st->st_mtime <= now &&
+ (st->st_mtime > st->st_ctime || !check_ctime) ?
+ st->st_mtime : st->st_ctime;
+ }
+ if (*last_change_r < change_time)
+ *last_change_r = change_time;
+ change->ino = st->st_ino;
+ change->dev = st->st_dev;
+ change->ctime = st->st_ctime;
+ change->mtime = st->st_mtime;
+ change->size = st->st_size;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static int update_lock_info(time_t now, struct lock_info *lock_info,
+ bool *changed_r)
+{
+ struct stat st;
+
+ /* don't waste time flushing attribute cache the first time we're here.
+ if it's stale we'll get back here soon. */
+ if (lock_info->set->nfs_flush && lock_info->lock_stated) {
+ nfs_flush_file_handle_cache(lock_info->lock_path);
+ nfs_flush_attr_cache_unlocked(lock_info->lock_path);
+ }
+
+ lock_info->lock_stated = TRUE;
+ if (nfs_safe_lstat(lock_info->lock_path, &st) < 0) {
+ if (errno != ENOENT) {
+ i_error("lstat(%s) failed: %m", lock_info->lock_path);
+ return -1;
+ }
+ return 1;
+ }
+
+ *changed_r = update_change_info(&st, &lock_info->lock_info,
+ &lock_info->last_change, now,
+ !lock_info->set->nfs_flush);
+ return 0;
+}
+
+static int dotlock_override(struct lock_info *lock_info)
+{
+ if (i_unlink_if_exists(lock_info->lock_path) < 0)
+ return -1;
+
+ /* make sure we sleep for a while after overriding the lock file.
+ otherwise another process might try to override it at the same time
+ and unlink our newly created dotlock. */
+ if (lock_info->use_io_notify)
+ i_sleep_usecs(LOCK_RANDOM_USLEEP_TIME);
+ return 0;
+}
+
+static int check_lock(time_t now, struct lock_info *lock_info)
+{
+ time_t stale_timeout = lock_info->set->stale_timeout;
+ pid_t pid = -1;
+ bool changed;
+ int ret;
+
+ if ((ret = update_lock_info(now, lock_info, &changed)) != 0)
+ return ret;
+ if (changed || !lock_info->pid_read) {
+ /* either our first check or someone else got the lock file.
+ if the dotlock was created only a couple of seconds ago,
+ don't bother to read its PID. */
+ if (lock_info->lock_info.mtime >= now - STALE_PID_CHECK_SECS)
+ lock_info->pid_read = FALSE;
+ else {
+ pid = read_local_pid(lock_info->lock_path);
+ lock_info->pid_read = TRUE;
+ }
+ lock_info->have_pid = pid != -1;
+ } else if (!lock_info->have_pid) {
+ /* no pid checking */
+ } else {
+ if (lock_info->last_pid_check == now) {
+ /* we just checked the pid */
+ return 0;
+ }
+
+ /* re-read the pid. even if all times and inodes are the same,
+ the PID in the file might have changed if lock files were
+ rapidly being recreated. */
+ pid = read_local_pid(lock_info->lock_path);
+ lock_info->have_pid = pid != -1;
+ }
+
+ if (lock_info->have_pid) {
+ /* we've local PID. Check if it exists. */
+ if (kill(pid, 0) == 0 || errno != ESRCH) {
+ if (pid != getpid()) {
+ /* process exists, don't override */
+ return 0;
+ }
+ /* it's us. either we're locking it again, or it's a
+ stale lock file with same pid than us. either way,
+ recreate it.. */
+ }
+
+ /* doesn't exist - now check again if the dotlock was just
+ deleted or replaced */
+ if ((ret = update_lock_info(now, lock_info, &changed)) != 0)
+ return ret;
+
+ if (!changed) {
+ /* still there, go ahead and override it */
+ return dotlock_override(lock_info);
+ }
+ return 1;
+ }
+
+ if (stale_timeout == 0) {
+ /* no change checking */
+ return 0;
+ }
+
+ if (now > lock_info->last_change + stale_timeout) {
+ struct stat st;
+
+ /* possibly stale lock file. check also the timestamp of the
+ file we're protecting. */
+ if (lock_info->set->nfs_flush) {
+ nfs_flush_file_handle_cache(lock_info->path);
+ nfs_flush_attr_cache_maybe_locked(lock_info->path);
+ }
+ if (nfs_safe_stat(lock_info->path, &st) < 0) {
+ if (errno == ENOENT) {
+ /* file doesn't exist. treat it as if
+ it hasn't changed */
+ } else {
+ i_error("stat(%s) failed: %m", lock_info->path);
+ return -1;
+ }
+ } else {
+ (void)update_change_info(&st, &lock_info->file_info,
+ &lock_info->last_change, now,
+ !lock_info->set->nfs_flush);
+ }
+ }
+
+ if (now > lock_info->last_change + stale_timeout) {
+ /* no changes for a while, assume stale lock */
+ return dotlock_override(lock_info);
+ }
+
+ return 0;
+}
+
+static int file_write_pid(int fd, const char *path, bool nfs_flush)
+{
+ const char *str;
+
+ /* write our pid and host, if possible */
+ str = t_strdup_printf("%s:%s", my_pid, my_hostname);
+ if (write_full(fd, str, strlen(str)) < 0 ||
+ (nfs_flush && fdatasync(fd) < 0)) {
+ /* failed, leave it empty then */
+ if (ftruncate(fd, 0) < 0) {
+ i_error("ftruncate(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int try_create_lock_hardlink(struct lock_info *lock_info, bool write_pid,
+ string_t *tmp_path, time_t now)
+{
+ const char *temp_prefix = lock_info->set->temp_prefix;
+ const char *p;
+ mode_t old_mask;
+ struct stat st;
+
+ if (lock_info->temp_path == NULL) {
+ /* we'll need our temp file first. */
+ i_assert(lock_info->fd == -1);
+
+ p = strrchr(lock_info->lock_path, '/');
+
+ str_truncate(tmp_path, 0);
+ if (temp_prefix != NULL) {
+ if (*temp_prefix != '/' && p != NULL) {
+ /* add directory */
+ str_append_data(tmp_path, lock_info->lock_path,
+ p - lock_info->lock_path);
+ str_append_c(tmp_path, '/');
+ }
+ str_append(tmp_path, temp_prefix);
+ } else {
+ if (p != NULL) {
+ /* add directory */
+ str_append_data(tmp_path, lock_info->lock_path,
+ p - lock_info->lock_path);
+ str_append_c(tmp_path, '/');
+ }
+ str_printfa(tmp_path, ".temp.%s.%s.",
+ my_hostname, my_pid);
+ }
+
+ old_mask = umask(0666);
+ lock_info->fd = safe_mkstemp(tmp_path, 0666 ^ old_mask,
+ (uid_t)-1, (gid_t)-1);
+ umask(old_mask);
+ if (lock_info->fd == -1)
+ return -1;
+
+ if (write_pid) {
+ if (file_write_pid(lock_info->fd,
+ str_c(tmp_path),
+ lock_info->set->nfs_flush) < 0) {
+ i_close_fd(&lock_info->fd);
+ return -1;
+ }
+ }
+
+ lock_info->temp_path = str_c(tmp_path);
+ } else if (fstat(lock_info->fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", lock_info->temp_path);
+ return -1;
+ } else if (st.st_ctime < now) {
+ /* we've been waiting for a while.
+ refresh the file's timestamp. */
+ if (utime(lock_info->temp_path, NULL) < 0)
+ i_error("utime(%s) failed: %m", lock_info->temp_path);
+ }
+
+ if (nfs_safe_link(lock_info->temp_path,
+ lock_info->lock_path, TRUE) < 0) {
+ if (errno == EEXIST)
+ return 0;
+
+ if (errno != EACCES) {
+ i_error("link(%s, %s) failed: %m",
+ lock_info->temp_path, lock_info->lock_path);
+ }
+ return -1;
+ }
+
+ if (i_unlink(lock_info->temp_path) < 0) {
+ /* non-fatal, continue */
+ }
+ lock_info->temp_path = NULL;
+ return 1;
+}
+
+static int try_create_lock_excl(struct lock_info *lock_info, bool write_pid)
+{
+ int fd;
+
+ fd = open(lock_info->lock_path, O_RDWR | O_EXCL | O_CREAT, 0666);
+ if (fd == -1) {
+ if (errno == EEXIST)
+ return 0;
+
+ if (errno != ENOENT && errno != EACCES)
+ i_error("open(%s) failed: %m", lock_info->lock_path);
+ return -1;
+ }
+
+ if (write_pid) {
+ if (file_write_pid(fd, lock_info->lock_path,
+ lock_info->set->nfs_flush) < 0) {
+ i_close_fd(&fd);
+ return -1;
+ }
+ }
+
+ lock_info->fd = fd;
+ return 1;
+}
+
+static void dotlock_wait_end(struct ioloop *ioloop)
+{
+ io_loop_stop(ioloop);
+}
+
+static void dotlock_wait(struct lock_info *lock_info)
+{
+ struct ioloop *ioloop;
+ struct io *io;
+ struct timeout *to;
+
+ if (!lock_info->use_io_notify) {
+ i_sleep_usecs(lock_info->wait_usecs);
+ return;
+ }
+
+ ioloop = io_loop_create();
+ switch (io_add_notify(lock_info->lock_path, dotlock_wait_end,
+ ioloop, &io)) {
+ case IO_NOTIFY_ADDED:
+ break;
+ case IO_NOTIFY_NOTFOUND:
+ /* the lock file doesn't exist anymore, don't sleep */
+ io_loop_destroy(&ioloop);
+ return;
+ case IO_NOTIFY_NOSUPPORT:
+ /* listening for files not supported */
+ io_loop_destroy(&ioloop);
+ lock_info->use_io_notify = FALSE;
+ i_sleep_usecs(LOCK_RANDOM_USLEEP_TIME);
+ return;
+ }
+ /* timeout after a random time even when using notify, since it
+ doesn't work reliably with e.g. NFS. */
+ to = timeout_add(lock_info->wait_usecs/1000,
+ dotlock_wait_end, ioloop);
+ io_loop_run(ioloop);
+ io_remove(&io);
+ timeout_remove(&to);
+ io_loop_destroy(&ioloop);
+}
+
+static int
+dotlock_create(struct dotlock *dotlock, enum dotlock_create_flags flags,
+ bool write_pid, const char **lock_path_r)
+{
+ const struct dotlock_settings *set = &dotlock->settings;
+ const char *lock_path;
+ struct lock_info lock_info;
+ struct stat st;
+ unsigned int stale_notify_threshold;
+ unsigned int change_secs, wait_left;
+ time_t now, max_wait_time, last_notify;
+ time_t prev_last_change = 0, prev_wait_update = 0;
+ string_t *tmp_path;
+ int ret;
+ bool do_wait;
+
+ now = time(NULL);
+
+ lock_path = *lock_path_r =
+ t_strconcat(dotlock->path, set->lock_suffix, NULL);
+ stale_notify_threshold = set->stale_timeout / 2;
+ max_wait_time = (flags & DOTLOCK_CREATE_FLAG_NONBLOCK) != 0 ? 0 :
+ now + set->timeout;
+ tmp_path = t_str_new(256);
+
+ i_zero(&lock_info);
+ lock_info.path = dotlock->path;
+ lock_info.set = set;
+ lock_info.lock_path = lock_path;
+ lock_info.fd = -1;
+ lock_info.use_io_notify = set->use_io_notify;
+
+ last_notify = 0; do_wait = FALSE;
+
+ file_lock_wait_start();
+ do {
+ if (do_wait) {
+ if (prev_last_change != lock_info.last_change) {
+ /* dotlock changed since last check,
+ reset the wait time */
+ lock_info.wait_usecs = LOCK_RANDOM_USLEEP_TIME;
+ prev_last_change = lock_info.last_change;
+ prev_wait_update = now;
+ } else if (prev_wait_update != now &&
+ lock_info.wait_usecs < LOCK_MAX_WAIT_USECS) {
+ /* we've been waiting for a while now, increase
+ the wait time to avoid wasting CPU */
+ prev_wait_update = now;
+ lock_info.wait_usecs += lock_info.wait_usecs/2;
+ }
+ dotlock_wait(&lock_info);
+ now = time(NULL);
+ }
+
+ ret = check_lock(now, &lock_info);
+ if (ret < 0)
+ break;
+
+ if (ret == 1) {
+ if ((flags & DOTLOCK_CREATE_FLAG_CHECKONLY) != 0)
+ break;
+
+ ret = set->use_excl_lock ?
+ try_create_lock_excl(&lock_info, write_pid) :
+ try_create_lock_hardlink(&lock_info, write_pid,
+ tmp_path, now);
+ if (ret != 0) {
+ /* if we succeeded, get the current time once
+ more in case disk I/O usage was really high
+ and it took a long time to create the lock */
+ now = time(NULL);
+ break;
+ }
+ }
+
+ if (last_notify != now && set->callback != NULL &&
+ now < max_wait_time) {
+ last_notify = now;
+ change_secs = now - lock_info.last_change;
+ wait_left = max_wait_time - now;
+
+ if (change_secs >= stale_notify_threshold &&
+ change_secs <= wait_left) {
+ unsigned int secs_left =
+ set->stale_timeout < change_secs ?
+ 0 : set->stale_timeout - change_secs;
+ if (!set->callback(secs_left, TRUE,
+ set->context)) {
+ /* we don't want to override */
+ lock_info.last_change = now;
+ }
+ } else if (wait_left > 0) {
+ (void)set->callback(wait_left, FALSE,
+ set->context);
+ }
+ }
+
+ do_wait = TRUE;
+ now = time(NULL);
+ } while (now < max_wait_time);
+ file_lock_wait_end(dotlock->path);
+
+ if (ret > 0) {
+ i_assert(lock_info.fd != -1);
+ if (fstat(lock_info.fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", lock_path);
+ ret = -1;
+ } else {
+ /* successful dotlock creation */
+ dotlock->dev = st.st_dev;
+ dotlock->ino = st.st_ino;
+
+ dotlock->fd = lock_info.fd;
+ dotlock->lock_time = now;
+ lock_info.fd = -1;
+
+ if (st.st_ctime + MAX_TIME_DIFF < now ||
+ st.st_ctime - MAX_TIME_DIFF > now) {
+ i_warning("Created dotlock file's timestamp is "
+ "different than current time "
+ "(%s vs %s): %s", dec2str(st.st_ctime),
+ dec2str(now), dotlock->path);
+ }
+ }
+ }
+
+ if (lock_info.fd != -1) {
+ int old_errno = errno;
+
+ if (close(lock_info.fd) < 0)
+ i_error("close(%s) failed: %m", lock_path);
+ errno = old_errno;
+ }
+ if (lock_info.temp_path != NULL)
+ i_unlink(lock_info.temp_path);
+
+ if (ret == 0)
+ errno = EAGAIN;
+ return ret;
+}
+
+static void file_dotlock_free(struct dotlock **_dotlock)
+{
+ struct dotlock *dotlock = *_dotlock;
+ int old_errno;
+
+ *_dotlock = NULL;
+
+ if (dotlock->fd != -1) {
+ old_errno = errno;
+ if (close(dotlock->fd) < 0)
+ i_error("close(%s) failed: %m", dotlock->path);
+ dotlock->fd = -1;
+ errno = old_errno;
+ }
+
+ i_free(dotlock->path);
+ i_free(dotlock->lock_path);
+ i_free(dotlock);
+}
+
+static int file_dotlock_create_real(struct dotlock *dotlock,
+ enum dotlock_create_flags flags)
+{
+ const char *lock_path;
+ struct stat st;
+ int fd, ret;
+
+ ret = dotlock_create(dotlock, flags, TRUE, &lock_path);
+ if (ret <= 0 || (flags & DOTLOCK_CREATE_FLAG_CHECKONLY) != 0)
+ return ret;
+
+ fd = dotlock->fd;
+ dotlock->fd = -1;
+
+ if (close(fd) < 0) {
+ i_error("close(%s) failed: %m", lock_path);
+ return -1;
+ }
+
+ /* With NFS the writes may have been flushed only when closing the
+ file. Get the mtime again after that to avoid "dotlock was modified"
+ errors. */
+ if (lstat(lock_path, &st) < 0) {
+ if (errno != ENOENT)
+ i_error("stat(%s) failed: %m", lock_path);
+ else {
+ i_error("dotlock %s was immediately deleted under us",
+ lock_path);
+ }
+ return -1;
+ }
+ /* extra sanity check won't hurt.. */
+ if (st.st_dev != dotlock->dev || st.st_ino != dotlock->ino) {
+ errno = ENOENT;
+ i_error("dotlock %s was immediately recreated under us",
+ lock_path);
+ return -1;
+ }
+ dotlock->mtime = st.st_mtime;
+ return 1;
+}
+
+int file_dotlock_create(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ struct dotlock **dotlock_r)
+{
+ struct dotlock *dotlock;
+ int ret;
+
+ dotlock = file_dotlock_alloc(set, path);
+ T_BEGIN {
+ ret = file_dotlock_create_real(dotlock, flags);
+ } T_END;
+ if (ret <= 0 || (flags & DOTLOCK_CREATE_FLAG_CHECKONLY) != 0)
+ file_dotlock_free(&dotlock);
+
+ *dotlock_r = dotlock;
+ return ret;
+}
+
+static void dotlock_replaced_warning(struct dotlock *dotlock, bool deleted)
+{
+ const char *lock_path;
+ time_t now = time(NULL);
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (dotlock->mtime == dotlock->lock_time) {
+ i_warning("Our dotlock file %s was %s "
+ "(locked %d secs ago, touched %d secs ago)",
+ lock_path, deleted ? "deleted" : "overridden",
+ (int)(now - dotlock->lock_time),
+ (int)(now - dotlock->mtime));
+ } else {
+ i_warning("Our dotlock file %s was %s "
+ "(kept it %d secs)", lock_path,
+ deleted ? "deleted" : "overridden",
+ (int)(now - dotlock->lock_time));
+ }
+}
+
+static bool file_dotlock_has_mtime_changed(time_t t1, time_t t2)
+{
+ time_t diff;
+
+ if (t1 == t2)
+ return FALSE;
+
+ /* with NFS t1 may have been looked up from local cache.
+ allow it to be a little bit different. */
+ diff = t1 > t2 ? t1-t2 : t2-t1;
+ return diff > FILE_DOTLOCK_MAX_STAT_MTIME_DIFF;
+}
+
+int file_dotlock_delete(struct dotlock **dotlock_p)
+{
+ struct dotlock *dotlock;
+ const char *lock_path;
+ struct stat st;
+ int ret;
+
+ dotlock = *dotlock_p;
+ *dotlock_p = NULL;
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (nfs_safe_lstat(lock_path, &st) < 0) {
+ if (errno == ENOENT) {
+ dotlock_replaced_warning(dotlock, TRUE);
+ file_dotlock_free(&dotlock);
+ return 0;
+ }
+
+ i_error("lstat(%s) failed: %m", lock_path);
+ file_dotlock_free(&dotlock);
+ return -1;
+ }
+
+ if (dotlock->ino != st.st_ino ||
+ !CMP_DEV_T(dotlock->dev, st.st_dev)) {
+ dotlock_replaced_warning(dotlock, FALSE);
+ errno = EEXIST;
+ file_dotlock_free(&dotlock);
+ return 0;
+ }
+
+ if (file_dotlock_has_mtime_changed(dotlock->mtime, st.st_mtime) &&
+ dotlock->fd == -1) {
+ i_warning("Our dotlock file %s was modified (%s vs %s), "
+ "assuming it wasn't overridden (kept it %d secs)",
+ lock_path,
+ dec2str(dotlock->mtime), dec2str(st.st_mtime),
+ (int)(time(NULL) - dotlock->lock_time));
+ }
+
+ if ((ret = i_unlink_if_exists(lock_path)) == 0)
+ dotlock_replaced_warning(dotlock, TRUE);
+ file_dotlock_free(&dotlock);
+ return ret;
+}
+
+int file_dotlock_open(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ struct dotlock **dotlock_r)
+{
+ struct dotlock *dotlock;
+ int ret;
+
+ dotlock = file_dotlock_alloc(set, path);
+ T_BEGIN {
+ const char *lock_path;
+
+ ret = dotlock_create(dotlock, flags, FALSE, &lock_path);
+ } T_END;
+
+ if (ret <= 0) {
+ file_dotlock_free(&dotlock);
+ *dotlock_r = NULL;
+ return -1;
+ }
+
+ *dotlock_r = dotlock;
+ return dotlock->fd;
+}
+
+static int ATTR_NULL(7)
+file_dotlock_open_mode_full(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, uid_t uid, gid_t gid,
+ const char *gid_origin, struct dotlock **dotlock_r)
+{
+ struct dotlock *dotlock;
+ mode_t old_mask;
+ int fd;
+
+ old_mask = umask(0666 ^ mode);
+ fd = file_dotlock_open(set, path, flags, &dotlock);
+ umask(old_mask);
+
+ if (fd != -1 && (uid != (uid_t)-1 || gid != (gid_t)-1)) {
+ if (fchown(fd, uid, gid) < 0) {
+ if (errno == EPERM && uid == (uid_t)-1) {
+ i_error("%s", eperm_error_get_chgrp("fchown",
+ file_dotlock_get_lock_path(dotlock),
+ gid, gid_origin));
+ } else {
+ i_error("fchown(%s, %ld, %ld) failed: %m",
+ file_dotlock_get_lock_path(dotlock),
+ (long)uid, (long)gid);
+ }
+ file_dotlock_delete(&dotlock);
+ return -1;
+ }
+ }
+ *dotlock_r = dotlock;
+ return fd;
+}
+
+int file_dotlock_open_mode(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, uid_t uid, gid_t gid,
+ struct dotlock **dotlock_r)
+{
+ return file_dotlock_open_mode_full(set, path, flags, mode, uid, gid,
+ NULL, dotlock_r);
+}
+
+int file_dotlock_open_group(const struct dotlock_settings *set, const char *path,
+ enum dotlock_create_flags flags,
+ mode_t mode, gid_t gid, const char *gid_origin,
+ struct dotlock **dotlock_r)
+{
+ return file_dotlock_open_mode_full(set, path, flags, mode, (uid_t)-1,
+ gid, gid_origin, dotlock_r);
+}
+
+int file_dotlock_replace(struct dotlock **dotlock_p,
+ enum dotlock_replace_flags flags)
+{
+ struct dotlock *dotlock;
+ const char *lock_path;
+ bool is_locked;
+
+ dotlock = *dotlock_p;
+ *dotlock_p = NULL;
+
+ is_locked = (flags & DOTLOCK_REPLACE_FLAG_VERIFY_OWNER) == 0 ? TRUE :
+ file_dotlock_is_locked(dotlock);
+
+ if ((flags & DOTLOCK_REPLACE_FLAG_DONT_CLOSE_FD) != 0)
+ dotlock->fd = -1;
+
+ if (!is_locked) {
+ dotlock_replaced_warning(dotlock, FALSE);
+ errno = EEXIST;
+ file_dotlock_free(&dotlock);
+ return 0;
+ }
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (rename(lock_path, dotlock->path) < 0) {
+ i_error("rename(%s, %s) failed: %m", lock_path, dotlock->path);
+ if (errno == ENOENT)
+ dotlock_replaced_warning(dotlock, TRUE);
+ file_dotlock_free(&dotlock);
+ return -1;
+ }
+ file_dotlock_free(&dotlock);
+ return 1;
+}
+
+int file_dotlock_touch(struct dotlock *dotlock)
+{
+ time_t now = time(NULL);
+ struct utimbuf buf;
+ int ret = 0;
+
+ if (dotlock->mtime == now)
+ return 0;
+
+ dotlock->mtime = now;
+ buf.actime = buf.modtime = now;
+
+ T_BEGIN {
+ const char *lock_path = file_dotlock_get_lock_path(dotlock);
+ if (utime(lock_path, &buf) < 0) {
+ i_error("utime(%s) failed: %m", lock_path);
+ ret = -1;
+ }
+ } T_END;
+ return ret;
+}
+
+bool file_dotlock_is_locked(struct dotlock *dotlock)
+{
+ struct stat st, st2;
+ const char *lock_path;
+
+ lock_path = file_dotlock_get_lock_path(dotlock);
+ if (fstat(dotlock->fd, &st) < 0) {
+ i_error("fstat(%s) failed: %m", lock_path);
+ return FALSE;
+ }
+
+ if (nfs_safe_lstat(lock_path, &st2) < 0) {
+ i_error("lstat(%s) failed: %m", lock_path);
+ return FALSE;
+ }
+ return st.st_ino == st2.st_ino && CMP_DEV_T(st.st_dev, st2.st_dev);
+}
+
+const char *file_dotlock_get_lock_path(struct dotlock *dotlock)
+{
+ if (dotlock->lock_path == NULL) {
+ dotlock->lock_path =
+ i_strconcat(dotlock->path,
+ dotlock->settings.lock_suffix, NULL);
+ }
+ return dotlock->lock_path;
+}