/*
** lock.c -- routines to lock/unlock files
**
** This code is Copyright (c) 2002, by the authors of nmh.  See the
** COPYRIGHT file in the root directory of the nmh distribution for
** complete copyright information.
*/

/*
** Modified by Ruud de Rooij to support Miquel van Smoorenburg's liblockfile
**
** Since liblockfile locking shares most of its code with dot locking, it
** is enabled by defining both DOT_LOCKING and HAVE_LIBLOCKFILE.
**
** Ruud de Rooij <ruud@debian.org>  Sun, 28 Mar 1999 15:34:03 +0200
*/

#include <unistd.h>
#include <h/mh.h>
#include <h/signals.h>
#include <h/utils.h>
#include <sys/stat.h>

#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif
#include <time.h>

#include <errno.h>

#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#else
# include <sys/file.h>
#endif

#if defined(LOCKF_LOCKING) || defined(FLOCK_LOCKING)
# include <sys/file.h>
#endif

#include <signal.h>

#if defined(HAVE_LIBLOCKFILE)
#include <lockfile.h>
#endif

#ifdef DOT_LOCKING
# ifdef LOCKDIR
char *lockdir = LOCKDIR;
# endif
#endif

/* Are we using any kernel locking? */
#if defined (FLOCK_LOCKING) || defined(LOCKF_LOCKING) || defined(FCNTL_LOCKING)
# define KERNEL_LOCKING
#endif

#ifdef DOT_LOCKING

/* struct for getting name of lock file to create */
struct lockinfo {
	char curlock[BUFSIZ];
#if !defined(HAVE_LIBLOCKFILE)
	char tmplock[BUFSIZ];
#endif
};

/*
** Amount of time to wait before
** updating ctime of lock file.
*/
#define NSECS 20

#if !defined(HAVE_LIBLOCKFILE)
/*
** How old does a lock file need to be
** before we remove it.
*/
#define RSECS 180
#endif /* HAVE_LIBLOCKFILE */

/* struct for recording and updating locks */
struct lock {
	int l_fd;
	char *l_lock;
	struct lock *l_next;
};

/* top of list containing all open locks */
static struct lock *l_top = NULL;
#endif /* DOT_LOCKING */

/*
** static prototypes
*/
#ifdef KERNEL_LOCKING
static int lkopen_kernel(char *, int, mode_t);
#endif

#ifdef DOT_LOCKING
static int lkopen_dot(char *, int, mode_t);
static void lockname(char *, struct lockinfo *, int);
static void timerON(char *, int);
static void timerOFF(int);
static void alrmser(int);

#if !defined(HAVE_LIBLOCKFILE)
static int lockit(struct lockinfo *);
#endif
#endif

/*
** Base routine to open and lock a file,
** and return a file descriptor.
*/

int
lkopen(char *file, int access, mode_t mode)
{
#ifdef KERNEL_LOCKING
	return lkopen_kernel(file, access, mode);
#endif

#ifdef DOT_LOCKING
	return lkopen_dot(file, access, mode);
#endif
}


/*
** Base routine to close and unlock a file,
** given a file descriptor.
*/

int
lkclose(int fd, char *file)
{
#ifdef FCNTL_LOCKING
	struct flock buf;
#endif

#ifdef DOT_LOCKING
	struct lockinfo lkinfo;
#endif

	if (fd == -1)
		return 0;

#ifdef FCNTL_LOCKING
	buf.l_type   = F_UNLCK;
	buf.l_whence = SEEK_SET;
	buf.l_start  = 0;
	buf.l_len = 0;
	fcntl(fd, F_SETLK, &buf);
#endif

#ifdef FLOCK_LOCKING
	flock(fd, LOCK_UN);
#endif

#ifdef LOCKF_LOCKING
	/* make sure we unlock the whole thing */
	lseek(fd, (off_t) 0, SEEK_SET);
	lockf(fd, F_ULOCK, 0L);
#endif

#ifdef DOT_LOCKING
	lockname(file, &lkinfo, 0);  /* get name of lock file */
#if !defined(HAVE_LIBLOCKFILE)
	unlink(lkinfo.curlock);  /* remove lock file */
#else
	lockfile_remove(lkinfo.curlock);
#endif /* HAVE_LIBLOCKFILE */
	timerOFF(fd);  /* turn off lock timer   */
#endif /* DOT_LOCKING */

	return (close(fd));
}


/*
** Base routine to open and lock a file,
** and return a FILE pointer
*/

