/*
 * This is free and unencumbered software released into the public domain.
 *
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 *
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * For more information, please refer to <http://unlicense.org/>
 *
 **************************************************************************
 *
 * To compile:
 *   Console mode: gcc -Wall -O3 -o hddled hddled.c
 *   X mode:       gcc -Wall -O3 -DX -lX11 -o hddled hddled.c
 *
 * Options:
 * -d, --detach               Detach from terminal
 * -r, --refresh=VALUE        Refresh interval (default: 50 ms)
 *
 * Console options:
 * -C, --console=DEVICE       Console device (default: /dev/console)
 *
 * X options:
 * -D, --display=DISPLAY      X display (default: $DISPLAY)
 */

#define PACKAGE_STRING    "hddled 0.3"
#define PACKAGE_BUGREPORT "hddled@very.puzzling.org"

#define VMSTAT "/proc/vmstat"

#define _GNU_SOURCE

#include <argp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#ifdef X
# include <X11/X.h>
# include <X11/Xlib.h>
# define SCROLL_LOCK 3

/* X display connection */
typedef Display *data_t;
static char *o_display = NULL;

#else
# include <fcntl.h>
# include <linux/kd.h>
# include <sys/ioctl.h>
# define CONSOLE "/dev/console"

/* File descriptor */
typedef int data_t;
static char *o_console = NULL;

#endif

const char *argp_program_version = PACKAGE_STRING;
const char *argp_program_bug_address = "<" PACKAGE_BUGREPORT ">";

static unsigned int o_refresh = 50; /* milliseconds */
static int o_detach = 0;

static volatile sig_atomic_t running = 1;
static char *line = NULL;
static size_t len = 0;

/* Reread the vmstat file */
int
activity(FILE *vmstat) {
	static unsigned int prev_pgpgin, prev_pgpgout;
	unsigned int pgpgin, pgpgout;
	int found_pgpgin, found_pgpgout;
	int result;

	/* Reload the vmstat file */
	result = TEMP_FAILURE_RETRY(fseek(vmstat, 0L, SEEK_SET));
	if (result) {
		perror("Could not rewind " VMSTAT);
		return result;
	}

	/* Clear glibc's buffer */
	result = TEMP_FAILURE_RETRY(fflush(vmstat));
	if (result) {
		perror("Could not flush input stream");
		return result;
	}

	/* Extract the I/O stats */
	found_pgpgin = found_pgpgout = 0;
	while (getline(&line, &len, vmstat) != -1 && errno != EINTR) {
		if (sscanf(line, "pgpgin %u", &pgpgin))
			found_pgpgin++;
		else if (sscanf(line, "pgpgout %u", &pgpgout))
			found_pgpgout++;
		if (found_pgpgin && found_pgpgout)
			break;
	}
	if (!found_pgpgin || !found_pgpgout) {
		fprintf(stderr, "Could not find required lines in " VMSTAT);
		return -1;
	}

	/* Anything changed? */
	result =
		(prev_pgpgin  != pgpgin) ||
		(prev_pgpgout != pgpgout);
	prev_pgpgin = pgpgin;
	prev_pgpgout = pgpgout;

	return result;
}

/* Update the keyboard LED */
void
led(data_t data, int on) {
	static int current = 1; /* Ensure the LED turns off on first call */
	if (current == on)
		return;

#ifdef X
	{
		static XKeyboardControl control;
		control.led = SCROLL_LOCK;
		control.led_mode = on ? LedModeOn : LedModeOff;
		XChangeKeyboardControl(data, KBLed | KBLedMode, &control);
		XFlush(data);
	}
#else
	{
		unsigned long state;
		ioctl(data, KDGETLED, &state);
		if (on)
			state |= LED_SCR;
		else
			state &= ~LED_SCR;
		ioctl(data, KDSETLED, state);
	}
#endif

	current = on;
}

/* Signal handler -- break out of the main loop */
void
shutdown(int sig) { running = 0; }

