/* -*-Mode: C++;-*-
 * PRCS - The Project Revision Control System
 * Copyright (C) 1997  Josh MacDonald
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: lock.cc 1.10.1.1.1.11.1.6 Sun, 25 May 1997 21:10:06 -0700 jmacd $
 */


extern "C" {
#include <time.h>
#include <fcntl.h>
#include "maketime.h"
}

#include "prcs.h"
#include "misc.h"
#include "utils.h"
#include "lock.h"

#define LOCK_FILE_MODE 0666

/* This file was once implemented using fcntl(), I still have the code,
 * but since NFS sucks, I don't even include it anymore. */

/*
 * The number of seconds the process waits between stat calls to check
 * on the status of the lock.
 */
#define TRIAL_WAIT_PERIOD 1

long get_file_age(const char* file)
{
    FILE* lock_file = fopen(file, "r");
    char line_buf[1024];
    time_t youngest = 0;

    if(!lock_file)
	goto done;

    if(!fgets(line_buf, 1024, lock_file))
	goto done;

    while(fgets(line_buf, 1024, lock_file)) {
	const char* ptr = line_buf;
	int spaces = 0;
	char c;

	if(line_buf[0] == ' ')
	    continue;

	/* this scan depends on the format of the file, it looks for the
	 * 5th space and assumes that is a time. */

	while((c = *ptr++) != 0) {
	    if(c == ' ')
		spaces += 1;

	    if(spaces == 5) {
		time_t this_age = str2time(ptr, 0, 0);

		if(this_age > youngest)
		    youngest = this_age;

		break;
	    }
	}
    }

done:
    if (lock_file)
	fclose (lock_file);

    return time(NULL) - youngest;
}

/*
 * chill until the lock is available.
 */
int chilling_out(const char* waitfile, int wait_type)
{
    long age = get_file_age(waitfile);

    if(age < 0) {
	return LOCK_AVAILABLE;
    } else if(age > STALE_LOCK_TIMEOUT) {
	return LOCK_TRY_STEAL;
    } else {
	if(wait_type == LOCK_WAIT) {
	    struct stat buf;
	    long sleeptime = STALE_LOCK_TIMEOUT - age;
	    while(sleeptime > 0) {
		sleeptime -= TRIAL_WAIT_PERIOD - sleep(TRIAL_WAIT_PERIOD);
		if(stat(waitfile, &buf) < 0)
		    return LOCK_AVAILABLE;
	    }
	    return LOCK_TRY_STEAL;
	} else {
	    return LOCK_TRY_AGAIN;
	}
    }
}

int AdvisoryLock::write_lock(int wait_type)
{
    int chill;
    struct stat buf;

    if(locktype == WRITE_LOCK)
	return LOCK_SUCCESS;

    ASSERT(locktype != READ_LOCK, "unlock first");

    while (true) {
	while((wrfd = open(wrlockname, O_RDWR|O_CREAT|O_EXCL, LOCK_FILE_MODE)) < 0) {
	    if(errno == EEXIST) {
		chill = chilling_out(wrlockname, wait_type);
		if(chill != LOCK_AVAILABLE)
		    return chill;
	    } else {
		prcserror << "Error opening lock file "
			   << squote(wrlockname) << " for write lock" << perror;
		return LOCK_FAIL;
	    }
	}

	if(locktype != TRYING_READ_LOCK && stat(rdlockname, &buf) >= 0) {
	    if(close(wrfd) < 0) {
		prcserror << "Write failed on write lock file" << perror;
		return LOCK_FAIL;
	    }
	    if(unlink(wrlockname) < 0 && errno != ENOENT) {
		prcserror << "Unlink failed on write lock file" << perror;
		return LOCK_FAIL;
	    }
	    chill = chilling_out(rdlockname, wait_type);
	    if(chill != LOCK_AVAILABLE)
		return chill;
	} else {
	    break;
	}
    }

    if(!write_write_owner_notice())
	return LOCK_FAIL;

    if(close(wrfd) < 0) {
	prcserror << "Write failed on read lock file " << squote(wrlockname) << perror;
	return LOCK_FAIL;
    }

    locktype = WRITE_LOCK;

    return LOCK_SUCCESS;
}

