/*
 * Postpone a command if a lockfile exists, also avoid running multiple times.
 *
 * Copyright (C) 2007, 2011  Christoph Berg
 *
 * Based on code from:
 * Debian menu system -- update-menus
 * update-menus/update-menus.cc
 *
 * Copyright (C) 1996-2003  Joost Witteveen
 * Copyright (C) 2002-2004  Bill Allombert and Morten Brix Pedersen
 *
 * 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.
 */

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <libintl.h>
#include <locale.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>


#define DPKG_LOCKFILE "/var/lib/dpkg/lock"
#define DEBIAN_EXTRA_LOCKFILE "/var/lib/dpkg/postpone.lock"
#define MAXLOCKS 32
#define PACKAGE "postpone"
#define VERSION "0.1"
#define LOCALEDIR "/usr/share/locale"
#define _(x) (gettext (x))
#define perror(x) print (stderr, "%s: %s", (x), strerror(errno))

static int foreground = 0;
static int verbose = 0;
static char *postpone_lock = NULL;
static char *waitlist[MAXLOCKS];
static int n_waits = 0;
static char *extra_lock = NULL;
static char *output = NULL;
static int redirect_always = 0;
static int backgrounded = 0;

static void
print (FILE *f, const char *format, ...)
{
    static int opened = 0;
    va_list ap;
    va_start (ap, format);

    if (backgrounded) {
	if (!opened) {
	    openlog("postpone", LOG_PID, LOG_DAEMON);
	    opened = 1;
	}
	vsyslog (LOG_INFO, format, ap);
    } else {
	vfprintf (f, format, ap);
    }

    va_end (ap);
}

/* Try to create a lock file for this postpone task */
static int
get_lock (char *fname)
{
    int fd;

    if ((fd = open (fname, O_WRONLY|O_CREAT, 00644)) == -1) {
	print (stderr, _("open %s: %s\n"), fname, strerror (errno));
	exit (1);
    }

    if (flock (fd, LOCK_EX|LOCK_NB) == -1) {
	if (errno != EWOULDBLOCK && errno != EAGAIN) {
	    print (stderr, _("flock %s: %s\n"), fname, strerror (errno));
	    exit (1);
	}
	close (fd);
	return -1;
    }

    char buf[10];
    snprintf (buf, 10, "%d\n", getpid());
    if (write(fd, buf, strlen(buf)) < 1) {
	print (stderr, _("write %s: %s\n"), fname, strerror (errno));
	close (fd);
	return -1;
    }

    if (verbose > 1)
	print (stdout, _("Got lock on %s.\n"), fname);
    return fd; /* beware: might be 0 */
}

/* Try to remove postpone lock */
static void
remove_lock (char *fname)
{
    if (unlink (fname))
	perror (fname);
}

/* Check whether dpkg is locked
 * return 1 if DPKG_LOCKFILE is locked and we should wait
 * return 0 if we don't need to wait
 * when in doubt return 0 to avoid deadlocks.
 */
static int
wait_for_file (char *fname)
{
    int fd;
    struct flock fl;

    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;
    fd = open(fname, O_RDWR|O_TRUNC, 0660);
    if (fd == -1) {
	/* Probably /var/lib/dpkg does not exist.
	 * Most probably dpkg is not running.
	 */
	if (verbose > 1)
	    print (stdout, _("%s does not exist. Good.\n"), fname);
	return 0;
    }
    if (fcntl(fd, F_GETLK, &fl) == -1)
    {
	/* Probably /var/lib/dpkg filesystem does not support
	 * locks.
	 */
	close (fd);
	if (verbose > 1)
	    print (stdout, _("Error while locking %s. Assuming this is a good sign.\n"), fname);
	return 0;
    }
    close (fd);

    if (verbose > 1) {
	if (fl.l_type == F_UNLCK)
	    print (stdout, _("%s is not locked. Good.\n"), fname);
	else
	    print (stdout, _("%s is locked. Waiting.\n"), fname);
    }
    return fl.l_type != F_UNLCK;
}

static int
wait_for_locks ()
{
    int i;
    for (i = 0; i < n_waits; i++) {
	int ret = wait_for_file (waitlist[i]);
	if (ret != 0) {
	    return 1;
	}
    }

    if (extra_lock) {
	int ret = get_lock (extra_lock);
	if (ret == -1) {
	    if (verbose > 1)
		print (stdout, _("Extra lock %s is not available. Waiting.\n"), extra_lock);
	    return 1;
	}
    }

    return 0; /* all locks available */
}

static int
run_command (char **argv, int foreground)
{
    int pid, status;
    int redirect = output && (!foreground || redirect_always);

    if ((pid = fork ()) == 0) { /* child */
	backgrounded = 1;

	if (redirect) {
	    int fd;
	    if (strlen (output) < 6 || strncmp (output + strlen (output) - 6, "XXXXXX", 6))
		fd = open (output, O_RDWR|O_CREAT|O_TRUNC, 0600);
	    else
		fd = mkstemp (output);
	    if (fd == -1) {
		perror (output);
	    }
	    dup2 (fd, 1);
	    dup2 (fd, 2);
	}

	if (verbose > 1)
	    print (stdout, _("Running %s\n"), argv[0]);

	execvp (argv[0], argv);
	perror (argv[0]);
	exit (1);
    }
    /* parent */

    if (pid == -1 ) {
	if (foreground)
	    perror ("fork");
	exit (1);
    }

    waitpid (pid, &status, 0);
    if (postpone_lock)
	remove_lock (postpone_lock);
    if (extra_lock)
	remove_lock (extra_lock);

    if (verbose > 1)
	print (stdout, _("Child %d exited\n"), pid);

    if (WIFEXITED (status))
	if (redirect && WEXITSTATUS (status) == 0)
	    unlink (output);
	return WEXITSTATUS (status);

    return 1;
}

