/*
 * Copyright (C) 2002  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.
 *
 */

#include "perperl.h"

void perperl_frontend_dispose(slotnum_t gslotnum, slotnum_t fslotnum) {
    if (fslotnum) {
	gr_slot_t *gslot = &FILE_SLOT(gr_slot, gslotnum);

	perperl_slot_remove(fslotnum, &(gslot->fe_head), &(gslot->fe_tail));
	SLOT_FREE(fslotnum, "frontend (perperl_frontend_dispose)");
    }
}

void perperl_frontend_remove_running(const slotnum_t fslotnum) {
    fe_slot_t *fslot = &FILE_SLOT(fe_slot, fslotnum);

    if (fslot->backend) {
	be_slot_t *bslot = &FILE_SLOT(be_slot, fslot->backend);
	if (bslot->fe_running == fslotnum)
	    bslot->fe_running = fslot->backend;
    }
    perperl_slot_remove(fslotnum, &(FILE_HEAD.fe_run_head), &(FILE_HEAD.fe_run_tail));
    SLOT_FREE(fslotnum, "frontend (remove_running)");
}

#ifdef PERPERL_FRONTEND

int perperl_frontend_collect_status
    (const slotnum_t fslotnum, int *exit_on_sig, int *exit_val)
{
    fe_slot_t *fslot = &FILE_SLOT(fe_slot, fslotnum);

    if (fslot->backend && perperl_backend_dead(fslot->backend))
	perperl_backend_died(fslot->backend);

    if (fslot->backend == 0) {
	*exit_on_sig = fslot->exit_on_sig;
	*exit_val = fslot->exit_val;
	perperl_frontend_remove_running(fslotnum);
	return 1;
    }
    return 0;
}

void perperl_frontend_clean_running(void) {
    /* See if we can kill some dead frontends in the fe_run list */
    while (FILE_HEAD.fe_run_tail && perperl_frontend_dead(FILE_HEAD.fe_run_tail))
	perperl_frontend_remove_running(FILE_HEAD.fe_run_tail);
}


/*
 * Signal handling routines
 */

#define NUMSIGS (sizeof(signum) / sizeof(int))

static const int	signum[] = {SIGALRM};
static char		sig_setup_done;
static time_t		next_alarm;
static SigList		sl;

static void sig_handler_teardown(int put_back_alarm) {

    if (!sig_setup_done)
	return;
    
    alarm(0);

    perperl_sig_free(&sl);

    /* Put back alarm */
    if (put_back_alarm && next_alarm) {
	next_alarm -= perperl_util_time();
	alarm(next_alarm > 0 ? next_alarm : 1);
    }

    sig_setup_done = 0;
}

static void sig_handler_setup(void) {
    sig_handler_teardown(1);

    /* Save alarm for later */
    if ((next_alarm = alarm(0))) {
	next_alarm += perperl_util_time();
    }

    perperl_sig_init(&sl, signum, NUMSIGS, SIG_BLOCK);

    sig_setup_done = 1;
}

/*
 * End of Signal handling routines
 */

#define BE_SUFFIX "_backend"

