/*
 *  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.
 */

/* Author: Pekka "PQ" Paalanen, pq@iki.fi, http://www.iki.fi/pq/ */

/* $Id: supernice.c,v 1.14 2003/09/12 13:50:11 pq Exp $ */

/* Compile with:
     gcc -O -W -Wall -ansi -o supernice supernice.c
   Then set it to SUID root for normal users to be able to
   raise process priority.
 */

/* Supernice was developed under Linux 2.4. */


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <syslog.h>
#include <linux/capability.h>
#include <sys/time.h>
#include <sys/resource.h>

#define PROGRAM_NAME "supernice"
#define MAX_USER_PRIORITY -4
#define DEFAULT_ADJUSTMENT 10

/* my return codes */
enum {
	RET_OK,
	ERR_EXE_MISSING,
	ERR_INV_ADJ
};


int parseopts(int argc, char *argv[], int *adjustment, int *cmdstart);
void drop_privileges(void);
void print_error_message(int code);
void perror_exit(char *msg);
void print_usage(void);
int get_new_priority(int adjustment);


int main(int argc, char *argv[])
{
	int adjustment, cmdstart;
	int ret;
	int new_priority;
	
	/* open system log */
	openlog(PROGRAM_NAME, LOG_PID, LOG_USER);

	/* parse parameters */
	ret = parseopts(argc, argv, &adjustment, &cmdstart);
	if(ret != RET_OK)
	{
		drop_privileges();
		print_error_message(ret);
		return EXIT_FAILURE;
	}
	
	/* get and calculate priority */
	new_priority = get_new_priority(adjustment);
	
	/* set priority */
	ret = setpriority(PRIO_PROCESS, 0, new_priority);
	if(ret)
	{
		perror_exit(PROGRAM_NAME ", setpriority");
	}
	
	/* drop privileges and run the program */
	drop_privileges();
	if(new_priority < 0)
	{
		syslog(LOG_INFO, "executing '%s' with priority %d",
			argv[cmdstart], new_priority);
	}
	
	execvp(argv[cmdstart], &argv[cmdstart]);
	
	/* program could not be run */
	if(new_priority < 0)
	{
		/* log if tried to run elevated priority */
		syslog(LOG_ERR, "executing '%s' failed: %m", argv[cmdstart]);
	}
	
	fprintf(stderr, "%s: executing '%s' failed: ",
		PROGRAM_NAME, argv[cmdstart]);
	perror(NULL);
	
	return EXIT_FAILURE;
}


int parseopts(int argc, char *argv[], int *adjustment, int *cmdstart)
{
	int adj;
	char *startp;
	char *endp;
	
	if(argc < 2)
	{
		return ERR_EXE_MISSING;
	}
	
	/* assume that first parameter is adjustment, if it starts with '-' */
	if((argc == 2) && (argv[1][0] == '-'))
	{
		return ERR_EXE_MISSING;
	}
	
	/* get adjustment */
	if(argv[1][0] == '-')
	{
		startp = argv[1]+1;
		adj = strtol(startp, &endp, 10);
		if((*startp != 0) && (*endp == 0))
		{
			*adjustment = adj;
			*cmdstart = 2;
		} else
		{
			return ERR_INV_ADJ;
		}
	} else
	{
		/* no adjustment, use default */
		*adjustment = DEFAULT_ADJUSTMENT;
		*cmdstart = 1;
	}

	return RET_OK;
}


void drop_privileges(void)
{
	int ret;
	ret = setuid(getuid());
	if(ret)
	{
		perror("Unable to drop privileges, abort.");
		exit(EXIT_FAILURE);
	}
}


void print_error_message(int code)
{
	char *msg;
	switch(code)
	{
		case ERR_EXE_MISSING:
			msg = "executable missing";
			break;
		case ERR_INV_ADJ:
			msg = "invalid adjustment string";
			break;
		default:
			msg = "[unknown error code]";
	}
	
	fprintf(stderr, "supernice: %s\n\n", msg);
	print_usage();
}

void perror_exit(char *msg)
{
	drop_privileges();
	perror(msg);
	exit(EXIT_FAILURE);
}

void print_usage(void)
{
	fprintf(stderr,
	"Usage: %s [-ADJ] COMMAND\n"
	"Run COMMAND with priority chaged by amount ADJ.\n"
	"ADJ can be from %d to 19, other values will be truncated.\n"
	"ADJ is relative to current priority, but new priority can\n"
	"never be higher than %d or current priority.\n"
	, PROGRAM_NAME, MAX_USER_PRIORITY, MAX_USER_PRIORITY );
}


int get_new_priority(int adjustment)
{
	int current_priority;
	int new_priority;
	
	errno=0;
	current_priority = getpriority(PRIO_PROCESS, 0);
	if(errno != 0)
	{
		perror_exit(PROGRAM_NAME ", getpriority");
	}
	
	new_priority = current_priority + adjustment;
	if(new_priority < current_priority)
	{
		if(current_priority < MAX_USER_PRIORITY)
		{
			new_priority = current_priority;
		} else
		{
			if(new_priority < MAX_USER_PRIORITY)
			{
				new_priority = MAX_USER_PRIORITY;
			}
		}
	}
	if(new_priority > 19)
	{
		new_priority = 19;
	}
	
	return new_priority;
}

