/*
 * Copyright (c) 2005 Proofpoint, Inc. and its suppliers.
 *	All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 *
 */

#include <sm/gen.h>
SM_IDSTR(id, "@(#)$Id: t-lockfile.c,v 1.2 2013-11-22 20:51:50 ca Exp $")
#include <stdlib.h>
#include <stdio.h>
#include <sendmail.h>

#define IOBUFSZ	64
char iobuf[IOBUFSZ];
#define FIRSTLINE	"first line\n"
#define LASTLINE	"last line\n"
static int noio, chk;
static pid_t pid;

/*
**  OPENFILE -- open a file
**
**	Parameters:
**		owner -- create file?
**		filename -- name of file.
**		flags -- flags for open(2)
**
**	Returns:
**		>=0 fd
**		<0 on failure.
*/

static int
openfile(owner, filename, flags)
	int owner;
	char *filename;
	int flags;
{
	int fd;

	if (owner)
		flags |= O_CREAT;
	fd = open(filename, flags, 0640);
	if (fd >= 0)
		return fd;
	fprintf(stderr, "%d: %ld: owner=%d, open(%s) failed\n",
		(int) pid, (long) time(NULL), owner, filename);
	return -1;
}

/*
**  WRBUF -- write iobuf to fd
**
**	Parameters:
**		fd -- file descriptor.
**
**	Returns:
**		==0 write was ok
**		!=0 on failure.
*/

static int
wrbuf(fd)
	int fd;
{
	int r;

	if (noio)
		return 0;
	r = write(fd, iobuf, sizeof(iobuf));
	if (sizeof(iobuf) == r)
		return 0;
	fprintf(stderr, "%d: %ld: owner=1, write(%s)=fail\n",
		(int) pid, (long) time(NULL), iobuf);
	return 1;
}

/*
**  RDBUF -- read from fd
**
**	Parameters:
**		fd -- file descriptor.
**		xbuf -- expected content.
**
**	Returns:
**		==0 read was ok and content matches
**		!=0 otherwise
*/

static int
rdbuf(fd, xbuf)
	int fd;
	const char *xbuf;
{
	int r;

	if (noio)
		return 0;
	r = read(fd, iobuf, sizeof(iobuf));
	if (sizeof(iobuf) != r)
	{
		fprintf(stderr, "%d: %ld: owner=0, read()=fail\n",
			(int) pid, (long) time(NULL));
		return 1;
	}
	if (strncmp(iobuf, xbuf, strlen(xbuf)))
	{
		fprintf(stderr, "%d: %ld: owner=0, read=%s expected=%s\n",
			(int) pid, (long) time(NULL), iobuf, xbuf);
		return 1;
	}
	return 0;
}

/*
**  LOCKTESTWR -- test WR/EX file locking
**
**	Parameters:
**		owner -- create file?
**		filename -- name of file.
**		flags -- flags for open(2)
**		delay -- how long to keep file locked?
**
**	Returns:
**		0 on success
**		!= 0 on failure.
*/

#define DBGPRINTR(str)	\
	do	\
	{	\
		fprintf(stderr, "%d: %ld: owner=0, ", (int) pid,	\
			(long) time(NULL));	\
		fprintf(stderr, str, filename, shared ? "RD" : "EX");	\
	} while (0)

static int
locktestwr(filename, flags, delay)
	char *filename;
	int flags;
	int delay;
{
	int fd;
	bool locked;

	fd = openfile(1, filename, flags);
	if (fd < 0)
		return errno;
	locked = lockfile(fd, filename, "[owner]", LOCK_EX);
	if (!locked)
	{
		fprintf(stderr, "%d: %ld: owner=1, lock(%s) failed\n",
			(int) pid, (long) time(NULL), filename);
		return 1;
	}
	else
		fprintf(stderr, "%d: %ld: owner=1, lock(%s) ok\n",
			(int) pid, (long) time(NULL), filename);

	sm_strlcpy(iobuf, FIRSTLINE, sizeof(iobuf));
	if (wrbuf(fd))
		return 1;
	if (delay > 0)
		sleep(delay);
	sm_strlcpy(iobuf, LASTLINE, sizeof(iobuf));
	if (wrbuf(fd))
		return 1;
	locked = lockfile(fd, filename, "[owner]", LOCK_UN);
	if (!locked)
	{
		fprintf(stderr, "%d: %ld: owner=1, unlock(%s) failed\n",
			(int) pid, (long) time(NULL), filename);
		return 1;
	}
	fprintf(stderr, "%d: %ld: owner=1, unlock(%s) done\n",
		(int) pid, (long) time(NULL), filename);
	if (fd > 0)
	{
		close(fd);
		fd = -1;
	}
	return 0;
}

/*
**  CHKLCK -- check whether fd is locked (only for fcntl())
**
**	Parameters:
**		owner -- create file?
**		filename -- name of file.
**		flags -- flags for open(2)
**		delay -- how long to keep file locked?
**
**	Returns:
**		0 if not locked
**		>0 pid of process which holds a WR lock
**		<0 error
*/