/* Spawn the be_parent process */
static void be_parent_spawn(slotnum_t gslotnum) {
    int pid;
    const char * const *argv;

    /* Get args for exec'ing backend */
    argv = perperl_opt_exec_argv();

    /* Fork */
    pid = fork();

    if (pid > 0) {
	/* Parent */

	int child_status;

	if (waitpid(pid, &child_status, 0) == -1)
	    perperl_util_die("wait");
    }
    else if (pid == 0) {
	/* Child */

	/* Get rid of alarm handler and any alarms */
	sig_handler_teardown(0);

	/* Unblock any signals due to file lock */
	perperl_file_fork_child();

	/* Fork again */
	pid = fork();

	if (pid == -1) {
	    perperl_util_exit(1,1);
	}
	else if (pid) {
	    /* Parent of Grandchild */

	    /* We don't hold the lock on the temp file, but our parent does,
	     * and it's waiting for us to exit before proceeding, so it's
	     * safe to write to the file here
	     */
	    FILE_SLOT(gr_slot, gslotnum).be_parent = pid;
	    FILE_SLOT(gr_slot, gslotnum).be_starting = pid;

	    perperl_util_exit(0,1);
	}
	else {
	    /* Grandchild */

	    /* We should be in our own session */
	    setsid();

	    /* Exec the backend */
	    perperl_util_execvp(argv[0], argv);

	    /* Failed.  Try the original argv[0] + "_backend" */
	    {
		const char *orig_file = perperl_opt_orig_argv()[0];
		if (orig_file && *orig_file) {
		    char *fname;
		    
		    perperl_new(
			fname, strlen(orig_file)+sizeof(BE_SUFFIX)+1, char
		    );
		    sprintf(fname, "%s%s", orig_file, BE_SUFFIX);
		    perperl_util_execvp(fname, argv);
		}
	    }
	    perperl_util_die(argv[0]);
	}
    } else {
	perperl_util_die("fork");
    }
}

/* Check on / spawn backends.  Should only be done by the fe at the
 * head of the list (think 100+ fe's in the queue)
 */
static int backend_check(slotnum_t gslotnum, int *did_spawn) {
    gr_slot_t *gslot = &FILE_SLOT(gr_slot, gslotnum);

    /* Don't spawn a backend while a backend is starting */
    if (perperl_group_be_starting(gslotnum))
	return 1;

    /* If we already did this once, it didn't work */
    if (*did_spawn)
	return 0;

    /* Start up a be_parent if necessary */
    if (!gslot->be_parent)
	be_parent_spawn(gslotnum);

    /* Are we below the maxbackends limit? */
    if (perperl_backend_below_maxbe(gslotnum)) {

	/* Signal the be parent to start a new backend */
	if (perperl_group_start_be(gslotnum)) {
	    /* Let it start one before spawning again */
	    gslot->be_starting = gslot->be_parent;
	    *did_spawn = 1;
	}
    } else {
	/* If we're above the maxbaceknds limit, we still need to ping the
	 * be parent to make sure it's alive.
	 */
	perperl_group_parent_sig(gslotnum, 0);
    }
    return 1;
}

/* Go up the fe list, going to the next group if we're at the
 * begininng of the list.  Wrap to the first group if we go off the end
 * of the group list.  Worst case we wrap around and return ourself.
 */
static void fe_prev(slotnum_t *gslotnum, slotnum_t *fslotnum) {
    *fslotnum = perperl_slot_prev(*fslotnum);
    while (!*fslotnum) {
	if (!(*gslotnum = perperl_slot_next(*gslotnum)) &&
	    !(*gslotnum = FILE_HEAD.group_head))
	{
	    DIE_QUIET("Group list or frontend lists are corrupt");
	}
	*fslotnum = FILE_SLOT(gr_slot, *gslotnum).fe_tail;
    }
}

static void frontend_check_prev(slotnum_t gslotnum, slotnum_t fslotnum) {
    fe_prev(&gslotnum, &fslotnum);

    while (perperl_frontend_dead(fslotnum)) {
	slotnum_t g_prev = gslotnum, f_prev = fslotnum;

	/* Must do "prev" function while this slot/group is still valid */
	fe_prev(&g_prev, &f_prev);

	/* This frontend is not running so dispose of it */
	perperl_frontend_dispose(gslotnum, fslotnum);

	/* Try to remove this group if possible */
	perperl_group_cleanup(gslotnum);

	/* If we wrapped around to ourself, then all done */
	if (f_prev == fslotnum)
	    break;

	gslotnum = g_prev;
	fslotnum = f_prev;
    }
}

/* Check that the frontend in front of is running.  Also run backend check
 * if we are the head frontend
 */
