#include "args.h"
#include "config.h"
/*
 * Copyright (c) 1986, 2014 by The Trustees of Columbia University in
 * the City of New York.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  + Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 *  + Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 *  + Neither the name of Columbia University nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 */

/*
 * exit.c - various ways to exit mm
 */

#include "mm.h"
#include "parse.h"
#include "cmds.h"

static int askem ARGS((int tty, char *pr, int prlen));
static int do_wait ARGS((int pid, int blocking));
static void forget_pid ARGS((unsigned n));
static void squish ARGS((msgvec *mail));

extern int expunge_on_bye;
extern int OLD_UMASK;

int
#if HAVE_STDC
cmd_quit (int n)
#else /* K&R style */
cmd_quit (n)
int n;
#endif /* HAVE_STDC */
{
    int i,updfl;

    confirm ();
    if (mode & MM_SEND) {		/* if sending, quit that mode */
	mode &= ~(MM_SEND|MM_ANSWER);
	return true;
    }
    if (mode & MM_READ) {		/* if reading, quit that mode */
	mode &= ~MM_READ;
	return true;
    }
    if (suspend_on_quit && (n == CMD_QUIT))  {
	suspend(UPD_SAVEMOD);
    }
    else {				/* qquit/bye */
	if (cf != NULL &&
	    (!(cf->flags & MF_RDONLY) || (cf->flags & MF_SAVEONLY))) {
	    /* if deleted messages, ask about expunging */
	    switch (expunge_on_bye) {
	    case SET_NO:
		updfl = UPD_SAVEMOD;
		break;
	    case SET_YES:
		updfl = UPD_SAVEMOD|UPD_EXPUNGE;
		break;
	    default:			/* SET_ASK */
		updfl = UPD_SAVEMOD;
		for (i = 1; i <= cf->count; i++)
		    if (cf->msgs[i].flags & M_DELETED) {
			if (yesno ("Expunge deleted messages? ", "yes"))
			    updfl |= UPD_EXPUNGE;
			break;
		    }
		break;
	    }
	    cmtend ();			/* let go of the terminal */
	    if (!update (&cf,updfl) && (cf->flags & MF_WRITERR)) {
		fprintf (stderr, "Cannot save file, not quitting.\n");
		fprintf (stderr,
"Use the SHELL or SUSPEND command to temporarily exit MM, free up some\n\
space, and then try again.\n");
		cmtset ();		/* set CCMD up again */
		cmcsb._cmwrp = autowrap_column;	/* put that back */
		return (false);
	    }
	}
	else
	    cmtend ();			/* let go of the terminal */

	if (keep_tmp_files)
	  clean_edit_tempfiles();

#ifdef MDEBUG
	m_done();
#endif /* MDEBUG */
#ifdef USAGEFILE
	usage_stop(USAGEFILE);
#endif
	exit (0);			/* quit entirely */
    }
    return (false);			/*NOTREACHED*/
}

int
#if HAVE_STDC
cmd_exit (int n)
#else /* K&R style */
cmd_exit (n)
int n;
#endif /* HAVE_STDC */
{
    confirm ();
    if (!suspend_on_exit) {
	cmtend ();
	if (cf != NULL)			/* expunge while saving */
	    if (!update (&cf,UPD_EXPUNGE|UPD_SAVEMOD) &&
		(cf->flags & MF_WRITERR)) {
		fprintf (stderr, "Cannot save file, not exiting.\n\
Try suspending MM, and making space by deleting some files.\n");
		cmtset ();		/* set CCMD up again */
		cmcsb._cmwrp = autowrap_column;	/* put that back */
		return (false);
	    }

	done (0);
    }
    else {
	if (mode == MM_TOP_LEVEL) {
#ifdef COMMENT
	    /* squish after we come back from suspend */
	    /* note that the return code from suspend comes from update */
	    /* and if the update failed we don't want to squish */
	    if (suspend(UPD_EXPUNGE|UPD_SAVEMOD) &&
		(cf != NULL))
		squish (cf);		/* free deleted messages */
#else
/* beebe */
	    suspend(UPD_EXPUNGE|UPD_SAVEMOD); /* squish on return */
	    if (cf != NULL)		/* is it empty? */
		squish (cf);		/* no, free deleted messages */
#endif	/* COMMENT */
	}
	else
	    suspend(0);			/* same as suspend */
    }
}


