
/*
* battery-sleep.c
* Spawns child processes, and stops them when the system is unplugged
* (to maximize battery life) and restarts them when external power is restored.
* This is mainly because the folding-at-home client won't do it itself.
* Compile: cc -g -o battery-sleep battery-sleep.c -framework CoreFoundation -framework IOKit
* Crufted together by Perette Barella from some Apple sample code,
* the job-control example from http://www.erlenstar.demon.co.uk/unix/faq_2.html,
* and some of my own knowledge.
* Incidentally, there's a plist file in this directory that you can put
* into ~/Library/LaunchAgents in your home directory and it will launch
* that folding at home client at boot.  You'll need to correct some
* paths in the file, but it should be obvious what to do.  See launchd(8).
*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <errno.h>

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/ps/IOPowerSources.h>
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/IOKitLib.h>

char *prog_name;
int debug = false;
int child_process = 0;
double cpu_temp_limit = 56.0;
double cpu_low_threshold = 0.0;
double load_average_threshold = 1.2;
int any_AC_power = false;
int was_running = true;
CFStringRef temperature_sensor;
int modulation_off = 1;
int modulation_on = 1;

// Temperature retrieving function
// Copyright 2008-2011 Perette Barella
// In this function, we're going to make extensive use of IOKitLib to
// walk the IORegistry, which is where the temperature data is.
// There are functions to query a name/value pair, but the problem
// is we want a name/value pair from a specific temperature sensor.
// Thus, we need to locate the sensor ourselves.
// The parameter is the sensor name, like "CPU BOTTOMSIDE".

#define TEMPERATURE_INVALID (0.0)
double get_temperature_sensor (CFStringRef sensor_location) {
	kern_return_t status;
	mach_port_t master_port;

    // Open a connection to the IOKit master port.
    // The master port doesn't need to be freed.
    status = IOMasterPort(MACH_PORT_NULL, &master_port);
    if (status != KERN_SUCCESS) {
	return(TEMPERATURE_INVALID);
    }


    // Get an iterator for walking the power plane.
    io_iterator_t walker;
    status = IORegistryCreateIterator (master_port,
		    kIOPowerPlane,
		    kIORegistryIterateRecursively,
		    &walker);
    if (walker == MACH_PORT_NULL) {
	return (TEMPERATURE_INVALID);
    }

    // Alright, we've got the iterator.  Now, walk the bastard and find
    // an entry with "location" => "CPU BOTTOMSIDE" (or whatever sensor was
    // requested).
    CFDataRef answer = NULL;
    long temperature;
    CFStringRef location;
    io_object_t registry_entry;
    while ((registry_entry = IOIteratorNext (walker)) != 0 && (answer == 0)) {
	location = IORegistryEntrySearchCFProperty(registry_entry,
		kIOPowerPlane,CFSTR("location"), NULL, 0);
	if (location) {
	    // CFShow(location);
	    // CFEqual handles possibility of differing types
	    if (CFEqual (sensor_location, location)) {
		answer = IORegistryEntrySearchCFProperty (registry_entry,
			kIOPowerPlane, CFSTR("current-value"), NULL, 0);
		// Check that what we got is really a number.
		if (answer) {
		    if (CFNumberGetTypeID () == CFGetTypeID (answer)) {
			CFNumberGetValue ((CFNumberRef) answer,
					     kCFNumberLongType, &temperature);
		    }
		    CFRelease (answer);
		}
	    }
	    CFRelease (location);
	}
	IOObjectRelease (registry_entry);
    }
    IOObjectRelease (walker);

    return(answer ? temperature / 65536.0 : TEMPERATURE_INVALID);
}



// This polling function checks to see if the
// temperature is too hot or cool enough.
void check_temperature(CFRunLoopTimerRef timer, void *context) {
	if (debug)
		CFShow(CFSTR("Time to check the temperature:\n"));
	// If the machine is plugged in, then we need to check the
	// temperature.  Otherwise, skip it.
	if (any_AC_power) {
		double temperature = get_temperature_sensor (temperature_sensor);
		if (temperature == TEMPERATURE_INVALID) {
			CFShow (CFSTR ("Can't read temperature sensor."));
			return;
		}
		if (debug)
			printf ("It's %f C in here!\n", temperature);
		if (temperature < cpu_low_threshold) {
			// If we're getting cold, start.
			if (!was_running) {
				if (debug)
					CFShow (CFSTR("Cooled, restarting."));
				was_running = true;
				kill (-child_process, SIGCONT);
			}
		} else if (temperature > cpu_temp_limit) {
			// If we're too hot, stop.
			if (was_running) {
				if (debug)
					CFShow (CFSTR("Hot, stopping"));
				was_running = false;
				kill (-child_process, SIGSTOP);
			}
		// and if we're in the intermediate range, don't do anything.
		}
	}
}



// This polling function checks to see if the
// load average is too high.  If not, it modules the
// child process on/off to regulate system temperature.
void do_modulation(CFRunLoopTimerRef timer, void *context) {
	static int modulation_state = 0;
	double load_average;
	int should_run;
	if (debug)
		CFShow(CFSTR("Modulating the process:\n"));
	// If the machine is plugged in, then we need to check the
	// temperature.  Otherwise, skip it.
	if (any_AC_power) {
		// Only let process run if the load average is low enough.
		if (getloadavg (&load_average, 1) != -1) {
			should_run = (load_average < load_average_threshold);
		} else {
			should_run = 1;
		}
		if (should_run) {
			// If the load is okay, modulate.
			modulation_state = (modulation_state + 1) % (modulation_on + modulation_off);
			should_run = modulation_state >= modulation_off;
		} else {
			// When the load drops, modulate on first.
			modulation_state = modulation_off - 1;
		}

		if (should_run) {
			// If we're getting cold, start.
			if (!was_running) {
				if (debug)
					CFShow (CFSTR("Modulation start."));
				was_running = true;
				kill (-child_process, SIGCONT);
			}
		} else {
			// If we're too hot, stop.
			if (was_running) {
				if (debug)
					CFShow (CFSTR("Modulation stop."));
				was_running = false;
				kill (-child_process, SIGSTOP);
			}
		}
	}
}
		
		
// This event handler is called once on startup, then on a
// power source change event.
void ps_change(void *in)
{
	CFTypeRef	blob = NULL;
	CFDictionaryRef	one_ps = NULL;
	CFArrayRef	list = NULL;
	CFStringRef	powerSource;
	int		i, count;
	
	if (debug)
		CFShow(CFSTR("Power source has changed:\n"));
	blob = IOPSCopyPowerSourcesInfo();
	list = IOPSCopyPowerSourcesList(blob);
	if (debug)
		CFShow(list);
	count = CFArrayGetCount(list);
	any_AC_power = false;
	for(i=0; i<count; i++)
	{
		one_ps = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(list, i));
		if(!one_ps) break;
		powerSource = CFDictionaryGetValue(one_ps, CFSTR(kIOPSPowerSourceStateKey));
		/* CFShow (powerSource); */
		
		/* If the power source is AC, then record it. */
		if (CFStringCompare (powerSource, CFSTR(kIOPSACPowerValue), 0) == kCFCompareEqualTo) {
			any_AC_power = true;
		}
	}
	CFRelease(blob);
	CFRelease(list);
	
	if (any_AC_power != was_running) {
		if (was_running) {
			if (debug)
				CFShow (CFSTR("Unplugged, stopping"));
			kill (-child_process, SIGSTOP);
		} else {
			if (debug)
				CFShow (CFSTR("Plugged in, starting"));
			kill (-child_process, SIGCONT);
		}
		was_running = any_AC_power;
	}
}


