/*
 * Copyright (c) 1992, Brian Berliner and Jeff Polk
 * Copyright (c) 1989-1992, Brian Berliner
 * 
 * You may distribute under the terms of the GNU General Public License as
 * specified in the README file that comes with the CVS 1.4 kit.
 * 
 * Set Lock
 * 
 * Lock file support for CVS.
 */

#include "cvs.h"

#ifndef lint
static const char rcsid[] = "$CVSid: @(#)lock.c 1.50 94/09/30 $";
USE(rcsid);
#endif

static int readers_exist PROTO((char *repository));
static int set_lock PROTO((char *repository, int will_wait));
static void clear_lock PROTO((void));
static void set_lockers_name PROTO((struct stat *statp));
static int set_writelock_proc PROTO((Node * p, void *closure));
static int unlock_proc PROTO((Node * p, void *closure));
static int write_lock PROTO((char *repository));
static void unlock PROTO((char *repository));
static void lock_wait PROTO((char *repository));
static int Check_Owner PROTO((char *lockdir));

static char lockers_name[20];
static char *repository;
static char readlock[PATH_MAX], writelock[PATH_MAX], masterlock[PATH_MAX];
static int cleanup_lckdir;
static List *locklist;

#define L_OK		0		/* success */
#define L_ERROR		1		/* error condition */
#define L_LOCKED	2		/* lock owned by someone else */

/*
 * Clean up all outstanding locks
 */
void
Lock_Cleanup ()
{
    /* clean up simple locks (if any) */
    if (repository != NULL)
    {
	unlock (repository);
	repository = (char *) NULL;
    }

    /* clean up multiple locks (if any) */
    if (locklist != (List *) NULL)
    {
	(void) walklist (locklist, unlock_proc, NULL);
	locklist = (List *) NULL;
    }
}

/*
 * walklist proc for removing a list of locks
 */
static int
unlock_proc (p, closure)
    Node *p;
    void *closure;
{
    unlock (p->key);
    return (0);
}

/*
 * Remove the lock files (without complaining if they are not there),
 */
static void
unlock (repository)
    char *repository;
{
    char tmp[PATH_MAX];

    if (readlock[0] != '\0')
    {
	(void) sprintf (tmp, "%s/%s", repository, readlock);
	if (unlink (tmp) < 0 && ! existence_error (errno))
	    error (0, errno, "failed to remove lock %s", tmp);
    }

    if (writelock[0] != '\0')
    {
	(void) sprintf (tmp, "%s/%s", repository, writelock);
	if (unlink (tmp) < 0 && ! existence_error (errno))
	    error (0, errno, "failed to remove lock %s", tmp);
    }

    /*
     * Only remove the lock directory if it is ours, note that this does
     * lead to the limitation that one user ID should not be committing
     * files into the same Repository directory at the same time. Oh well.
     */
    if (writelock[0] != '\0' || (readlock[0] != '\0' && cleanup_lckdir)) 
    {
	    (void) sprintf (tmp, "%s/%s", repository, CVSLCK);
    	    if (Check_Owner(tmp))
	    {
#ifdef AFSCVS
		char rmuidlock[PATH_MAX];
		sprintf(rmuidlock, "rm -f %s/uidlock%d", tmp, geteuid() );
		system(rmuidlock);
#endif
	    (void) rmdir (tmp);
	    }
    }
    cleanup_lckdir = 0;
}

/*
 * Check the owner of a lock.  Returns 1 if we own it, 0 otherwise.
 */
static int
Check_Owner(lockdir)
     char *lockdir;
{
  struct stat sb;

#ifdef AFSCVS
  /* In the Andrew File System (AFS), user ids from stat don't match
     those from geteuid().  The AFSCVS code can deal with either AFS or
     non-AFS repositories; the non-AFSCVS code is faster.  */
  char uidlock[PATH_MAX];

  /* Check if the uidlock is in the lock directory */
  sprintf(uidlock, "%s/uidlock%d", lockdir, geteuid() );
  if( stat(uidlock, &sb) != -1)
    return 1;   /* The file exists, therefore we own the lock */
  else
    return 0; 	/* The file didn't exist or some other error.
		 * Assume that we don't own it.
		 */
#else
  if (stat (lockdir, &sb) != -1 && sb.st_uid == geteuid ())
    return 1;
  else
    return 0;
#endif
}  /* end Check_Owner() */