int AdvisoryLock::read_lock(int wait_type)
{
    int trywrite;

    if(locktype == READ_LOCK) return LOCK_SUCCESS;

    ASSERT(locktype != WRITE_LOCK, "unlock first");

    locktype = TRYING_READ_LOCK;

    trywrite = write_lock(wait_type);

    if(trywrite != LOCK_SUCCESS) {
	locktype = NO_LOCK;
	return trywrite;
    }

    if((rdfd = open(rdlockname, O_RDWR|O_CREAT|O_EXCL, LOCK_FILE_MODE)) < 0) {
	if(errno != EEXIST) {
	    prcserror << "Error opening lock file " << squote(rdlockname)
		      << " for read lock" << perror;
	    locktype = NO_LOCK;
	    return LOCK_FAIL;
	} else if((rdfd = open(rdlockname, O_RDWR|O_CREAT, LOCK_FILE_MODE)) < 0) {
	    prcserror << "Error opening lock file " << squote(rdlockname)
		      << " for read lock" << perror;
	    locktype = NO_LOCK;
	    return LOCK_FAIL;
	}
    } else /* file was created */ {
	if(write(rdfd, LOCK_HEADER, LOCK_LENGTH) < 0) {
	    prcserror << "Write failed on lock file " << squote(rdlockname) << perror;
	    locktype = NO_LOCK;
	    return LOCK_FAIL;
	}
    }

    if(!write_read_owner_notice()) {
	locktype = NO_LOCK;
	return LOCK_FAIL;
    }

    locktype = READ_LOCK;

    if(unlink(wrlockname) < 0 && errno != ENOENT) {
	prcserror << "Unlink failed on lock file " << squote(wrlockname) << perror;
	locktype = NO_LOCK;
	return LOCK_FAIL;
    }

    if(close(rdfd) < 0) {
	prcserror << "Close failed on lock file " << squote(rdlockname) << perror;
	locktype = NO_LOCK;
	return LOCK_FAIL;
    }

    return LOCK_SUCCESS;
}

bool AdvisoryLock::unlock()
{
    if(locktype == NO_LOCK) {
	return true;
    } else if(locktype == WRITE_LOCK) {
	if(unlink(wrlockname) < 0 && errno != ENOENT) {
	    prcserror << "Unlink failed on lock file " << squote(wrlockname) << perror;
	    return false;
	}
    } else if(locktype == READ_LOCK) {
	char buf[256];
	int trywrite;
	char* p;
	bool lastlock = true;
	int c;

	memset(buf, ' ', rd_lock_banner_len);

	locktype = TRYING_READ_LOCK;

	trywrite = write_lock_nowait();

	while(true) {
	    /* There are two PRCS specific warnings here, cause I'm
	       not sure how to handle trouble obtaining the lock
	       while unlocking a read lock */
	    if(trywrite == LOCK_SUCCESS) {
		break;
	    } else if(trywrite == LOCK_TRY_AGAIN) {
		prcsinfo << "Read lock modifier lock unavailable, waiting to unlock the repository"
			 << dotendl;
	    } else /* if(trywrite == LOCK_FAIL || trywrite == LOCK_TRY_STEAL) */ {
		prcserror << "Unrecoverable error preventing unlocking of the repository, "
		    "you can manually remove locks " << squote(wrlockname) << " and "
			  << squote(rdlockname) << dotendl;
		return false;
	    }
	    trywrite = write_lock_wait();
	}

	locktype = READ_LOCK;

	if((rdfd = open(rdlockname, O_RDWR, LOCK_FILE_MODE)) < 0) {
	    prcserror << "Open failed while unlocking read lock file "
		      << squote(rdlockname) << ", the lock may have been stolen" << perror;
	    return unlink_writelock();
	} else if(lseek(rdfd, rd_lock_banner_offset, SEEK_SET) < 0) {
	    prcserror << "Lseek failed unlocking lock file" << perror;
	    return unlink_writelock();
	} else if(write(rdfd, buf, rd_lock_banner_len - 1) < 0) {
	    prcserror << "Write failed unlocking lock file" << perror;
	    return unlink_writelock();
	} else if(lseek(rdfd, LOCK_LENGTH, SEEK_SET) < 0) {
	    prcserror << "Lseek failed unlocking lock file" << perror;
	    return unlink_writelock();
	}

	while (true) {
	    do {
		c = read(rdfd, buf, 256);
	    } while (c < 0 && errno == EINTR);

	    if (c <= 0) /* error ignored */
		break;

	    p = buf;
	    while(p < buf + c){
		if(!isspace(*p))
		    lastlock = false;
		p += 1;
	    }
	}

	if(close(rdfd) < 0) {
	    prcserror << "Close failed unlocking lock file" << perror;
	    return unlink_writelock();
	}

	if(lastlock && unlink(rdlockname) < 0 && errno != ENOENT) {
	    prcserror << "Failed removing lock fail " << squote(rdlockname) << perror;
	    return unlink_writelock();
	}

	locktype = WRITE_LOCK;

	if(unlink(wrlockname) < 0 && errno != ENOENT) {
	    prcserror << "Failed removing lock file " << squote(wrlockname) << perror;
	    return unlink_writelock();
	}
    }

    locktype = NO_LOCK;
    return true;
}