FILE *
lkfopen(char *file, char *mode)
{
	int fd, access;
	FILE *fp;

	if (strcmp(mode, "r") == 0)
		access = O_RDONLY;
	else if (strcmp(mode, "r+") == 0)
		access = O_RDWR;
	else if (strcmp(mode, "w") == 0)
		access = O_WRONLY | O_CREAT | O_TRUNC;
	else if (strcmp(mode, "w+") == 0)
		access = O_RDWR | O_CREAT | O_TRUNC;
	else if (strcmp(mode, "a") == 0)
		access = O_WRONLY | O_CREAT | O_APPEND;
	else if (strcmp(mode, "a+") == 0)
		access = O_RDWR | O_CREAT | O_APPEND;
	else {
		errno = EINVAL;
		return NULL;
	}

	if ((fd = lkopen(file, access, 0666)) == -1)
		return NULL;

	if ((fp = fdopen(fd, mode)) == NULL) {
		close(fd);
		return NULL;
	}

	return fp;
}


/*
** Base routine to close and unlock a file,
** given a FILE pointer
*/

int
lkfclose(FILE *fp, char *file)
{
#ifdef FCNTL_LOCKING
	struct flock buf;
#endif

#ifdef DOT_LOCKING
	struct lockinfo lkinfo;
#endif

	if (fp == NULL)
		return 0;

#ifdef FCNTL_LOCKING
	buf.l_type   = F_UNLCK;
	buf.l_whence = SEEK_SET;
	buf.l_start  = 0;
	buf.l_len = 0;
	fcntl(fileno(fp), F_SETLK, &buf);
#endif

#ifdef FLOCK_LOCKING
	flock(fileno(fp), LOCK_UN);
#endif

#ifdef LOCKF_LOCKING
	/* make sure we unlock the whole thing */
	fseek(fp, 0L, SEEK_SET);
	lockf(fileno(fp), F_ULOCK, 0L);
#endif

#ifdef DOT_LOCKING
	lockname(file, &lkinfo, 0);  /* get name of lock file */
#if !defined(HAVE_LIBLOCKFILE)
	unlink(lkinfo.curlock);  /* remove lock file */
#else
	lockfile_remove(lkinfo.curlock);
#endif /* HAVE_LIBLOCKFILE */
	timerOFF(fileno(fp));  /* turn off lock timer   */
#endif /* DOT_LOCKING */

	return (fclose(fp));
}


#ifdef KERNEL_LOCKING

/*
** open and lock a file, using kernel locking
*/

static int
lkopen_kernel(char *file, int access, mode_t mode)
{
	int fd, i, j;

# ifdef FCNTL_LOCKING
	struct flock buf;
# endif /* FCNTL_LOCKING */

	for (i = 0; i < 5; i++) {

# if defined(LOCKF_LOCKING) || defined(FCNTL_LOCKING)
		/* remember the original mode */
		j = access;

		/* make sure we open at the beginning */
		access &= ~O_APPEND;

		/*
		** We MUST have write permission or
		** lockf/fcntl() won't work
		*/
		if ((access & 03) == O_RDONLY) {
			access &= ~O_RDONLY;
			access |= O_RDWR;
		}
# endif /* LOCKF_LOCKING || FCNTL_LOCKING */

		if ((fd = open(file, access | O_NDELAY, mode)) == -1)
			return -1;

# ifdef FCNTL_LOCKING
		buf.l_type   = F_WRLCK;
		buf.l_whence = SEEK_SET;
		buf.l_start  = 0;
		buf.l_len = 0;
		if (fcntl(fd, F_SETLK, &buf) != -1)
			return fd;
# endif

# ifdef FLOCK_LOCKING
		if (flock(fd, (((access & 03) == O_RDONLY) ? LOCK_SH :
			LOCK_EX) | LOCK_NB) != -1)
			return fd;
# endif

# ifdef LOCKF_LOCKING
		if (lockf(fd, F_TLOCK, 0L) != -1) {
			/* see if we should be at the end */
			if (j & O_APPEND)
				lseek(fd, (off_t) 0, SEEK_END);
			return fd;
		}
# endif

		j = errno;
		close(fd);
		sleep(1);
	}

	close(fd);
	errno = j;
	return -1;
}

#endif /* KERNEL_LOCKING */


#ifdef DOT_LOCKING

/*
** open and lock a file, using dot locking
*/

static int
lkopen_dot(char *file, int access, mode_t mode)
{
	int fd;
	struct lockinfo lkinfo;

	/* open the file */
	if ((fd = open(file, access, mode)) == -1)
		return -1;

	/*
	** Get the name of the eventual lock file, as well
	** as a name for a temporary lock file.
	*/
	lockname(file, &lkinfo, 1);

#if !defined(HAVE_LIBLOCKFILE)
	{
		int i;
		for (i = 0;;) {
			/* attempt to create lock file */
			if (lockit(&lkinfo) == 0) {
				/* if successful, turn on timer and return */
				timerON(lkinfo.curlock, fd);
				return fd;
			} else {
				/*
				** Abort locking, if we fail to lock after 5
				** attempts and are never able to stat the
				** lock file.
				*/
				struct stat st;
				if (stat(lkinfo.curlock, &st) == -1) {
					if (i++ > 5)
						return -1;
					sleep(1);
				} else {
					time_t curtime;
					i = 0;
					time(&curtime);

					/*
					** check for stale lockfile,
					** else sleep
					*/
					if (curtime > st.st_ctime + RSECS)
						unlink(lkinfo.curlock);
					else
						sleep(1);
				}
				lockname(file, &lkinfo, 1);
			}
		}
	}
#else
	if (lockfile_create(lkinfo.curlock, 5, 0) == L_SUCCESS) {
		timerON(lkinfo.curlock, fd);
		return fd;
	} else {
		close(fd);
		return -1;
	}
#endif /* HAVE_LIBLOCKFILE */
}