/*
 * cmd_expunge:
 * Write out the mail file without the deleted messages.  Adjust the
 * msgvec structure to remove these messages internally as well.
 */
void
#if HAVE_STDC
cmd_expunge (int n)
#else /* K&R style */
cmd_expunge (n)
int n;
#endif /* HAVE_STDC */
{
    confirm ();
    if (mode & MM_READ)	{		/* read mode? */
	fprintf (stderr,
		 "You cannot run expunge while in read mode, try write.\n");
	return;
    }
    /* don't use check_cf because it consults modify_read_only */
    /* and we want to know if we can *really* write to the file */
#ifdef COMMENT
    if (!cf)
	return;
    if (cf->flags & MF_RDONLY) {
	fprintf (stderr, "File is read-only, cannot expunge.\n");
	return;
    }
#else  /* beebe */
    if (!check_cf(O_WRONLY))		/* fails if no file or unwriteable */
	return;
#endif	/* COMMENT */
    if (!update (&cf,UPD_EXPUNGE|UPD_SAVEMOD)) /* do the expunge */
	return;
    if (cf != nil)			/* is it empty? */
	squish (cf);			/* no, free deleted messages */
}

/*
 * cmd_write:
 * Write out the mail file with the deleted messages.
 */
void
#if HAVE_STDC
cmd_write (int n)
#else /* K&R style */
cmd_write (n)
int n;
#endif /* HAVE_STDC */
{
    char *savefile;			/* where to write it to */
    time_t save_mtime;

    noise ("out mail file to");
    if (!check_cf(O_RDONLY))		/* error return if no file */
	return;
    savefile = parse_output_file("confirm with carriage return\n\
  or alternate filename", cf->filename, true);
    confirm ();				/* XXX memory leak */
    if (strcmp (savefile, cf->filename) == 0) { /* same file, save work */
	if ((cf->flags & MF_RDONLY) && !(cf->flags & MF_SAVEONLY))
	    cmerr ("cannot save read-only (examined) file under same name\n");
	update (&cf,UPD_ALWAYS);	/* do the write */
	return;
    }
    else {
	char realfile[MAXPATHLEN];
	/* XXX This is bogus */
	strcpy (realfile,cf->filename);	/* remember real filename */
	strcpy (cf->filename, savefile); /* pretend this is the file */
	save_mtime = cf->mtime;
	update (&cf, UPD_ALWAYS|UPD_ALTFILE);
	cf->mtime = save_mtime;		/* shouldn't change for altfile */
	strcpy (cf->filename, realfile); /* put that back */
    }
    free (savefile);
    return;
}


/*
 * squish:
 * Remove deleted messages from the msgvec structure passed, and
 * squish remaining messages together.
 */
static void
#if HAVE_STDC
squish (msgvec *mail)
#else /* K&R style */
squish (mail)
msgvec *mail;
#endif /* HAVE_STDC */
{
    int oldpos,newpos;			/* to move remaining messages */
    char *text;				/* text to free */
    message *msgs;			/* gonna use it a lot */

#ifdef DEBUG
	debug_validate_msgvec("before squish");
#endif
    if ((msgs = mail->msgs) == NULL)	/* easy access! */
	return;				/* empty, nothing to squish */
    for (oldpos = newpos = 1; oldpos <= mail->count; oldpos++) {
	if (msgs[oldpos].flags & M_DELETED) {
	    if ((text = msgs[oldpos].text) != NULL)
		free (text);
	    if (msgs[oldpos].from != NULL)
	        free (msgs[oldpos].from);
	    if (msgs[oldpos].hdrsum != NULL)
	        free (msgs[oldpos].hdrsum);
	    if (msgs[oldpos].keywords != NULL)
	        free_keylist(msgs[oldpos].keywords);
	}
	else {
	    if (newpos != oldpos)	/* past deleted messages? */
		msgs[newpos] = msgs[oldpos]; /* move it down */
	    newpos++;
	}
    }
    mail->current = mail->count = newpos-1; /* didn't write to final pos'n */
    mail->msgs = (message *) realloc (mail->msgs,
				      (mail->count+1)*sizeof (message));
#ifdef DEBUG
	debug_validate_msgvec("after squish");
#endif
}