// This signal handler is called if the child sends a signal.
// Check if the child has exited, and if so, follow suit.
void child_signal_handler (int sig) {
	int child_status;
	
	if (waitpid (child_process, &child_status, WNOHANG) > 0) {
		if (WIFEXITED (child_status) || WIFSIGNALED (child_status)) {
			exit (0);
		}
	}
}


// This signal handler is called when we get a SIGINT.
// Make sure the children processes are running (not stopped),
// then pass the signal through so they can process it and
// shutdown cleanly.
void sigint_handler (int signal) {
	int child_status;

	kill (-child_process, SIGCONT);
	kill (child_process, signal);
	was_running = true;
}

void usage (void) {
	fprintf (stderr, "%s [options] <program> [program parameters]\n"
		"Program is started with any parameters specified.\n"
		"If temperature sensor is unavailable, processes are modulated\n"
		"on and off to perform thermoregulation."
		"Options:\n"
		"\t-t temp     Temperature limit (default %.2f C), 0 to disable\n"
		"\t-i interval Temperature check interval\n"
		"\t-r restart  Temperature at which to restart process\n"
		"\t-l load     Load average at which process stops when modulating\n"
		"\t-s sensor   Name/location of sensor to monitor\n"
		"\t-m offtime  Process disabled periods per modulation\n"
		"\t-M ontime   Process run periods per modulation\n",
		prog_name, cpu_temp_limit);
	exit (1);
}


