/*
    Anacron - run commands periodically
    Copyright (C) 1998  Itai Tzur <itzur@actcom.co.il>
    Copyright (C) 1999  Sean 'Shaleh' Perry <shaleh@debian.org>
 
    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
 
    The GNU General Public License can also be found in the file
    `COPYING' that comes with the Anacron source distribution.
*/


#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <pwd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include "global.h"

static int
temp_file()
/* Open a temporary file and return its file descriptor */
{
    const int max_retries = 50;
    char *name;
    int fd, i;

    i = 0;
    name = NULL;
    do
    {
	i++;
	free(name);
	name = tempnam(NULL, NULL);
	if (name == NULL) die("Can't find a unique temporary filename");
	fd = open(name, O_RDWR | O_CREAT | O_EXCL | O_APPEND,
		  S_IRUSR | S_IWUSR);
	/* I'm not sure we actually need to be so persistent here */
    } while (fd == -1 && errno == EEXIST && i < max_retries);
    
    if (fd == -1) die_e("Can't open temporary file");
    if (unlink(name)) die_e("Can't unlink temporary file");
    free(name);
    fcntl(fd, F_SETFD, 1);    /* set close-on-exec flag */
    return fd;
}

static off_t
file_size(int fd)
/* Return the size of temporary file fd */
{
    struct stat st;

    if (fstat(fd, &st)) die_e("Can't fstat temporary file");
    return st.st_size;
}

static char *
username()
{
    struct passwd *ps;

    ps = getpwuid(geteuid());
    if (ps == NULL) die_e("getpwuid() error");
    return ps->pw_name;
}

static void
xputenv(const char *s)
{
    if (putenv(s)) die_e("Can't set the environment");
}

static void
setup_env(const job_rec *jr)
/* Setup the environment for the job according to /etc/anacrontab */
{
    env_rec *er;

    er = first_env_rec;
    if (er == NULL || jr->prev_env_rec == NULL) return;
    xputenv(er->assign);
    while (er != jr->prev_env_rec)
    {
	er = er->next;
	xputenv(er->assign);
    }
}

static void
run_job(const job_rec *jr)
/* This is called to start the job, after the fork */
{
    setup_env(jr);
    /* setup stdout and stderr */
    xclose(1);
    xclose(2);
    if (dup2(jr->output_fd, 1) != 1 || dup2(jr->output_fd, 2) != 2)
	die_e("dup2() error");     /* dup2 also clears close-on-exec flag */
    in_background = 0;  /* now, errors will be mailed to the user */
    if (chdir("/")) die_e("Can't chdir to '/'");

    umask(old_umask);
    if (sigprocmask(SIG_SETMASK, &old_sigmask, NULL))
	die_e("sigprocmask error");
    xcloselog();
    execl("/bin/sh", "/bin/sh", "-c", jr->command, (char *)NULL);
    die_e("execl() error");
}

static void
xwrite(int fd, const char *string)
/* Write (using write()) the string "string" to temporary file "fd".
 * Don't return on failure */
{
    if (write(fd, string, strlen(string)) == -1)
	die_e("Can't write to temporary file");
}

static int
xwait(pid_t pid , int *status)
/* Check if child process "pid" has finished.  If it has, return 1 and its
 * exit status in "*status".  If not, return 0.
 */
{
    pid_t r;

    r = waitpid(pid, status, WNOHANG);
    if (r == -1) die_e("waitpid() error");
    if (r == 0) return 0;
    return 1;
}

static void
launch_mailer(job_rec *jr)
{
    pid_t pid;

    pid = xfork();
    if (pid == 0)
    {
	/* child */
	in_background = 1;
	/* set stdin to the job's output */
	xclose(0);
	if (dup2(jr->output_fd, 0) != 0) die_e("Can't dup2()");
	if (lseek(0, 0, SEEK_SET) != 0) die_e("Can't lseek()");
	umask(old_umask);
	if (sigprocmask(SIG_SETMASK, &old_sigmask, NULL))
	    die_e("sigprocmask error");
	xcloselog();

	/* Here, I basically mirrored the way /usr/sbin/sendmail is called
	 * by cron on a Debian system, except for the "-oem" and "-or0s"
	 * options, which don't seem to be appropriate here.
	 * Hopefully, this will keep all the MTAs happy. */
	execl(SENDMAIL, SENDMAIL, "-FAnacron", "-odi",
	      username(), (char *)NULL);
	die_e("Can't exec " SENDMAIL);
    }
    /* parent */
    /* record mailer pid */
    jr->mailer_pid = pid;
    running_mailers++;
}

static void
tend_mailer(job_rec *jr, int status)
{
    if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
	complain("Tried to mail output of job `%s', "
		 "but mailer process (" SENDMAIL ") exited with ststus %d",
		 jr->ident, WEXITSTATUS(status));
    else if (!WIFEXITED(status) && WIFSIGNALED(status))
	complain("Tried to mail output of job `%s', "
		 "but mailer process (" SENDMAIL ") got signal %d",
		 jr->ident, WTERMSIG(status));
    else if (!WIFEXITED(status) && !WIFSIGNALED(status))
	complain("Tried to mail output of job `%s', "
		 "but mailer process (" SENDMAIL ") terminated abnormally"
		 , jr->ident);

    jr->mailer_pid = 0;
    running_mailers--;
}

void
launch_job(job_rec *jr)
{
    pid_t pid;
    int fd;

    /* create temporary file for stdout and stderr of the job */
    fd = jr->output_fd = temp_file();
    /* write mail header */
    xwrite(fd, "From: ");
    xwrite(fd, username());
    xwrite(fd, " (Anacron)\n");
    xwrite(fd, "To: ");
    xwrite(fd, username());
    xwrite(fd, "\n");
    xwrite(fd, "Subject: Anacron job '");
    xwrite(fd, jr->ident);
    xwrite(fd, "'\n\n");
    jr->mail_header_size = file_size(fd);

    pid = xfork();
    if (pid == 0)
    {
	/* child */
	in_background = 1;
	run_job(jr);
	/* execution never gets here */
    }
    /* parent */
    explain("Job `%s' started", jr->ident);
    jr->job_pid = pid;
    running_jobs++;
}

static void
tend_job(job_rec *jr, int status)
/* Take care of a finished job */
{
    int mail_output;
    char *m;

    update_timestamp(jr);
    unlock(jr);
    if (file_size(jr->output_fd) > jr->mail_header_size) mail_output = 1;
    else mail_output = 0;

    m = mail_output ? " (mailing output)" : "";
    if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
	explain("Job `%s' terminated%s", jr->ident, m);
    else if (WIFEXITED(status))
	explain("Job `%s' terminated (exit status: %d)%s",
		jr->ident, WEXITSTATUS(status), m);
    else if (WIFSIGNALED(status))
	complain("Job `%s' terminated due to signal %d%s",
		 jr->ident, WTERMSIG(status), m);
    else /* is this possible? */
	complain("Job `%s' terminated abnormally%s", jr->ident, m);

    jr->job_pid = 0;
    running_jobs--;
    if (mail_output) launch_mailer(jr);
    xclose(jr->output_fd);
}

void
tend_children()
/* This is called whenever we get a SIGCHLD.
 * Takes care of zombie children.
 */
{
    int j;
    int status;

    j = 0;
    while (j < njobs)
    {
	if (job_array[j]->mailer_pid != 0 &&
	    xwait(job_array[j]->mailer_pid, &status))
	    tend_mailer(job_array[j], status);
	if (job_array[j]->job_pid != 0 &&
	    xwait(job_array[j]->job_pid, &status))
	    tend_job(job_array[j], status);
	j++;
    }
}