/*
 * Create a lock file for readers
 */
int
Reader_Lock (xrepository)
    char *xrepository;
{
    int err = 0;
    FILE *fp;
    char tmp[PATH_MAX];

    if (noexec)
	return (0);

    /* we only do one directory at a time for read locks! */
    if (repository != NULL)
    {
	error (0, 0, "Reader_Lock called while read locks set - Help!");
	return (1);
    }

    if (readlock[0] == '\0')
      (void) sprintf (readlock, 
#ifdef HAVE_LONG_FILE_NAMES
		"%s.%s.%d", CVSRFL, hostname,
#else
		"%s.%d", CVSRFL,
#endif
		getpid ());

    /* remember what we're locking (for lock_cleanup) */
    repository = xrepository;

#ifdef BOGUS_UNLESS_PROVEN_OTHERWISE
    /* make sure we can write the repository */
    (void) sprintf (tmp,
#ifdef HAVE_LONG_FILE_NAMES
	"%s/%s.%s.%d", xrepository, CVSTFL, hostname,
#else
	"%s/%s.%d", xrepository, CVSTFL,
#endif
	getpid());
    if ((fp = fopen (tmp, "w+")) == NULL || fclose (fp) == EOF)
    {
	error (0, errno, "cannot create read lock in repository `%s'",
	       xrepository);
	readlock[0] = '\0';
	if (unlink (tmp) < 0 && ! existence_error (errno))
	    error (0, errno, "failed to remove lock %s", tmp);
	return (1);
    }
    if (unlink (tmp) < 0)
	error (0, errno, "failed to remove lock %s", tmp);
#endif

    /* get the lock dir for our own */
    if (set_lock (xrepository, 1) != L_OK)
    {
	error (0, 0, "failed to obtain dir lock in repository `%s'",
	       xrepository);
	readlock[0] = '\0';
	return (1);
    }

    /* write a read-lock */
    (void) sprintf (tmp, "%s/%s", xrepository, readlock);
    if ((fp = fopen (tmp, "w+")) == NULL || fclose (fp) == EOF)
    {
	error (0, errno, "cannot create read lock in repository `%s'",
	       xrepository);
	readlock[0] = '\0';
	err = 1;
    }

    /* free the lock dir */
    clear_lock();

    return (err);
}

/*
 * Lock a list of directories for writing
 */
static char *lock_error_repos;
static int lock_error;
int
Writer_Lock (list)
    List *list;
{
    if (noexec)
	return (0);

    /* We only know how to do one list at a time */
    if (locklist != (List *) NULL)
    {
	error (0, 0, "Writer_Lock called while write locks set - Help!");
	return (1);
    }

    for (;;)
    {
	/* try to lock everything on the list */
	lock_error = L_OK;		/* init for set_writelock_proc */
	lock_error_repos = (char *) NULL; /* init for set_writelock_proc */
	locklist = list;		/* init for Lock_Cleanup */
	(void) strcpy (lockers_name, "unknown");

	(void) walklist (list, set_writelock_proc, NULL);

	switch (lock_error)
	{
	    case L_ERROR:		/* Real Error */
		Lock_Cleanup ();	/* clean up any locks we set */
		error (0, 0, "lock failed - giving up");
		return (1);

	    case L_LOCKED:		/* Someone already had a lock */
		Lock_Cleanup ();	/* clean up any locks we set */
		lock_wait (lock_error_repos); /* sleep a while and try again */
		continue;

	    case L_OK:			/* we got the locks set */
		return (0);

	    default:
		error (0, 0, "unknown lock status %d in Writer_Lock",
		       lock_error);
		return (1);
	}
    }
}

/*
 * walklist proc for setting write locks
 */
static int
set_writelock_proc (p, closure)
    Node *p;
    void *closure;
{
    /* if some lock was not OK, just skip this one */
    if (lock_error != L_OK)
	return (0);

    /* apply the write lock */
    lock_error_repos = p->key;
    lock_error = write_lock (p->key);
    return (0);
}

/*
 * Create a lock file for writers returns L_OK if lock set ok, L_LOCKED if
 * lock held by someone else or L_ERROR if an error occurred
 */