static long
chklck(fd)
	int fd;
{
#if !HASFLOCK
	int action, i;
	struct flock lfd;

	(void) memset(&lfd, '\0', sizeof lfd);
	lfd.l_type = F_RDLCK;
	action = F_GETLK;
	while ((i = fcntl(fd, action, &lfd)) < 0 && errno == EINTR)
		continue;
	if (i < 0)
		return (long)i;
	if (F_WRLCK == lfd.l_type)
		return (long)lfd.l_pid;
	return 0L;
#else /* !HASFLOCK */
	fprintf(stderr, "%d: %ld: flock(): no lock test\n",
		(int) pid, (long) time(NULL));
	return -1L;
#endif /* !HASFLOCK */
}

/*
**  LOCKTESTRD -- test file locking for reading
**
**	Parameters:
**		filename -- name of file.
**		flags -- flags for open(2)
**		delay -- how long is file locked by owner?
**		shared -- LOCK_{EX/SH}
**
**	Returns:
**		0 on success
**		!= 0 on failure.
*/

static int
locktestrd(filename, flags, delay, shared)
	char *filename;
	int flags;
	int delay;
	int shared;
{
	int fd, cnt;
	int lt;
	bool locked;

	fd = openfile(0, filename, flags);
	if (fd < 0)
		return errno;
	if (chk)
	{
		long locked;

		locked = chklck(fd);
		if (locked > 0)
			fprintf(stderr, "%d: %ld: file=%s status=locked pid=%ld\n",
				 (int) pid, (long) time(NULL), filename, locked);
		else if (0 == locked)
			fprintf(stderr, "%d: %ld: file=%s status=not_locked\n",
				 (int) pid, (long) time(NULL), filename);
		else
			fprintf(stderr, "%d: %ld: file=%s status=unknown\n",
				 (int) pid, (long) time(NULL), filename);
		goto end;
	}

	if (shared)
		lt = LOCK_SH;
	else
		lt = LOCK_EX;

	for (cnt = 0; cnt < delay - 2; cnt++)
	{
		/* try to get lock: should fail (nonblocking) */
		locked = lockfile(fd, filename, "[client]", lt|LOCK_NB);
		if (locked)
		{
			DBGPRINTR("lock(%s)=%s succeeded\n");
			return 1;
		}
		sleep(1);
	}
	if (delay > 0)
		sleep(2);
	locked = lockfile(fd, filename, "[client]", lt);
	if (!locked)
	{
		DBGPRINTR("lock(%s)=%s failed\n");
		return 1;
	}
	DBGPRINTR("lock(%s)=%s ok\n");
	if (rdbuf(fd, FIRSTLINE))
		return 1;
	if (rdbuf(fd, LASTLINE))
		return 1;
	sleep(1);
	locked = lockfile(fd, filename, "[client]", LOCK_UN);
	if (!locked)
	{
		DBGPRINTR("unlock(%s)=%s failed\n");
		return 1;
	}
	DBGPRINTR("unlock(%s)=%s done\n");

  end:
	if (fd > 0)
	{
		close(fd);
		fd = -1;
	}
	return 0;
}

/*
**  USAGE -- show usage
**
**	Parameters:
**		prg -- name of program
**
**	Returns:
**		nothing.
*/

static void
usage(prg)
	const char *prg;
{
	fprintf(stderr, "usage: %s [options]\n"
		"-f filename	use filename\n"
		"-i		do not perform I/O\n"
		"-n		do not try non-blocking locking first\n"
		"-R		only start reader process\n"
		"-r		use shared locking for reader\n"
		"-s delay	sleep delay seconds before unlocking\n"
		"-W		only start writer process\n"
#if !HASFLOCK
		"uses fcntl()\n"
#else
		"uses flock()\n"
#endif

		, prg);
}

int
main(argc, argv)
	int argc;
	char *argv[];
{
	int ch, delay, r, status, flags, shared, nb, reader, writer;
	char *filename;
	pid_t fpid;
	extern char *optarg;

	delay = 5;
	filename = "testlock";
	flags = O_RDWR;
	shared = nb = noio = reader = writer = chk = 0;
#define OPTIONS	"cf:inRrs:W"
	while ((ch = getopt(argc, argv, OPTIONS)) != -1)
	{
		switch ((char) ch)
		{
		  case 'c':
			chk = 1;
			break;

		  case 'f':
			filename = optarg;
			break;

		  case 'i':
			noio = 1;
			break;

		  case 'n':
			nb = 0;
			break;

		  case 'R':
			reader = 1;
			break;

		  case 'r':
			shared = 1;
			break;

		  case 's':
			delay = atoi(optarg);
			break;

		  case 'W':
			writer = 1;
			break;

		  default:
			usage(argv[0]);
			exit(69);
			break;
		}
	}

	fpid = -1;
	if (0 == reader && 0 == writer && (fpid = fork()) < 0)
	{
		perror("fork failed\n");
		return 1;
	}

	r = 0;
	if (reader || fpid == 0)
	{
		/* give the parent the chance to set up data */
		pid = getpid();
		sleep(1);
		r = locktestrd(filename, flags, nb ? delay : 0, shared);
	}
	if (writer || fpid > 0)
	{
		fpid = getpid();
		r = locktestwr(filename, flags, delay);
		(void) wait(&status);
	}
	/* (void) unlink(filename); */
	return r;
}