void
#if HAVE_STDC
cmd_take (int n)
#else /* K&R style */
cmd_take (n)
int n;
#endif /* HAVE_STDC */
{
    cmtake(top_level_parser);
    cmcsb._cmwrp = autowrap_column;	/* put that back! */
}

void
#if HAVE_STDC
panic (const char *s)
#else /* K&R style */
panic (s)
const char *s;
#endif /* HAVE_STDC */
{
    printf ("%s: panic: %s\n", progname, s);
    cmtend ();
    abort();
}

const char *
#if HAVE_STDC
errstr(int err)
#else /* K&R style */
errstr(err)
int err;
#endif /* HAVE_STDC */
{
    switch (err) {
      case 0:
	return (const char *) "no error";
      case -1:
	err = errno;
      default:
#if defined(HAVE_GNU_LINUX)
	return (const char *) strerror(err);
#else
	{
	    static char temp[16];

	    if ((err < 0) || (err > sys_nerr)) {
		sprintf(temp,"Error %d", err);
		return (const char *) temp;
	    }
	    return (const char *) sys_errlist[err];
	}
#endif
    }
}

int
#if HAVE_STDC
cmd_push (int n)
#else /* K&R style */
cmd_push (n)
int n;
#endif /* HAVE_STDC */
{
    confirm ();
    shell (nil);
    return true;
}

int
#if HAVE_STDC
shell (const char *cmd)
#else /* K&R style */
shell (cmd)
const char *cmd;
#endif /* HAVE_STDC */
{
    int pid, status;
    const char *cp;

    /*
     * put tty back the way we found it
     */
    cmtend ();

    /*
     * update the user's file, just in case
     */
    if (cf)
	update(&cf,0);

    /*
     * make sure user knows how to get back
     */
    if (!cmd)
	printf("Pushing to subshell, use \"exit\" to return to MM\n");

    if (!(cp = getenv ("SHELL")))
	cp = "/bin/sh";

    fix_signals_for_fork (true);

    pid = vfork ();
    if (pid == 0) {
	umask(OLD_UMASK);
	if (cmd)
	    /* one-shot command */
	    execl (cp, cp, "-c", cmd, 0);
	else
	    /* interactive shell */
	    execl (cp, cp, 0);
	perror (cp);
	_exit (1);
    }
    if (pid > 0)
	status = wait_for_process (pid);
    else
	perror ("mm: fork");

    fix_signals_for_fork (false);	/* restore SIGCHLD, tty pgrp */

    cmtend ();				/* clean up after child */
    cmtset ();				/* set up terminal again */
    cmcsb._cmwrp = autowrap_column;	/* put that back */
    return status;
}

/*
 * mm_execute:
 * fork and execvp using given arguments and wait for child to terminate.
 * based on shell() command above.
 */

