/*
  rc-update
  Manage init scripts and runlevels
*/

/*
 * Copyright (c) 2007-2009 Roy Marples <roy@marples.name>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "builtins.h"
#include "einfo.h"
#include "queue.h"
#include "rc.h"
#include "rc-misc.h"

extern const char *applet;

/* Return the number of changes made:
 *  -1 = no changes (error)
 *   0 = no changes (nothing to do)
 *  1+ = number of runlevels updated
 */
static int
add(const char *runlevel, const char *service)
{
	int retval = -1;

	if (!rc_service_exists(service)) {
		if (errno == ENOEXEC)
			eerror("%s: service `%s' is not executeable",
			    applet, service);
		else
			eerror("%s: service `%s' does not exist",
			    applet, service);
	} else if (rc_service_in_runlevel(service, runlevel)) {
		einfo("%s: %s already installed in runlevel `%s'; skipping",
		    applet, service, runlevel);
		retval = 0;
	} else if (rc_service_add(runlevel, service)) {
		einfo("service %s added to runlevel %s", service, runlevel);
		retval = 1;
	} else
		eerror("%s: failed to add service `%s' to runlevel `%s': %s",
		    applet, service, runlevel, strerror (errno));

	return retval;
}

static int
delete(const char *runlevel, const char *service)
{
	int retval = -1;

	errno = 0;
	if (rc_service_delete(runlevel, service)) {
		einfo("service %s removed from runlevel %s",
		    service, runlevel);
		return 1;
	}

	if (errno == ENOENT)
		eerror("%s: service `%s' is not in the runlevel `%s'",
		    applet, service, runlevel);
	else
		eerror("%s: failed to remove service `%s' from runlevel `%s': %s",
		    applet, service, runlevel, strerror (errno));

	return retval;
}

static int
addstack(const char *runlevel, const char *stack)
{
	if (!rc_runlevel_exists(runlevel)) {
		eerror("%s: runlevel `%s' does not exist", applet, runlevel);
		return -1;
	}
	if (!rc_runlevel_exists(stack)) {
		eerror("%s: runlevel `%s' does not exist", applet, stack);
		return -1;
	}
	if (strcmp(runlevel, stack) == 0) {
		eerror("%s: cannot stack `%s' onto itself", applet, stack);
		return -1;
	}
	if (strcmp(runlevel, RC_LEVEL_SYSINIT) == 0 ||
	    strcmp(stack, RC_LEVEL_SYSINIT) == 0 ||
	    strcmp(runlevel, RC_LEVEL_BOOT) == 0 ||
	    strcmp(stack, RC_LEVEL_BOOT) == 0 ||
	    strcmp(runlevel, RC_LEVEL_SINGLE) == 0 ||
	    strcmp(stack, RC_LEVEL_SINGLE) == 0 ||
	    strcmp(runlevel, RC_LEVEL_SHUTDOWN) == 0 ||
	    strcmp(stack, RC_LEVEL_SHUTDOWN) == 0)
	{
		eerror("%s: cannot stack the %s runlevel",
		    applet, RC_LEVEL_SYSINIT);
		return -1;
	}
	if (!rc_runlevel_stack(runlevel, stack)) {
		eerror("%s: failed to stack `%s' to `%s': %s",
		    applet, stack, runlevel, strerror(errno));
		return -1;
	}
	einfo("runlevel %s added to runlevel %s", stack, runlevel);
	return 1;
}

static int
delstack(const char *runlevel, const char *stack)
{
	if (rc_runlevel_unstack(runlevel, stack)) {
		einfo("runlevel %s removed from runlevel %s", stack, runlevel);
		return 1;
	}

	if (errno == ENOENT)
		eerror("%s: runlevel `%s' is not in the runlevel `%s'",
		    applet, stack, runlevel);
	else
		eerror("%s: failed to remove runlevel `%s' from runlevel `%s': %s",
		    applet, stack, runlevel, strerror (errno));

	return -1;
}

static void
show(RC_STRINGLIST *runlevels, bool verbose)
{
	RC_STRINGLIST *services = rc_services_in_runlevel(NULL);
	RC_STRING *service;
	RC_STRING *runlevel;
	RC_STRINGLIST *in;
	bool inone;
	char buffer[PATH_MAX];
	size_t l;

	rc_stringlist_sort(&services);
	TAILQ_FOREACH(service, services, entries) {
		in = rc_stringlist_new();
		inone = false;

		TAILQ_FOREACH(runlevel, runlevels, entries) {
			if (rc_service_in_runlevel(service->value,
				runlevel->value))
			{
				rc_stringlist_add(in, runlevel->value);
				inone = true;
			} else {
				l = strlen(runlevel->value);
				memset (buffer, ' ', l);
				buffer[l] = 0;
				rc_stringlist_add (in, buffer);
			}
		}

		if (inone || verbose) {
			printf(" %20s |", service->value);
			TAILQ_FOREACH(runlevel, in, entries)
			    printf (" %s", runlevel->value);
			printf ("\n");
		}
		rc_stringlist_free(in);
	}

	rc_stringlist_free (services);
}