static int
write_lock (repository)
    char *repository;
{
    int status;
    FILE *fp;
    char tmp[PATH_MAX];

    if (writelock[0] == '\0')
	(void) sprintf (writelock,
#ifdef HAVE_LONG_FILE_NAMES
	    "%s.%s.%d", CVSWFL, hostname,
#else
	    "%s.%d", CVSWFL,
#endif
	getpid());

#ifdef BOGUS_UNLESS_PROVEN_OTHERWISE
    /* make sure we can write the repository */
    (void) sprintf (tmp,
#ifdef HAVE_LONG_FILE_NAMES
	"%s/%s.%s.%d", repository, CVSTFL, hostname,
#else
	"%s/%s.%d", repository, CVSTFL,
#endif
	getpid ());
    if ((fp = fopen (tmp, "w+")) == NULL || fclose (fp) == EOF)
    {
	error (0, errno, "cannot create write lock in repository `%s'",
	       repository);
	if (unlink (tmp) < 0 && ! existence_error (errno))
	    error (0, errno, "failed to remove lock %s", tmp);
	return (L_ERROR);
    }
    if (unlink (tmp) < 0)
	error (0, errno, "failed to remove lock %s", tmp);
#endif

    /* make sure the lock dir is ours (not necessarily unique to us!) */
    status = set_lock (repository, 0);
    if (status == L_OK)
    {
	/* we now own a writer - make sure there are no readers */
	if (readers_exist (repository))
	{
	    /* clean up the lock dir if we created it */
	    if (status == L_OK)
	    {
		clear_lock();
	    }

	    /* indicate we failed due to read locks instead of error */
	    return (L_LOCKED);
	}

	/* write the write-lock file */
	(void) sprintf (tmp, "%s/%s", repository, writelock);
	if ((fp = fopen (tmp, "w+")) == NULL || fclose (fp) == EOF)
	{
	    int xerrno = errno;

	    if (unlink (tmp) < 0 && ! existence_error (errno))
		error (0, errno, "failed to remove lock %s", tmp);

	    /* free the lock dir if we created it */
	    if (status == L_OK)
	    {
		clear_lock();
	    }

	    /* return the error */
	    error (0, xerrno, "cannot create write lock in repository `%s'",
		   repository);
	    return (L_ERROR);
	}
	return (L_OK);
    }
    else
	return (status);
}

/*
 * readers_exist() returns 0 if there are no reader lock files remaining in
 * the repository; else 1 is returned, to indicate that the caller should
 * sleep a while and try again.
 */
static int
readers_exist (repository)
    char *repository;
{
    char line[MAXLINELEN];
    DIR *dirp;
    struct dirent *dp;
    struct stat sb;
    int ret = 0;

#ifdef CVS_FUDGELOCKS
again:
#endif

    if ((dirp = opendir (repository)) == NULL)
	error (1, 0, "cannot open directory %s", repository);

    errno = 0;
    while ((dp = readdir (dirp)) != NULL)
    {
	if (fnmatch (CVSRFLPAT, dp->d_name, 0) == 0)
	{
#ifdef CVS_FUDGELOCKS
	    time_t now;
	    (void) time (&now);
#endif

	    (void) sprintf (line, "%s/%s", repository, dp->d_name);
	    if (stat (line, &sb) != -1)
	    {
#ifdef CVS_FUDGELOCKS
		/*
		 * If the create time of the file is more than CVSLCKAGE 
		 * seconds ago, try to clean-up the lock file, and if
		 * successful, re-open the directory and try again.
		 */
		if (now >= (sb.st_ctime + CVSLCKAGE) && unlink (line) != -1)
		{
		    (void) closedir (dirp);
		    goto again;
		}
#endif
		set_lockers_name (&sb);
	    }

	    ret = 1;
	    break;
	}
	errno = 0;
    }
    if (errno != 0)
	error (0, errno, "error reading directory %s", repository);

    closedir (dirp);
    return (ret);
}

/*
 * Set the static variable lockers_name appropriately, based on the stat
 * structure passed in.
 */
static void
set_lockers_name (statp)
    struct stat *statp;
{
    struct passwd *pw;