int
#if HAVE_STDC
mm_execute (const char *file, char *const *argv)
#else /* K&R style */
mm_execute (file, argv)
const char *file;
char * const *argv;
#endif /* HAVE_STDC */
{
    int pid, status;

    cmtend ();
    if (cf)				/* update, since who knows what may */
	update(&cf,0);			/* happen, and we turn of sig hndlrs */

    fix_signals_for_fork (true);

    pid = vfork ();
    if (pid == 0) {
	umask(OLD_UMASK);
	execvp (file, argv);
	perror (file);
	_exit (1);
    }
    if (pid > 0)
	status = wait_for_process (pid);
    else
	perror ("mm: fork");

    fix_signals_for_fork (false);

    cmtend ();				/* clean up after child */
    cmtset ();				/* set up terminal again */
    cmcsb._cmwrp = autowrap_column;	/* put that back */

    return status;
}


/* fancy exit */
void
#if HAVE_STDC
done(int n)
#else /* K&R style */
done(n)
int n;
#endif /* HAVE_STDC */
{
    cmtend ();
#ifdef MDEBUG
    m_done();
#endif
#ifdef USAGEFILE
    usage_stop(USAGEFILE);
#endif
    exit (n);
}


/*
 * CMD_SUSPEND:
 * suspend MM.
 */

void
#if HAVE_STDC
cmd_suspend (int n)
#else /* K&R style */
cmd_suspend (n)
int n;
#endif /* HAVE_STDC */
{
  confirm();
  suspend(0);
}

/*
 * suspend ourself.  this routine may be called either interactively,
 * or from the SIGTSTP handler.
 */

void
#if HAVE_STDC
suspend (int updflags)
             				/* flags for update */
#else /* K&R style */
suspend (updflags)
int updflags;				/* flags for update */
#endif /* HAVE_STDC */
{
#ifdef SIGTSTP
    long mask = block_signals ();

    cmtend ();				/* turn off ccmd's control of tty */

    if (cf)
	update (&cf, updflags);		/* make sure file is saved */

#ifdef USAGEFILE
    usage_stop (USAGEFILE);
#endif

    (void) kill (0, SIGSTOP);		/* stop ourself */

#ifdef USAGEFILE
    usage_start();
#endif

    cmtend ();				/* XXX fix up terminal again */
    cmtset ();				/* fix up the terminal */
    cmcsb._cmwrp = autowrap_column;	/* cmtset resets this */

    release_signals (mask);

#else	/* !SIGTSTP */
    printf ("The SUSPEND command is not supported on this system.\n");
#endif	/* SIGTSTP */
}

signalhandler
sighup_handler(VOID)
{
    if (cf != NULL)
	update (&cf, UPD_SAVEMOD);	/* make sure file is saved */
    done (1);
    return signalhandler_retval;
}

#ifdef SIGXCPU
signalhandler
sigxcpu_handler(VOID) {
    fprintf (stderr, "\
WARNING: This process has received an XCPU signal!\n\
This MM process has exceeded the per process CPU limit and will soon be\n\
killed.  Please save your work and exit MM.\n");
  if (cf != NULL) {
      printf ("Saving %s ...\n", cf->filename);
      update (&cf, UPD_SAVEMOD);
  }
  return signalhandler_retval;
}
#endif /* SIGXCPU */

/*
 * askem:
 * ask them the question and encourage them to give us a good answer
 */

#define MAXEOF 10

static int
#if HAVE_STDC
askem (int tty, char *pr, int prlen)
#else /* K&R style */
askem (tty, pr, prlen)
int tty;
char *pr;
int prlen;
#endif /* HAVE_STDC */
{
    char ans[BUFSIZ];
    int i;
    static char hint[] = "Please answer 'y' or 'n'\n";
    int eofcount = 0;

    while (TRUE) {
	i = 0;
	write (tty, pr, prlen);
	do {				/* get a line */
	    if (read (tty, &ans[i], 1) <= 0) {
		eofcount++;
	    }
	    if (eofcount > MAXEOF)
		return (TRUE);		/* assume yes */
	} while ((ans[i++] != '\n') && (i < BUFSIZ));
	if (i < BUFSIZ)
	    ans[i] = '\0';
	if (i == 2) {
	    if ((ans[0] == 'n') || (ans[0] == 'N'))
		return (FALSE);
	    if ((ans[0] == 'y') || (ans[0] == 'Y'))
		return (TRUE);
	}
	if (ustrcmp (ans, "no\n") == 0)
	    return (FALSE);
	if (ustrcmp (ans, "yes\n") == 0)
	    return (TRUE);

	/* failed, try again */
	write (tty, hint, sizeof (hint)-1);
    }
}