#include "_usage.h"
#define usagestring ""							\
	"Usage: rc-update [options] add <service> [<runlevel>...]\n"	\
	"   or: rc-update [options] del <service> [<runlevel>...]\n"	\
	"   or: rc-update [options] [show [<runlevel>...]]"
#define getoptstring "asu" getoptstring_COMMON
static const struct option longopts[] = {
	{ "all",             0, NULL, 'a' },
	{ "stack",           0, NULL, 's' },
	{ "update",          0, NULL, 'u' },
	longopts_COMMON
};
static const char * const longopts_help[] = {
	"Process all runlevels",
	"Stack a runlevel instead of a service",
	"Force an update of the dependency tree",
	longopts_help_COMMON
};
#include "_usage.c"

#define DOADD    (1 << 1)
#define DODELETE (1 << 2)
#define DOSHOW   (1 << 3)

int
rc_update(int argc, char **argv)
{
	RC_DEPTREE *deptree;
	RC_STRINGLIST *runlevels;
	RC_STRING *runlevel;
	char *service = NULL;
	char *p;
	int action = 0;
	bool verbose = false, stack = false, all_runlevels = false;
	int opt;
	int retval = EXIT_FAILURE;
	int num_updated = 0;
	int (*actfunc)(const char *, const char *);
	int ret;

	while ((opt = getopt_long(argc, argv, getoptstring,
		    longopts, (int *)0)) != -1)
		switch (opt) {
		case 'a':
		    all_runlevels = true;
		break;
		case 's':
		    stack = true;
		break;
		case 'u':
			deptree = _rc_deptree_load(-1, &ret);
			if (deptree)
				rc_deptree_free(deptree);
			return ret;
		case_RC_COMMON_GETOPT
		}

	verbose = rc_yesno(getenv ("EINFO_VERBOSE"));

	if ((action & DOSHOW   && action != DOSHOW) ||
	    (action & DOADD    && action != DOADD) ||
	    (action & DODELETE && action != DODELETE))
		eerrorx("%s: cannot mix commands", applet);

	/* We need to be backwards compatible */
	if (optind < argc) {
		if (strcmp(argv[optind], "add") == 0)
			action = DOADD;
		else if (strcmp(argv[optind], "delete") == 0 ||
		    strcmp(argv[optind], "del") == 0)
			action = DODELETE;
		else if (strcmp(argv[optind], "show") == 0)
			action = DOSHOW;
		if (action)
			optind++;
		else
			eerrorx("%s: invalid command `%s'",
			    applet, argv[optind]);
	}
	if (!action)
		action = DOSHOW;

	runlevels = rc_stringlist_new();

	if (optind >= argc) {
		if (!(action & DOSHOW))
			eerrorx("%s: no service specified", applet);
	} else {
		service = argv[optind];
		optind++;

		while (optind < argc)
			if (rc_runlevel_exists(argv[optind]))
				rc_stringlist_add(runlevels, argv[optind++]);
			else {
				rc_stringlist_free(runlevels);
				eerrorx ("%s: `%s' is not a valid runlevel",
				    applet, argv[optind]);
			}
	}

	retval = EXIT_SUCCESS;
	if (action & DOSHOW) {
		if (service)
			rc_stringlist_add(runlevels, service);
		if (!TAILQ_FIRST(runlevels)) {
			free(runlevels);
			runlevels = rc_runlevel_list();
		}

		rc_stringlist_sort(&runlevels);
		show (runlevels, verbose);
	} else {
		if (!service)
			eerror ("%s: no service specified", applet);
		else {
			if (action & DOADD) {
				if (all_runlevels) {
					rc_stringlist_free(runlevels);
					eerrorx("%s: the -a option is invalid with add", applet);
				}
				actfunc = stack ? addstack : add;
			} else if (action & DODELETE) {
				actfunc = stack ? delstack : delete;
			} else {
				rc_stringlist_free(runlevels);
				eerrorx("%s: invalid action", applet);
			}

			if (!TAILQ_FIRST(runlevels)) {
				if (all_runlevels) {
					free(runlevels);
					runlevels = rc_runlevel_list();
				} else {
					p = rc_runlevel_get();
					rc_stringlist_add(runlevels, p);
					free(p);
				}
			}

			if (!TAILQ_FIRST(runlevels)) {
				free(runlevels);
				eerrorx("%s: no runlevels found", applet);
			}

			TAILQ_FOREACH(runlevel, runlevels, entries) {
				if (!rc_runlevel_exists(runlevel->value)) {
					eerror ("%s: runlevel `%s' does not exist",
					    applet, runlevel->value);
					continue;
				}

				ret = actfunc(runlevel->value, service);
				if (ret < 0)
					retval = EXIT_FAILURE;
				num_updated += ret;
			}

			if (retval == EXIT_SUCCESS &&
			    num_updated == 0 && action & DODELETE)
				ewarnx("%s: service `%s' not found in any"
				    " of the specified runlevels",
				    applet, service);
		}
	}

	rc_stringlist_free(runlevels);
	return retval;
}