/*---------------------------------------------------------------------------
 * ThinkAlert - A small program to manipulate the ThinkLight.
 *    Based on a "stupid little hack to blink the ThinkLight"
 *    http://paste.lisp.org/display/37500
 *
 * New features:
 *    - Interval argument is optional
 *    - Turn the light on or off (and leave it that way)
 *    - Specify separate values for on/off periods
 *    - Restores the light back to its initial state before terminating
 *    - Drops privileges after opening ThinkLight interface
 *
 *-------------------------------------------------------------------------*/
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/types.h>
#include <unistd.h>

void blink(int, int, int);
void dropPrivs();
void lightOn();
void lightOff();
void restoreState();
void saveState();
void terminate(int);

const char filename[] = "/proc/acpi/ibm/light";
int initialState = 0;	// Initial state of the ThinkLight
FILE *thinklight;			// Handle to the ThinkLight proc interface.

#define DEFAULT_ON_PERIOD		250000
#define DEFAULT_OFF_PERIOD		250000


// ThinkAlert
int main(int argc, char *argv[]) {

	int noBlink = 0;	// Whether light should be blinked or switched
	int onPeriod;		// Duration of a blink (microseconds)
	int offPeriod;		// Interval between blinks (microseconds)
	int switchOn = 0;	// Whether light should be switched on or off
	int times;			// Number of times to blink

	// Attempt to open the ThinkLight interface.
	thinklight = fopen(filename, "r+");
	if (!thinklight) {
		perror("Unable to open the ThinkLight interface");
		return errno;
	}
	dropPrivs();

	switch (argc - 1) {
		case 1:
			if (!strcmp(argv[1], "on")) {
				noBlink = 1;
				switchOn = 1;
			} else if (!strcmp(argv[1], "off")) noBlink = 1;
			else {
				times = atoi(argv[1]);
				onPeriod = DEFAULT_ON_PERIOD;
				offPeriod = DEFAULT_OFF_PERIOD;
			}
			break;
		case 2:
			times = atoi(argv[1]);
			onPeriod = offPeriod = atoi(argv[2]);
			break;
		case 3:
			times = atoi(argv[1]);
			onPeriod = atoi(argv[2]);
			offPeriod = atoi(argv[3]);
			break;
		default:
			printf("thinkalert <on|off>\n");
			printf("thinkalert <times> [interval (microseconds)]\n");
			printf("thinkalert <times> <on period (microseconds)> "
				"<off period (microseconds)>\n");
			fclose(thinklight);
			exit(0);
	}

	// Just turn the light on or off and leave it that way.
	if (noBlink) {
		if (switchOn) lightOn();
		else lightOff();
		fclose(thinklight);
	}

	// Blink the light.
	else {
		saveState();
		signal(SIGINT, terminate);
		blink(times, onPeriod, offPeriod);
		terminate(0);
	}
	return 0;
}


// Blink the light a number of times.
void blink(int times, int onPeriod, int offPeriod) {

	if (!times) return;		// Blink zero times.

	int t = times;
	while (t--) {

		// Only shine the first time if the light was initially off.
		if ((t < (times - 1)) || !initialState) {
			lightOn();
			usleep(onPeriod);
		}

		// Only shade the last time if the light was initially on.
		if (t || initialState) {
			lightOff();
			usleep(offPeriod);
		}
	}
}


// Drop root privileges.  This code is Linux specific.  Adapted from Secure
// Programming Cookbook for C and C++.
void dropPrivs() {
	gid_t newgid = getgid(), oldgid = getegid();
	uid_t newuid = getuid(), olduid = geteuid();

	// Drop ancillary group memberships.
	if (!olduid) setgroups(1, &newgid);

	// Set the effective gid to the real gid.
	if ((newgid != oldgid) && (-1 == setregid(newgid, newgid))) abort();

	// Set the effective uid to the real uid.
	if ((newuid != olduid) && (-1 == setreuid(newuid, newuid))) abort();

	// Verify that the changes were successful.
	if (newgid != oldgid && (-1 != setegid(oldgid) || getegid() != newgid))
		abort();
	if (newuid != olduid && (-1 != seteuid(olduid) || geteuid() != newuid))
		abort();
}


// Turn the light on.
void lightOn() {
	rewind(thinklight);
	fprintf(thinklight, "on");
	fflush(thinklight);
}


// Turn the light off.
void lightOff() {
	rewind(thinklight);
	fprintf(thinklight, "off");
	fflush(thinklight);
}


// Restore the initial state of the ThinkLight.
void restoreState() {
	if (initialState) lightOn();
	else lightOff();
}


// Get the initial state of the ThinkLight.
void saveState() {
	char status;
	fseek(thinklight, 10, SEEK_SET);
	fread(&status, 1, 1, thinklight);
	if ('n' == status) initialState = 1;
}


// Terminate the program.
void terminate(int sig) {
	restoreState();
	fclose(thinklight);
	exit(sig);
}