int number_of_cpus (void) {
	int mib [2];
	int num_cpus;
	size_t size;
	

	mib [0] = CTL_HW;
	mib [1] = HW_NCPU;
	size = sizeof (num_cpus);
	if (sysctl (mib, 2, &num_cpus, &size, NULL, 0) == -1)
		return (1);
	return num_cpus;
}



int main (int argc, char * const *argv) {
	struct sigaction act;
	int option;
	int temperature_interval = 0;
	temperature_sensor = CFSTR("CPU BOTTOMSIDE");
	prog_name = *argv;
	load_average_threshold *= number_of_cpus();

	while ((option = getopt (argc, argv, "i:t:r:s:M:m:l:Z")) > 0) {
	    switch (option) {
		case 'i':
			temperature_interval = atoi (optarg);
			break;
		case 't':
			cpu_temp_limit = atof (optarg);
			break;
		case 'm':
			modulation_off = atoi (optarg);
			break;
		case 'M':
			modulation_on = atoi (optarg);
			break;
		case 'r':
			cpu_low_threshold = atof (optarg);
			break;
		case 's':
			temperature_sensor = CFStringCreateWithCString (
				NULL, optarg, kCFStringEncodingUTF8);
			break;
		case 'l':
			load_average_threshold = atof (optarg);
		case 'Z':
			debug = true;
			break;
		default:
			usage ();
	    }
	}
	argc -= optind;
	argv += optind;

	if (argc < 1)
		usage ();

	switch (child_process = fork())
	{
		case -1: /* fork failure */
		    return 1;

		case 0: /* child */
			/* Lower process priority */
			setpriority (PRIO_PROCESS, 0, 20);

			/* Assign the child process to its task. */
			/* Set up child with a process group using new PID number. */
			child_process = getpid ();
			setpgid(0, child_process);
			execvp (*argv, argv);
			perror (*argv);
		    return 1;
	}

	act.sa_handler = sigint_handler;
	act.sa_flags = 0;
	sigemptyset(&act.sa_mask);
	sigaction(SIGINT,&act,NULL);
	sigaction(SIGTERM,&act,NULL);

	act.sa_handler = child_signal_handler;
	act.sa_flags = SA_RESTART;
	sigaction(SIGCHLD,&act,NULL);

	sleep (3);
	ps_change ((void *) NULL);
	
	/* Create the power change event handler */
	CFRunLoopSourceRef		rls = NULL;
	/* CFShow(CFSTR("Hello, World!\n")); */

	rls = IOPSNotificationCreateRunLoopSource(ps_change, NULL);
	/* CFShow(CFSTR("1\n")); */

	CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
	/* CFShow(CFSTR("2\n")); */

	CFRelease(rls);
	/* CFShow(CFSTR("3\n")); */

	/* Create the timer event handler for checking the CPU temperature */
	if (cpu_low_threshold == 0.0)
		cpu_low_threshold = cpu_temp_limit - 1.0;
	if (cpu_temp_limit > 0) {
		double temperature = get_temperature_sensor (temperature_sensor);
		CFRunLoopTimerRef timer = NULL;
		if (temperature == TEMPERATURE_INVALID) {
			CFShow (CFSTR ("Can't read temperature sensor - using modulating algorithm"));
			timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
				time(0) - kCFAbsoluteTimeIntervalSince1970 + 10,
				11 /* seconds per interval */,
				0, 0, do_modulation, NULL);
		} else {
			timer = CFRunLoopTimerCreate (kCFAllocatorDefault,
				time(0) - kCFAbsoluteTimeIntervalSince1970 + 10,
				temperature_interval ? temperature_interval : 61,
				0, 0, check_temperature, NULL);
		}
		CFRunLoopAddTimer (CFRunLoopGetCurrent(),
				   timer, kCFRunLoopDefaultMode);
		CFRelease (timer);
	}

	CFRunLoopRun();
	/* CFShow(CFSTR("4\n")); */

	return 0;
}