static int frontend_ping
    (slotnum_t gslotnum, slotnum_t fslotnum, int *did_spawn)
{
    /* Check the frontend previous to us.  This may remove it */
    frontend_check_prev(gslotnum, fslotnum);

    /* If we're not the head of the list, then all done */
    if (perperl_slot_prev(fslotnum))
	return 1;

    /* Do a check of backends.  Returns false if we cannot start be */
    return backend_check(gslotnum, did_spawn);
}


/* Get a backend the hard-way - by queueing up
*/
static int get_a_backend_hard
    (slotnum_t gslotnum, slotnum_t fslotnum, slotnum_t *bslotnum)
{
    int file_changed, did_spawn = 0, spawn_working = 1, sent_sig;
    *bslotnum = 0;

    /* Install sig handlers */
    sig_handler_setup();

    /* Put ourself at the end of the fe queue */
    perperl_slot_append(fslotnum,
	&(FILE_SLOT(gr_slot, gslotnum).fe_head),
	&(FILE_SLOT(gr_slot, gslotnum).fe_tail));

    while (1) {
	/* Send signals to frontends */
	perperl_group_sendsigs(gslotnum);

	sent_sig = FILE_SLOT(fe_slot, fslotnum).sent_sig;
	FILE_SLOT(fe_slot, fslotnum).sent_sig = 0;

	/* If our sent_sig flag is set, and there are be's for us to use ,
	 * then all done.
	*/
	if (sent_sig &&
	    (*bslotnum = perperl_backend_be_wait_get(gslotnum)))
	{
	    break;
	}

	/* Check on frontends/backends running */
	spawn_working = frontend_ping(gslotnum, fslotnum, &did_spawn);

	/* Frontend ping may have invalidated our group */
	if (!spawn_working || !perperl_group_isvalid(gslotnum))
	    break;

	/* Unlock the file */
	perperl_file_set_state(FS_HAVESLOTS);

	/* Set an alarm for one-second or so. */
	alarm(OPTVAL_BECHECKTIMEOUT);

	/* Wait for a timeout or signal from backend */
	perperl_sig_wait(&sl);

	/* Find out if our file changed.  Do this while unlocked */
	file_changed = perperl_script_changed();

	/* Acquire lock.  If group bad or file changed, then done */
	if (!perperl_group_lock(gslotnum) || file_changed)
	    break;
    }

    /* Remove our FE slot from the queue.  */
    perperl_slot_remove(fslotnum,
	&(FILE_SLOT(gr_slot, gslotnum).fe_head),
	&(FILE_SLOT(gr_slot, gslotnum).fe_tail));

    /* Put sighandlers back to their original state */
    sig_handler_teardown(1);

    return spawn_working;
}

static int get_a_backend(slotnum_t fslotnum, slotnum_t *gslotnum) {
    slotnum_t bslotnum = 0;
    int spawn_working = 1;

    /* Locate the group for our script */
    *gslotnum = perperl_script_find();

    /* Try to quickly grab a backend without queueing */
    if (!FILE_SLOT(gr_slot, *gslotnum).fe_head)
	bslotnum = perperl_backend_be_wait_get(*gslotnum);

    /* If that failed, use the queue */
    if (!bslotnum)
	spawn_working = get_a_backend_hard(*gslotnum, fslotnum, &bslotnum);
    
    /* Clean up the group if necessary */
    perperl_group_cleanup(*gslotnum);

    FILE_SLOT(fe_slot, fslotnum).backend = bslotnum;
    return spawn_working;
}