    if ((pw = (struct passwd *) getpwuid (statp->st_uid)) !=
	(struct passwd *) NULL)
    {
	(void) strcpy (lockers_name, pw->pw_name);
    }
    else
	(void) sprintf (lockers_name, "uid%lu", (unsigned long) statp->st_uid);
}

/*
 * Persistently tries to make the directory "lckdir",, which serves as a
 * lock. If the create time on the directory is greater than CVSLCKAGE
 * seconds old, just try to remove the directory.
 */
static int
set_lock (repository, will_wait)
    char *repository;
    int will_wait;
{
    struct stat sb;
    mode_t omask;
#ifdef CVS_FUDGELOCKS
    time_t now;
#endif

    (void) sprintf (masterlock, "%s/%s", repository, CVSLCK);

    /*
     * Note that it is up to the callers of set_lock() to arrange for signal
     * handlers that do the appropriate things, like remove the lock
     * directory before they exit.
     */
    cleanup_lckdir = 0;
    for (;;)
    {
	int status = -1;
	omask = umask (cvsumask);
	SIG_beginCrSect ();
	if (CVS_MKDIR (masterlock, 0777) == 0)
	{
#ifdef AFSCVS
	    char uidlock[PATH_MAX];
	    FILE *fp;

	    sprintf(uidlock, "%s/uidlock%d", masterlock, geteuid() );
	    if ((fp = fopen(uidlock, "w+")) == NULL)
	    {
		/* We failed to create the uidlock,
		   so rm masterlock and leave */
		rmdir(masterlock);
		SIG_endCrSect ();
		status = L_ERROR;
		goto out;
	    }

	    /* We successfully created the uid lock, so close the file */
	    fclose(fp);
#endif
	    cleanup_lckdir = 1;
	    SIG_endCrSect ();
	    status = L_OK;
	    goto out;
	}
	SIG_endCrSect ();
      out:
	(void) umask (omask);
	if (status != -1)
	    return status;

	if (errno != EEXIST)
	{
	    error (0, errno,
		   "failed to create lock directory in repository `%s'",
		   repository);
	    return (L_ERROR);
	}

	/*
	 * stat the dir - if it is non-existent, re-try the loop since
	 * someone probably just removed it (thus releasing the lock)
	 */
	if (stat (masterlock, &sb) < 0)
	{
	    if (existence_error (errno))
		continue;

	    error (0, errno, "couldn't stat lock directory `%s'", masterlock);
	    return (L_ERROR);
	}

#ifdef CVS_FUDGELOCKS
	/*
	 * If the create time of the directory is more than CVSLCKAGE seconds
	 * ago, try to clean-up the lock directory, and if successful, just
	 * quietly retry to make it.
	 */
	(void) time (&now);
	if (now >= (sb.st_ctime + CVSLCKAGE))
	{
#ifdef AFSCVS
	  /* Remove the uidlock first */
	  char rmuidlock[PATH_MAX];
	  sprintf(rmuidlock, "rm -f %s/uidlock%d", masterlock, geteuid() );
	  system(rmuidlock);
#endif
	    if (rmdir (masterlock) >= 0)
		continue;
	}
#endif

	/* set the lockers name */
	set_lockers_name (&sb);

	/* if he wasn't willing to wait, return an error */
	if (!will_wait)
	    return (L_LOCKED);
	lock_wait (repository);
    }
}

/*
 * Clear master lock.  We don't have to recompute the lock name since
 * clear_lock is never called except after a successful set_lock().
 */
static void
clear_lock()
{
#ifdef AFSCVS
  /* Remove the uidlock first */
  char rmuidlock[PATH_MAX];
  sprintf(rmuidlock, "rm -f %s/uidlock%d", masterlock, geteuid() );
  system(rmuidlock);
#endif
    if (rmdir (masterlock) < 0)
	error (0, errno, "failed to remove lock dir `%s'", masterlock);
    cleanup_lckdir = 0;
}

/*
 * Print out a message that the lock is still held, then sleep a while.
 */
static void
lock_wait (repos)
    char *repos;
{
    time_t now;

    (void) time (&now);
    error (0, 0, "[%8.8s] waiting for %s's lock in %s", ctime (&now) + 11,
	   lockers_name, repos);
    (void) sleep (CVSLCKSLEEP);
}