static void
usage (FILE *f, int i)
{
    fprintf(f, _("Postpone schedules a command to be executed later when a lockfile disappears\n"
		 "Usage: postpone [-dfv] [-wlLoO FILE] command args...\n"
		 "-w --wait FILE [...]  wait for fcntl lock on FILE before running command\n"
		 "-l --lock FILE        create lockfile FILE, exit if it exists\n"
		 "                        use to avoid scheduling a command several times\n"
		 "-L --extra-lock FILE  extra lock to serialize different postponed commands\n"
		 "-d --debian           equivalent to --wait " DPKG_LOCKFILE "\n"
		 "                        --extra-lock " DEBIAN_EXTRA_LOCKFILE "\n"
		 "-o --output FILE      redirect stdout/stderr to FILE when in background\n"
		 "-O --all-output FILE  unconditionally redirect stdout/stderr to FILE\n"
		 "-f --foreground       do not detach while waiting for locks\n"
		 "-v --verbose          verbose operation, repeat for debugging output\n"
		 "   --help             print this help and exit\n"
		 "   --version          print version information and exit\n"
		 "   --version-string   print version string and exit\n"
		 ));
    exit (i);
}

static void
version (FILE *f, int i)
{
    fprintf(f, _("Postpone %s - schedule a task to be executed later when a lockfile disappears\n"
		 "Copyright (C) 1996-2003  Joost Witteveen\n"
		 "Copyright (C) 2002-2004  Bill Allombert and Morten Brix Pedersen\n"
		 "Copyright (C) 2007  Christoph Berg\n"), VERSION);
    exit (i);
}

static struct option long_options[] = {
  { "debian", no_argument, NULL, 'd' },
  { "foreground", no_argument, NULL, 'f' },
  { "help", no_argument, NULL, 'h' },
  { "lock", required_argument, NULL, 'l' },
  { "extra-lock", required_argument, NULL, 'L' },
  { "output", required_argument, NULL, 'o' },
  { "all-output", required_argument, NULL, 'O' },
  { "verbose", no_argument, NULL, 'v' },
  { "version", no_argument, NULL, 'V'},
  { "wait", required_argument, NULL, 'w' },
  { "version-string", no_argument, NULL, 'X'},
  { NULL, 0, NULL, 0 } };

/* Parse command line parameters */
static void
parse_params(int argc, char **argv)
{
    while(1) {
	int c = getopt_long (argc, argv, "+dfl:L:o:O:vw:", long_options, NULL);
	if (c == -1)
	    break;
	switch(c) {
	    case 'd':
		if (n_waits < MAXLOCKS)
		    waitlist[n_waits++] = DPKG_LOCKFILE;
		extra_lock = DEBIAN_EXTRA_LOCKFILE;
		break;
	    case 'f':
		foreground = 1;
		break;
	    case 'h':
		usage (stdout, 0);
	    case 'l':
		postpone_lock = strdup (optarg);
		break;
	    case 'L':
		extra_lock = strdup (optarg);
		break;
	    case 'O':
		redirect_always = 1;
		/* FALLTHROUGH */
	    case 'o':
		output = strdup (optarg);
		break;
	    case 'v':
		verbose++;
		break;
	    case 'V':
		version (stdout, 0);
	    case 'w':
		if (n_waits < MAXLOCKS)
		    waitlist[n_waits++] = strdup (optarg);
		break;
	    case 'X':
		puts (VERSION);
		exit (0);
	    /* ignore unknown options for easier compatibility with future versions */
	}
    }
}

int main (int argc, char **argv)
{
    int lock = -1, pid, i;

    setlocale (LC_ALL, "");
    bindtextdomain (PACKAGE, LOCALEDIR);
    textdomain (PACKAGE);

    parse_params (argc, argv);

    if (optind >= argc) {
	usage (stderr, 1);
    }

    if (postpone_lock)
	if ((lock = get_lock (postpone_lock)) == -1) {
	    if (verbose)
		print (stdout, _("%s is already running in background.\n"), argv[optind]);
	    exit (0);
	}

    if (wait_for_locks () == 0) {
	if (verbose > 1)
	    print (stdout, _("No locks to wait for, running in foreground now.\n"));
	return run_command (argv + optind, 1);
    }

    if (verbose > 1) {
	print (stdout, _("Waiting for lock on"));
	for (i = 0; i < n_waits; i++)
	    print (stdout, " %s", waitlist[i]);
	if (extra_lock)
	    print (stdout, " %s", extra_lock);
	print (stdout, ".\n");
    }

    if (!foreground) {
	if ((pid = fork ()) > 0) { /* parent */
	    if (verbose)
		print (stdout, _("Running %s in background.\n"), argv[optind]);
	    exit (0);
	} else if (pid == -1) {
	    if (postpone_lock)
		remove_lock (postpone_lock);
	    perror ("fork");
	    exit (1);
	}
	/* child */

	backgrounded = 1;

	/* Close all fds except the lock fd for daemon mode */
	for (i = 0; i < 32; i++) {
	    if (i != lock)
		close (i);
	}
    }

    /* wait for dpkg/other locks */
    do {
	sleep (2);
    } while (wait_for_locks ());

    return run_command (argv + optind, foreground);
}

/* vim:sw=4:
 */
