/*---------------------------------------------------------------------------
 * 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);
}