AdvisoryLock::AdvisoryLock(const char* wrlockname0)
    :wrfd(-1), rdfd(-1), locktype(NO_LOCK)
{
    wrlockname.assign(wrlockname0);
    wrlockname.append(".writers");
    rdlockname.assign(wrlockname0);
    rdlockname.append(".readers");
}

/*
 * common code to both implementations
 */
bool AdvisoryLock::unlink_writelock()
{
    unlink(wrlockname);
    return false;
}

bool AdvisoryLock::write_write_owner_notice()
{
    Dstring pid;

    if(ftruncate(wrfd, 0) < 0) {
	prcserror << "Ftruncate failed while write locking lock file "
		  << squote(wrlockname) << perror;
	return false;
    }

    if(lseek(wrfd, 0, SEEK_SET) < 0) {
	prcserror << "Lseek failed while write locking lock file "
		  << squote(wrlockname) << perror;
	return false;
    }

    if(write(wrfd, LOCK_HEADER, LOCK_LENGTH) < 0) {
	prcserror << "Write failed while write locking lock file "
		  << squote(wrlockname) << perror;
	return false;
    }

    age = time(NULL);

    pid.sprintf("write lock: %s %d %s %s\n", get_login(), getpid(),
		get_host_name(), time_t_to_rfc822(age));

    wr_lock_banner_offset = lseek(wrfd, 0, SEEK_CUR);
    wr_lock_banner_len = pid.length();

    if( write(wrfd, pid.cast(), pid.length()) != pid.length()) {
	prcserror << "Error recording write lock in lock file "
		  << squote(wrlockname) << perror;
	return false;
    }

    return true;
}

bool AdvisoryLock::write_read_owner_notice()
{
    Dstring pid;

    if(lseek(rdfd, 0, SEEK_END) < 0) {
	prcserror << "Lseek failed while read locking lock file "
		  << squote(rdlockname) << perror;
	return false;
    }

    age = time(NULL);

    pid.sprintf("read lock: %s %d %s %s\n", get_login(), getpid(),
		get_host_name(), time_t_to_rfc822(age));

    rd_lock_banner_offset = lseek(rdfd, 0, SEEK_CUR);
    rd_lock_banner_len = pid.length();

    if( write(rdfd, pid.cast(), pid.length()) != pid.length()) {
	prcserror << "Error recording read lock in lock file " << squote(rdlockname) << perror;
	return false;
    }

    return true;
}

bool AdvisoryLock::steal_lock()
{
    if(unlink(rdlockname) < 0 && errno != ENOENT) {
	prcserror << "Unlink failed while stealing lock" << perror;
	return false;
    }
    if(unlink(wrlockname) < 0 && errno != ENOENT) {
	prcserror << "Unlink failed while stealing lock" << perror;
	return false;
    }
    return true;
}

