#include "system.h"

#include <rpmio.h>
#include <rpmcb.h>		/* XXX fnpyKey */
#include <rpmmacro.h>
#include <rpmlib.h>

#include "rpmts.h"
#include "rpmlock.h"

#include "debug.h"

/*@unchecked@*/ /*@observer@*/
static const char * rpmlock_path_default = "%{?_rpmlock_path}";
/*@unchecked@*/ /*@only@*/ /*null@*/
static const char * rpmlock_path = NULL;

enum {
    RPMLOCK_READ   = 1 << 0,
    RPMLOCK_WRITE  = 1 << 1,
    RPMLOCK_WAIT   = 1 << 2,
};

typedef struct {
    int fd;
    int omode;
} * rpmlock;

/*@null@*/
static int rpmlock_new(/*@unused@*/ const char *rootdir, /*@null@*/ rpmlock *lockp)
	/*@globals rpmlock_path, rpmGlobalMacroContext, h_errno,
		fileSystem, internalState @*/
	/*@modifies *lockp, rpmlock_path, rpmGlobalMacroContext, h_errno,
		fileSystem, internalState @*/
{
    static int oneshot = 0;
    rpmlock lock = xmalloc(sizeof(*lock));
    int rc = -1;

    if (lockp)
	*lockp = NULL;

    /* XXX oneshot to determine path for fcntl lock. */
    /* XXX rpmlock_path is set once, cannot be changed with %{_rpmlock_path}. */
    if (!oneshot) {
	const char * t = NULL;
	char * check = rpmExpand(rpmlock_path_default, NULL);
	if (check[0] != '\0')
	    t = rpmGenPath(rootdir, rpmlock_path_default, NULL);
	check = _free(check);

	if (t == NULL || *t == '\0' || *t == '%')
	    t = _free(t);
	rpmlock_path = t;
	oneshot++;
    }

    if (rpmlock_path != NULL) {
	mode_t oldmask = umask(022);
	lock->fd = open(rpmlock_path, O_RDWR|O_CREAT, 0644);
	(void) umask(oldmask);

	if (lock->fd == -1) {
	    lock->fd = open(rpmlock_path, O_RDONLY);
	    if (lock->fd == -1)
		rc = -1;
	    else
		lock->omode = RPMLOCK_READ;
	} else
	    lock->omode = RPMLOCK_WRITE | RPMLOCK_READ;
	rc = 0;
    }
    if (rc == 0 && lockp != NULL)
	*lockp = lock;
    else
	lock = _free(lock);
/*@-compdef@*/ /*@-nullstate@*/ /*@-globstate@*/
    return rc;
/*@=compdef@*/ /*@=nullstate@*/ /*@=globstate@*/
}

static void * rpmlock_free(/*@only@*/ /*@null@*/ rpmlock lock)
	/*@globals fileSystem, internalState @*/
	/*@modifies lock, fileSystem, internalState @*/
{
    if (lock != NULL) {
	if (lock->fd > 0)
	    (void) close(lock->fd);
	lock = _free(lock);
    }
    return NULL;
}

static int rpmlock_acquire(/*@null@*/ rpmlock lock, int mode)
	/*@*/
{
    int rc = 0;

    if (lock != NULL && (mode & lock->omode)) {
	struct flock info;
	int cmd;
	if (mode & RPMLOCK_WAIT)
	    cmd = F_SETLKW;
	else
	    cmd = F_SETLK;
	if (mode & RPMLOCK_READ)
	    info.l_type = F_RDLCK;
	else
	    info.l_type = F_WRLCK;
	info.l_whence = SEEK_SET;
	info.l_start = 0;
	info.l_len = 0;
	info.l_pid = 0;
	if (fcntl(lock->fd, cmd, &info) != -1)
	    rc = 1;
    }
    return rc;
}

static int rpmlock_release(/*@null@*/ rpmlock lock)
	/*@globals internalState @*/
	/*@modifies internalState @*/
{
    int rc = 0;

    if (lock != NULL) {
	struct flock info;
	info.l_type = F_UNLCK;
	info.l_whence = SEEK_SET;
	info.l_start = 0;
	info.l_len = 0;
	info.l_pid = 0;
	rc = fcntl(lock->fd, F_SETLK, &info);
    }
    return rc;
}

void *rpmtsAcquireLock(rpmts ts)
{
    const char *rootDir = rpmtsRootDir(ts);
    rpmlock lock = NULL;
    int rc;

    if (rootDir == NULL || rpmtsChrootDone(ts))
	rootDir = "/";
    rc = rpmlock_new(rootDir, &lock);
    if (rc) {
	if (rpmlock_path != NULL && strcmp(rpmlock_path, rootDir))
	    rpmlog(RPMLOG_ERR,
		_("can't create transaction lock on %s (%s)\n"),
		rpmlock_path, strerror(errno));
    } else if (lock != NULL) {
	if (!rpmlock_acquire(lock, RPMLOCK_WRITE)) {
	    if (lock->omode & RPMLOCK_WRITE)
		rpmlog(RPMLOG_WARNING,
		   _("waiting for transaction lock on %s\n"), rpmlock_path);
	    if (!rpmlock_acquire(lock, RPMLOCK_WRITE|RPMLOCK_WAIT)) {
		if (rpmlock_path != NULL && strcmp(rpmlock_path, rootDir))
		    rpmlog(RPMLOG_ERR,
			_("can't create transaction lock on %s (%s)\n"),
			rpmlock_path, strerror(errno));
		lock = rpmlock_free(lock);
	    }
	}
    }
/*@-globstate@*/	/* rpmlock_path may be NULL. */
    return lock;
/*@=globstate@*/
}

void * rpmtsFreeLock(void *lock)
{
    (void) rpmlock_release((rpmlock)lock); /* Not really needed here. */
    lock = rpmlock_free((rpmlock)lock);
    return NULL;
}