/*
 * sigint_handler:
 * handle sigint (^C) by saving the file and then exiting
 * ask them if they really want to exit
 */
signalhandler
sigint_handler(VOID)
{
    int tty;
    static char ask[] = "\nDo you really want to exit MM? [y/n] ";
    static char ask2[] = "\nSave mail file before exiting? [y/n] ";
    static char warn[] = "Cannot save file, not exiting.\n\
Try suspending MM, and making space by deleting some files.\n";
    static char cont[] = "Continuing...\n";

    cmtend ();				/* restore sane tty settings */
    tty = open ("/dev/tty", O_RDWR, 0);	/* read and write at them */
    if (tty >= 0 && !askem(tty, ask, sizeof (ask)-1)) {
	write (tty, cont, sizeof(cont)-1);
	close (tty);
	cmtset ();			/* give tty back to CCMD */
	return signalhandler_retval;	/* that's it, no exit */
    }

    if ((cf != NULL) &&
	(cf->flags & (MF_DIRTY|MF_MODIFIED))) { /* save the file */
	if (tty < 0 || askem (tty, ask2, sizeof (ask2)-1)) {
	    if (!update (&cf, UPD_SAVEMOD|UPD_QUIET)
		&& (cf->flags & MF_WRITERR)) {
		if (tty >= 0) {
		    write (tty, warn, sizeof(warn)-1);
		    close (tty);
		    cmtset ();		/* give tty back to CCMD */
		    return signalhandler_retval; /* keep going */
		}
	    }
	}
    }
#ifdef USAGEFILE
    usage_stop(USAGEFILE);
#endif
    signal (SIGINT, SIG_DFL);
    kill (PID, SIGINT);
    return signalhandler_retval;
}

int
#if HAVE_STDC
wait_for_process (int pid)
#else /* K&R style */
wait_for_process (pid)
int pid;
#endif /* HAVE_STDC */
{
    return do_wait (pid, true);
}

int
#if HAVE_STDC
collect_process (int pid)
#else /* K&R style */
collect_process (pid)
int pid;
#endif /* HAVE_STDC */
{
    return do_wait (pid, false);
}

/*
 * wait for a process, optionally blocking
 *
 * a pid of zero means we are being invoked by the
 * SIGCHLD handler to collect the process that just
 * exited.
 *
 * signal handlers should not set "blocking" to true.
 */