/* Argp parser function */
error_t
parse_options(int key, char *arg, struct argp_state *state) {
	switch (key) {
	case 'd':
		o_detach = 1;
		break;
	case 'r':
		o_refresh = strtol(arg, NULL, 10);
		if (o_refresh < 10)
			argp_failure(state, EXIT_FAILURE, 0,
				"refresh interval must be at least 10");
		break;
#ifdef X
	case 'D':
		o_display = strdup(arg);
		break;
#else
	case 'C':
		o_console = strdup(arg);
		break;
#endif
	}
	return 0;
}

int
main(int argc, char **argv) {
	struct argp_option options[] = {
		{  "detach", 'd',      NULL, 0, "Detach from terminal" },
		{ "refresh", 'r',   "VALUE", 0, "Refresh interval (default: 50 ms)" },
#ifdef X
		{ "display", 'D', "DISPLAY", 0, "X display (default: $DISPLAY)", 1 },
#else
		{ "console", 'C', "DEVICE",  0, "Console device (default: " CONSOLE ")", 1 },
#endif
		{ 0 },
	};
	struct argp parser = {
		NULL, parse_options, NULL,
		"Show hard disk activity using the Scroll Lock LED.",
		NULL, NULL, NULL
	};
	int status = EXIT_FAILURE;
	FILE *vmstat = NULL;
	data_t data = 0;
	struct timespec delay;

	/* Parse the command-line */
	parser.options = options;
	if (argp_parse(&parser, argc, argv, ARGP_NO_ARGS, NULL, NULL))
		goto out;

	delay.tv_sec = o_refresh / 1000;
	delay.tv_nsec = 1000000 * (o_refresh % 1000);

#ifdef X
	/* Open the X display */
	data = XOpenDisplay(o_display);
	if (data == NULL) {
		if (o_display)
			fprintf(stderr, "Could not open display: %s\n", o_display);
		else
			fprintf(stderr, "Could not open default display\n");
		goto out;
	}
#else
	/* Open the console */
	data = open(o_console ? o_console : CONSOLE, O_RDONLY);
	if (data < 0) {
		fprintf(stderr, "Could not open console: %s: %s\n",
			o_console ? o_console : CONSOLE, strerror(errno));
		goto out;
	}
#endif

	/* Open the vmstat file */
	vmstat = fopen(VMSTAT, "r");
	if (!vmstat) {
		perror("Could not open " VMSTAT " for reading");
		goto out;
	}

	/* Ensure the LED is off */
	led(data, 0);

	/* Save the current I/O stat values */
	if (activity(vmstat) < 0)
		goto out;

	/* Detach from terminal? */
	if (o_detach) {
		pid_t child = fork();
		if (child < 0) {
			perror("Could not detach from terminal");
			goto out;
		}
		if (child) {
			/* I am the parent */
#ifdef X
			/* Don't close the display on my child */
			data = NULL;
#endif
			status = EXIT_SUCCESS;
			goto out;
		}
	}

	/* We catch these signals so we can clean up */
	{
		struct sigaction action;
		memset(&action, 0, sizeof(action));
		action.sa_handler = shutdown;
		sigemptyset(&action.sa_mask);
		action.sa_flags = 0; /* We block on usleep; don't use SA_RESTART */
		sigaction(SIGHUP, &action, NULL);
		sigaction(SIGINT, &action, NULL);
		sigaction(SIGTERM, &action, NULL);
	}

	/* Loop until signal received */
	while (running) {
		int a;
		if (nanosleep(&delay, NULL) < 0)
			break;
		a = activity(vmstat);
		if (a < 0)
			break;
		led(data, a);
	}

	/* Ensure the LED is off */
	led(data, 0);

	status = EXIT_SUCCESS;

out:
	if (line) free(line);
	if (vmstat) fclose(vmstat);
#ifdef X
	if (data) XCloseDisplay(data);
	if (o_display) free(o_display);
#else
	if (data) close(data);
	if (o_console) free(o_console);
#endif

	return status;
}
