/*
 * Copyright (C) 2003  Sam Horrocks
 * 
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

/* Open/create, mmap and lock the perperl temp file */

#include "perperl.h"

perperl_file_t		*perperl_file_maddr;
static int		file_fd = -1;
static int		maplen;
static int		file_locked;
static char		*file_name, *saved_tmpbase;
static struct stat	file_stat;
static int		cur_state;
static time_t		last_reopen;
#ifdef PERPERL_BACKEND
static int		fd_is_suspect;
#endif

#define fillin_fl(fl)		\
    fl.l_whence	= SEEK_SET;	\
    fl.l_start	= 0;		\
    fl.l_len	= 0

static void file_unmap(void) {
    if (maplen) {
#	if defined(__APPLE__) && defined(MS_INVALIDATE)
	    /*
	     * This makes Mac OS-X 10.1 pass all the tests, where it was failing
	     * alarm/2 and others intermittently.  The problem seems to happen
	     * when the temp file is expanded, and might be due to some
	     * memory flushing problem in the OS, but I can't isolate it.
	     * This change might slow things down due to more disk i/o.
	     *
	     * Reproduce the bug by removing the perperl temp file and running:
	     *   print "$$";
	     * twice.  The first backend will die, and the second run will
	     * output a different pid.
	     */
	    msync(perperl_file_maddr, maplen, MS_INVALIDATE);
#	endif
	(void) munmap((void*)perperl_file_maddr, maplen);
	perperl_file_maddr = 0;
	maplen = 0;
    }
}

static void file_map(unsigned int len) {
    if (maplen != len) {
	file_unmap();
	maplen = len;
	if (len) {
	    perperl_file_maddr = (perperl_file_t*)mmap(
		0, len, PROT_READ | PROT_WRITE, MAP_SHARED, file_fd, 0
	    );
	    if (perperl_file_maddr == (perperl_file_t*)MAP_FAILED)
		perperl_util_die("mmap failed");
	}
    }
}

static void file_unlock(void) {
    struct flock fl;

    if (!file_locked)
	return;

    FILE_HEAD.lock_owner = 0;

    fillin_fl(fl);
    fl.l_type = F_UNLCK;
    if (fcntl(file_fd, F_SETLK, &fl) == -1) perperl_util_die("unlock file");
    file_locked = 0;

    perperl_sig_blockall_undo();
}

/* Only call this if you're sure the fd is not suspect */
static void file_close2(void) {

#ifdef PERPERL_BACKEND
    if (fd_is_suspect)
	DIE_QUIET("file_close2: assertion failed - fd_is_suspect");
#endif

    file_unlock();
    file_unmap();
    if (file_fd != -1) {
	(void) close(file_fd);
	file_fd = -1;
    }
}


#ifdef PERPERL_BACKEND
PERPERL_INLINE void perperl_file_fd_is_suspect(void) {
    fd_is_suspect = 1;
}

static void fix_suspect_fd(void) {
    if (fd_is_suspect) {
	if (file_fd != -1) {
	    struct stat stbuf;

	    if (fstat(file_fd, &stbuf) == -1 ||
		stbuf.st_dev != file_stat.st_dev ||
		stbuf.st_ino != file_stat.st_ino)
	    {
		file_unmap();
		file_fd = -1;
	    }
	}
	fd_is_suspect = 0;
    }
}
#endif


#define get_stat() \
    if (fstat(file_fd, &file_stat) == -1) perperl_util_die("fstat")

static void remove_file(int is_corrupt) {
#ifdef PERPERL_DEBUG
    if (is_corrupt) {
	/* Keep the file for debugging */
	char newname[200];
	struct timeval tv;

	gettimeofday(&tv, NULL);
	sprintf(newname, "%s.corrupt.%d.%06d.%d",
	    file_name, (int)tv.tv_sec, (int)tv.tv_usec, getpid());
	if (rename(file_name, newname) == -1)
	    perperl_util_die("rename temp file");
	FILE_HEAD.file_removed = 1;
	DIE_QUIET("temp file corrupt");
    }
#endif
    if (unlink(file_name) == -1 && errno != ENOENT)
	perperl_util_die("unlink temp file");
    FILE_HEAD.file_removed = 1;
}

static void str_replace(char **ptr, char *newval) {
    if (*ptr)
	perperl_free(*ptr);
    *ptr = newval;
}