static int
#if HAVE_STDC
do_wait (int pid, int blocking)
#else /* K&R style */
do_wait (pid, blocking)
int pid, blocking;
#endif /* HAVE_STDC */
{
    int n;
    unsigned int status = 0;

/* #define DEBUGPRINTF(args)	(void)fprintf args */
#define DEBUGPRINTF(args)

    DEBUGPRINTF((stderr,"\n[1] Enter do_wait(pid=%d, blocking=%d)\n",
		 pid, blocking));

    if (blocking)
	fix_signals_for_wait (true);

    DEBUGPRINTF((stderr,"\n[2] do_wait(): back from fix_signals_for_wait()\n"));

    do {
#ifdef HAVE_WAIT3
	union wait w;
	int flags = blocking ? WUNTRACED : (WUNTRACED|WNOHANG);
#endif
	/*
	 * don't wait for a process that's already gone.
	 * this could happen if we're called while the
	 * SIGCHLD handler is active.
	 */
	if (blocking && pid > 0 && kill (pid, 0) < 0 && errno == ESRCH) {
	    DEBUGPRINTF((stderr,
			 "\nError in do_wait(pid=%d, blocking=%d): %s\n",
			 pid,
			 blocking,
			 "(blocking && pid > 0 && kill (pid, 0) < 0 && errno == ESRCH)"));

		status = (unsigned int)-2;
		break;
	}

#ifdef HAVE_WAIT3
#if defined(__osf__) || defined(__NeXT__)
	n = wait3 (&w, flags, 0);
#else
	n = wait3 (&w.w_status, flags, 0);
#endif

	DEBUGPRINTF((stderr,"[3] do_wait(): back from wait3()\n"));

#ifdef SIGCONT /* AIX 2.2.1 has wait3() but not SIGCONT */
	if (n > 0 && w.w_stopval == WSTOPPED) {
	    DEBUGPRINTF((stderr,
			 "[4] Error in do_wait(pid=%d, blocking=%d): %s (n=%d)\n",
			 pid,
			 blocking,
			"n > 0 && w.w_stopval == WSTOPPED",
			 n));
	    (void) kill (n, SIGCONT);
	    continue;
	}
#endif
	status = w.w_status;
#else
	n = wait ((int*)&status);
#endif
	if (n > 0)
	{
	    forget_pid (n);
	    DEBUGPRINTF((stderr,
			 "[5] do_wait(pid=%d, blocking=%d): forget_pid(%d)\n",
			 pid,
			 blocking,
			 n));

	}
	else
	    if (n == 0 && !blocking) {
		DEBUGPRINTF((stderr,
			     "[6] do_wait(pid=%d, blocking=%d)\n",
			     pid, blocking));
		status = 0;
		break;
	    }
	    else
		if (n < 0 && errno != EINTR) {
		    DEBUGPRINTF((stderr,
				 "[7] do_wait(pid=%d, blocking=%d) (n=%d)\n",
				 pid, blocking, n));
		    status = (unsigned int)-3;
		    break;
		}

	DEBUGPRINTF((stderr,
		     "[8] do_wait(pid=%d, blocking=%d) (n=%d)\n",
		     pid, blocking, n));


	if (n == pid)
	    break;

	DEBUGPRINTF((stderr,
		     "[9] do_wait(pid=%d, blocking=%d): end of while(blocking) loop\n",
		     pid, blocking));
    } while (blocking);

    DEBUGPRINTF((stderr,
		 "[10] do_wait(pid=%d, blocking=%d): exit from while(blocking) loop\n",
		 pid, blocking));

    if (blocking)
	fix_signals_for_wait (false);

    DEBUGPRINTF((stderr,
		 "[11] do_wait(pid=%d, blocking=%d): return from fix_signals_for_wait(): status=%d\n",
		 pid, blocking, (int)status));

    return (int)status;
}

#define MAXBG 10
static int nrun = 0;
static unsigned bg_procs[MAXBG];

void
#if HAVE_STDC
maybe_wait_for_process (int pid)
#else /* K&R style */
maybe_wait_for_process (pid)
int pid;
#endif /* HAVE_STDC */
{
    int i;

    if (nrun < MAXBG) {
	for (i = 0; i < MAXBG; i++)
	    if (bg_procs[i] == 0) {
		bg_procs[i] = pid;
		nrun++;
		return;
	    }
    }
    /* see if any of the procs went away when we weren't looking */
    for (i = 0; i < MAXBG; i++)
	if ((kill (bg_procs[i], 0) < 0) && (errno == ESRCH)) {
	    bg_procs[i] = pid;		/* oh good, this slot's empty */
	    return;
	}

    /*
     * we've exceeded the number of allowed background processes,
     * so we'll just have to wait for this one.
     */
    wait_for_process (pid);
}

static void
#if HAVE_STDC
forget_pid (unsigned int n)
#else /* K&R style */
forget_pid (n)
unsigned n;
#endif /* HAVE_STDC */
{
    int i;
    for (i = 0; i < MAXBG; i++)
	if (bg_procs[i] == n) {
	    bg_procs[i] = 0;
	    --nrun;
	}
}