int perperl_frontend_connect(int socks[NUMFDS], slotnum_t *fslotnum_p) {
    static int did_clean;
    int connected = 0, spawn_working = 1, sockets_open = 0;

    /* May need options from the #! line in the script.  This also
     * opens the script file
     */
    perperl_opt_read_shbang();

    while (spawn_working && !connected) {
	slotnum_t gslotnum, bslotnum, fslotnum;

	/* Create sockets in preparation for connect.  This may take a while,
	 * esp on FreeBSD, when it's out of sockets.
	 */
	if (!sockets_open++)
	    perperl_ipc_connect_prepare(socks);

	/* Lock temp file */
	perperl_file_set_state(FS_CORRUPT);

	/* Need to clean out the fe_run list, once per frontend execution */
	if (!did_clean++)
	    perperl_frontend_clean_running();

	/* Allocate a frontend slot */
	fslotnum = SLOT_ALLOC("frontend (perperl_frontend_connect)");
	FILE_SLOT(fe_slot, fslotnum).pid = perperl_util_getpid();

	/* Try to find a backend.  Bad return status if cannot spawn */
	spawn_working = get_a_backend(fslotnum, &gslotnum);

	/* Did we get a backend slot to connect to? */
	if (spawn_working && (bslotnum = FILE_SLOT(fe_slot, fslotnum).backend))
	{
	    /* Try to connect to this backend. */
	    connected = perperl_ipc_connect(bslotnum, socks);

	    if (!connected) {
		/* Failed to connect */
		sockets_open = 0;

		/* Make sure to get rid of backend record */
		perperl_backend_dispose(gslotnum, bslotnum);
	    }
	} else {
	    connected = 0;
	}

	if (fslotnum_p)
	    *fslotnum_p = 0;

	if (connected) {
	    be_slot_t *bslot = &FILE_SLOT(be_slot, bslotnum);

	    /* See if caller wants to hold onto fslot for exit status */
	    if (fslotnum_p) {
		*fslotnum_p = fslotnum;

		/* Link our frontend to that backend */
		bslot->fe_running = fslotnum;

		/* Add our frontend to the list of running fe's */
		perperl_slot_insert(fslotnum, &(FILE_HEAD.fe_run_head), &(FILE_HEAD.fe_run_tail));
	    } else {
		/* Fe_running must be non-zero while backend is running */
		bslot->fe_running = bslotnum;
	    }

	    /* Prevent further spawns until this backend starts to run */
	    FILE_SLOT(gr_slot, gslotnum).be_starting = bslot->pid;
	}

	if (fslotnum_p && *fslotnum_p) {
	    perperl_file_set_state(FS_HAVESLOTS);
	} else {
	    /* Jettison this frontend */
	    SLOT_FREE(fslotnum, "frontend (perperl_frontend_connect)");
	    perperl_file_set_state(FS_OPEN);
	}
    }
    if (sockets_open && !connected) {
	int i;
	for (i = 0; i < NUMFDS; ++i)
	    close(socks[i]);
    }
    perperl_script_close();
    return spawn_working;
}

/* Return size of the buffer needed to send a string of the given length */
#define STR_BUFSIZE(l) (1 + (l >= MAX_SHORT_STR ? sizeof(int) : 0) + l)

/* Add something to the buffer */
#define BUF_ENLARGE(b,l) \
    if ((b)->len + (l) > (b)->alloced) \
	enlarge_buf((b),(l))

#define BUF_REALLOC(b,sz) \
    do { \
	int newsz = (sz); \
	(b)->alloced = newsz; \
	perperl_renew((b)->buf, newsz, char); \
    } while (0)

#define ADD2(b,s,l) \
    perperl_memcpy((b)->buf + (b)->len, (s), (l)); \
    (b)->len += (l)

#define ADD(b,s,l) BUF_ENLARGE(b,l); ADD2(b,s,l)

#define ADDCHAR2(b,c) ((b)->buf)[(b)->len++] = (char)c
    
#define ADDCHAR(b,c) BUF_ENLARGE(b,1); ADDCHAR2(b,c)

#define ADD_DEVINO(b,stbuf) \
    do { \
	PersistentDevIno devino = perperl_util_stat_devino(stbuf); \
	ADD((b), &devino, sizeof(PersistentDevIno)); \
    } while (0)