#if !defined(HAVE_LIBLOCKFILE)
/*
** Routine that actually tries to create
** the lock file.
*/

static int
lockit(struct lockinfo *li)
{
	int fd;
	char *curlock, *tmplock;

#if 0
	char buffer[128];
#endif

	curlock = li->curlock;
	tmplock = li->tmplock;

	if ((fd = mkstemp(tmplock)) == -1) {
		advise(NULL, "unable to create temporary file in %s", tmplock);
		return -1;
	}

#if 0
	/* write our process id into lock file */
	snprintf(buffer, sizeof(buffer), "nmh lock: pid %d\n", (int) getpid());
	write(fd, buffer, strlen(buffer) + 1);
#endif

	close(fd);

	/*
	** Now try to create the real lock file
	** by linking to the temporary file.
	*/
	fd = link(tmplock, curlock);
	unlink(tmplock);

	return (fd == -1 ? -1 : 0);
}
#endif /* HAVE_LIBLOCKFILE */

/*
** Get name of lock file, and temporary lock file
*/

static void
lockname(char *file, struct lockinfo *li, int isnewlock)
{
	int bplen, tmplen;
	char *bp, *cp;

	if ((cp = strrchr(file, '/')) == NULL || *++cp == 0)
		cp = file;

	bp = li->curlock;
	bplen = 0;
#ifdef LOCKDIR
	snprintf(bp, sizeof(li->curlock), "%s/", lockdir);
	tmplen = strlen(bp);
	bp += tmplen;
	bplen += tmplen;
#else
	if (cp != file) {
		snprintf(bp, sizeof(li->curlock), "%.*s", (int)(cp - file), file);
		tmplen = strlen(bp);
		bp += tmplen;
		bplen += tmplen;
	}
#endif

	snprintf(bp, sizeof(li->curlock) - bplen, "%s.lock", cp);

#if !defined(HAVE_LIBLOCKFILE)
	/*
	** If this is for a new lock, create a name for
	** the temporary lock file for lockit()
	*/
	if (isnewlock) {
		if ((cp = strrchr(li->curlock, '/')) == NULL || *++cp == 0)
			strncpy(li->tmplock, ",LCK.XXXXXX", sizeof(li->tmplock));
		else
			snprintf(li->tmplock, sizeof(li->tmplock),
					"%.*s,LCK.XXXXXX",
					(int)(cp - li->curlock), li->curlock);
	}
#endif
}


/*
** Add new lockfile to the list of open lockfiles
** and start the lock file timer.
*/

static void
timerON(char *curlock, int fd)
{
	struct lock *lp;
	size_t len;

	lp = mh_xcalloc(1, sizeof(*lp));

	len = strlen(curlock) + 1;
	lp->l_fd = fd;
	lp->l_lock = mh_xcalloc(len, sizeof(char));
	memcpy(lp->l_lock, curlock, len);
	lp->l_next = l_top;

	if (!l_top) {
		/* perhaps SIGT{STP,TIN,TOU} ? */
		SIGNAL(SIGALRM, alrmser);
		alarm(NSECS);
	}

	l_top = lp;
}


/*
** Search through the list of lockfiles for the
** current lockfile, and remove it from the list.
*/

static void
timerOFF(int fd)
{
	struct lock *pp, *lp;

	alarm(0);

	if (l_top) {
		for (pp = lp = l_top; lp; pp = lp, lp = lp->l_next) {
			if (lp->l_fd == fd)
				break;
		}
		if (lp) {
			if (lp == l_top)
				l_top = lp->l_next;
			else
				pp->l_next = lp->l_next;

			mh_free0(&(lp->l_lock));
			mh_free0(&lp);
		}
	}

	/* if there are locks left, restart timer */
	if (l_top)
		alarm(NSECS);
}


/*
** If timer goes off, we update the ctime of all open
** lockfiles, so another command doesn't remove them.
*/

static void
alrmser(int sig)
{
	char *lockfile;
	struct lock *lp;

	/* update the ctime of all the lock files */
	for (lp = l_top; lp; lp = lp->l_next) {
		lockfile = lp->l_lock;
#if !defined(HAVE_LIBLOCKFILE)
		{
			int j;
			if (*lockfile && (j = creat(lockfile, 0600)) != -1)
				close(j);
		}
#else
	lockfile_touch(lockfile);
#endif
	}

	/* restart the alarm */
	alarm(NSECS);
}

#endif /* DOT_LOCKING */