static void file_lock(void) {
    static struct timeval file_create_time;
    struct flock fl;
    int tries;
    time_t now;

    if (file_locked)
	return;

#ifdef PERPERL_BACKEND
    fix_suspect_fd();
#endif

    /* Re-open the temp file occasionally or if tmpbase changed */
    if ((now = perperl_util_time()) - last_reopen > OPTVAL_RESTATTIMEOUT ||
	!saved_tmpbase || strcmp(saved_tmpbase, OPTVAL_TMPBASE) != 0)
    {
	last_reopen = now;
	file_close2();
    }

    for (tries = 5; tries; --tries) {
	/* If file is not open, open it */
	if (file_fd == -1) {
	    str_replace(&saved_tmpbase, perperl_util_strdup(OPTVAL_TMPBASE));
	    str_replace(&file_name, perperl_util_fname(FILE_REV, 'F'));
	    file_fd = perperl_util_pref_fd(
		open(file_name, O_RDWR | O_CREAT, 0600), PREF_FD_FILE
	    );
	    if (file_fd == -1) perperl_util_die("open temp file");
	    fcntl(file_fd, F_SETFD, FD_CLOEXEC);
	}

	/* Lock the file */
	fillin_fl(fl);
	fl.l_type = F_WRLCK;
	if (fcntl(file_fd, F_SETLKW, &fl) == -1) perperl_util_die("lock file");

	/* Fstat the file, now that it's locked down */
	get_stat();

	/* Map into memory */
	file_map(file_stat.st_size);

	/* If file is too small (0 or below MIN_SLOTS_FREE), extend it */
	if (file_stat.st_size < sizeof(file_head_t) ||
	    file_stat.st_size < sizeof(file_head_t) +
		sizeof(slot_t) * (FILE_HEAD.slots_alloced + MIN_SLOTS_FREE))
	{
	    if (ftruncate(file_fd, file_stat.st_size + FILE_ALLOC_CHUNK) == -1)
		perperl_util_die("ftruncate");
	    get_stat();
	    file_map(file_stat.st_size);
	}

	/* Initialize file's create time if necessary */
	if (!FILE_HEAD.create_time.tv_sec)
	    perperl_util_gettimeofday(&(FILE_HEAD.create_time));
	
	/* Initialize our copy of the create-time if necessary */
	if (!file_create_time.tv_sec || cur_state < FS_HAVESLOTS) {
	    file_create_time = FILE_HEAD.create_time;
	}
	/* Check whether this file is a different version  */
	else if ((file_create_time.tv_sec  != FILE_HEAD.create_time.tv_sec ||
	          file_create_time.tv_usec != FILE_HEAD.create_time.tv_usec))
	{
	    remove_file(1);
	}

	/* If file is corrupt (didn't finish all writes), remove it */
	if (FILE_HEAD.lock_owner)
	    remove_file(1);

	/* If file has not been removed then all done */
	if (!FILE_HEAD.file_removed)
	    break;

	/* File is invalid */
	if (cur_state >= FS_HAVESLOTS) {
	    /* Too late for this proc - slotnums have changed, can't recover */
	    DIE_QUIET("temp file is corrupt");
	} else {
	    /* Bad luck - the file was unlinked after we opened it (possibly
	     * by us because it was corrupt), but before we locked it.
	     * Try again.
	     */
	    file_close2();
	}
    }
    if (!tries) {
	DIE_QUIET("could not open temp file");
    }

    /* Block all sigs while writing to file */
    perperl_sig_blockall();
    file_locked = 1;
    FILE_HEAD.lock_owner = perperl_util_getpid();
}

static void file_close(void) {
    /* If no groups left, remove the file */
    if (cur_state >= FS_HAVESLOTS) {
	file_lock();
	if (!FILE_HEAD.group_head && !FILE_HEAD.fe_run_head)
	    remove_file(0);
    }
    file_close2();
}

int perperl_file_size(void) {
    return maplen;
}

static void switch_state(int new_state) {
    switch(new_state) {
    case FS_CLOSED:
	file_close();
	break;
    case FS_OPEN:
	file_unlock();
	break;
    case FS_HAVESLOTS:
	file_unlock();
	break;
    case FS_CORRUPT:
	file_lock();
	break;
    }
}

PERPERL_INLINE int perperl_file_set_state(int new_state) {
    int retval = cur_state;

    if (new_state != cur_state) {
	switch_state(new_state);
	cur_state = new_state;
    }
    return retval;
}

void perperl_file_fork_child(void) {
    if (file_locked)
	perperl_sig_blockall_undo();
    file_locked = 0;
    if (cur_state > FS_HAVESLOTS)
	perperl_file_set_state(FS_HAVESLOTS);
}

#ifdef PERPERL_BACKEND
void perperl_file_need_reopen(void) {
    last_reopen = 0;
}
#endif