#define ADD_STRING(b, s, l) \
    do { \
	if ((l) >= MAX_SHORT_STR) { \
	    BUF_ENLARGE(b, (sizeof(int)+1)); \
	    ADDCHAR2(b, MAX_SHORT_STR); \
	    ADD2(b, &(l), sizeof(int)); \
	} else { \
	    ADDCHAR(b, l); \
	} \
	ADD(b, s, l); \
    } while (0)

static void enlarge_buf(PersistentBuf *b, int min_to_add) {
    int new_size = b->alloced * 2;
    new_size = max(new_size, b->len + min_to_add);
    BUF_REALLOC(b, new_size);
}

static void alloc_buf(PersistentBuf *b, int bytes) {
    b->alloced = bytes;
    perperl_new(b->buf, bytes, char);
    b->len = 0;
}

/* Add a string to the buffer */
static void add_string(PersistentBuf *b, const char *s, int l) {
    ADD_STRING(b, s, l);
}

/* Copy a block of strings into the buffer,  */
/* Profiling shows this is the top function for cpu time */
static void add_strings(register PersistentBuf *b, register const char * const * p)
{
    int l;
    register const char *s;

    /* Add strings in p array */
    for (; (s = *p); ++p) {
	if ((l = strlen(s))) {
	    ADD_STRING(b, s, l);
	}
    }

    /* Terminate with zero-length string */
    ADDCHAR(b, 0);
}

void perperl_frontend_mkenv(
    const char * const * envp, const char * const * scr_argv, int min_alloc,
    PersistentBuf *sb, int script_has_cwd
)
{
    struct stat dir_stat;
    const char *script_fname = perperl_opt_script_fname();

    if (!script_fname)
	perperl_script_missing();

    /* Create buffer */
    alloc_buf(sb, max(512, min_alloc));

    /* Add env and argv */
    add_strings(sb, envp);
    add_strings(sb, scr_argv+1);

    /* Put script filename into buffer */
    add_string(sb, script_fname, strlen(script_fname));

    /* Put script device/inode into buffer */
    ADD_DEVINO(sb, perperl_script_getstat());

    /* Handle passing over cwd */
    if (script_has_cwd) {
	ADDCHAR(sb, PERPERL_CWD_IN_SCRIPT);
    }
    else if (stat(".", &dir_stat) != -1) {
	ADDCHAR(sb, PERPERL_CWD_DEVINO);
	ADD_DEVINO(sb, &dir_stat);
    } else {
	ADDCHAR(sb, PERPERL_CWD_UNKNOWN);
    }
}

void perperl_frontend_proto2(int err_sock, int first_byte) {
    int n, cwd_len, buflen;
    char *bp, *cwd;
    PollInfo pi;
    PersistentBuf b;

    if (!first_byte)
	return;

    /* Get current directory */
    cwd = perperl_util_getcwd();
    cwd_len = cwd ? strlen(cwd) : 0;

    /* Create buffer for the string */
    alloc_buf(&b, STR_BUFSIZE(cwd_len));

    /* Put cwd into the buffer */
    if (cwd) {
	add_string(&b, cwd, cwd_len);
	perperl_free(cwd);
    } else {
	add_string(&b, "", 0);
    }

    /* Send it over */
    perperl_poll_init(&pi, err_sock);
    bp = b.buf;
    buflen = b.len;
    while (1) {

	/* TEST - send over one byte at a time to test the poll */
	/* n = write(err_sock, bp, 1); */

	n = write(err_sock, bp, buflen);
	if (n == -1 && SP_NOTREADY(errno))
	    n = 0;
	if (n == -1)
	    break;

	if (!(buflen -= n))
	    break;
	bp += n;

	/* Do this instead of bothering to change socket to non-blocking */
	perperl_poll_quickwait(&pi, err_sock, PERPERL_POLLOUT, 1000);
    }
    perperl_poll_free(&pi);
    perperl_free(b.buf);

    shutdown(err_sock, 1);
}

#endif /* PERPERL_FRONTEND */