FILE* AdvisoryLock::who_is_locking()
{
    FILE* ret;
    if(fs_file_exists(rdlockname)) {
	ret = fopen(rdlockname, "r");
    } else {
	ret = fopen(wrlockname, "r");
    }

    if(ret) {
	if(fseek(ret, LOCK_LENGTH, 0) < 0) {
	    prcserror << "Seek error in lock file " << squote(wrlockname) << perror;
	    return NULL;
	}
	return ret;
    } else {
	prcserror << "Error opening lock file " << squote(wrlockname)
		  << " to get lockers" << perror;
	return NULL;
    }
}

int AdvisoryLock::touch_lock()
{
    if (locktype == READ_LOCK) {

	if ((rdfd = open (rdlockname, O_RDWR, LOCK_FILE_MODE)) < 0)
	    return LOCK_FAIL;

	if (lseek (rdfd, rd_lock_banner_offset, SEEK_SET) < 0)
	    return LOCK_FAIL;

	Dstring pid;

	age = time(NULL);

	pid.sprintf("read lock: %s %d %s %s\n", get_login(), getpid(),
		    get_host_name(), time_t_to_rfc822(age));

	if( write(rdfd, pid.cast(), pid.length()) != pid.length())
	    return LOCK_FAIL;

	if (close(rdfd) < 0)
	    return LOCK_FAIL;

    } else if (locktype == WRITE_LOCK) {

	if ((wrfd = open (wrlockname, O_RDWR, LOCK_FILE_MODE)) < 0)
	    return LOCK_FAIL;

	if (lseek (wrfd, wr_lock_banner_offset, SEEK_SET) < 0)
	    return LOCK_FAIL;

	Dstring pid;

	age = time(NULL);

	pid.sprintf("write lock: %s %d %s %s\n", get_login(), getpid(),
		    get_host_name(), time_t_to_rfc822(age));

	if( write(wrfd, pid.cast(), pid.length()) != pid.length())
	    return LOCK_FAIL;

	if ( close(wrfd) < 0)
	    return LOCK_FAIL;
    }

    return LOCK_SUCCESS;
}

int AdvisoryLock::write_lock_wait() { return write_lock(LOCK_WAIT); }
int AdvisoryLock::read_lock_wait() { return read_lock(LOCK_WAIT); }
int AdvisoryLock::write_lock_nowait() { return write_lock(LOCK_NOWAIT); }
int AdvisoryLock::read_lock_nowait() { return read_lock(LOCK_NOWAIT); }

#if 0

int main()
{
    AdvisoryLock lock1("lockfile");
    int lock1val;
    bool wait = true;
    bool write = true;
    int pid;
    long foo;

    setup_streams("lock");

    while(true) {
	wait = foo % 2;
	foo += random();
	write = foo % 2;
	foo += random();

	if((pid = fork()) == 0)
	    break;
    }

    pid = getpid();

    prcsout << "pid " << pid << " requests " << (write ? "write" : "read")
	    << " " << (wait ? "wait" : "nowait") << " lock" << dotendl;

    if(wait) {
	if(write)
	    lock1val = lock1.write_lock_wait();
	else
	    lock1val = lock1.read_lock_wait();
    } else {
	if(write)
	    lock1val = lock1.write_lock_nowait();
	else
	    lock1val = lock1.read_lock_nowait();
    }

    if(lock1val == LOCK_SUCCESS) {
	prcserror("got %s lock pid %d\n", (write? "write": "read"), pid);
	sleep(10);
    } else if(lock1val == LOCK_FAIL) {
	prcserror("%s lock failed pid %d\n",(write? "write": "read"), pid);
    } else if(lock1val == LOCK_TRY_STEAL) {
	prcserror("%s lock says try steal %d\n", (write? "write": "read"), pid);
    } else {
	prcserror("%s lock returns try again pid %d\n",(write? "write": "read"), pid);
    }

    lock1.unlock();
    prcserror("exiting %d\n", pid);
}

#endif
