/* apmd.c -- APM event-monitoring daemon
 * Created: Mon Jan  8 14:29:18 1996 by faith@acm.org
 * Revised: Fri Dec 26 21:38:28 1997 by faith@acm.org
 * Copyright 1996, 1997 Rickard E. Faith (faith@acm.org)
 *
 * 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, 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.,
 * 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * Changes to support pre_suspend, post_resume and low_battery commands
 * based on patches from Bjoern Kriews (bkr@cut.de), 1996/12/24.
 *
 */

/*
 * Changes to support ac_offline, ac_online commands to
 * execute when the power status changes.
 * 1999/2/24  J.D. Smith - comments to jdsmith@alum.mit.edu.
 * This is useful to set hard drive spindown rates and other
 * configureable power saving options when the ac cord is 
 * inserted or removed.  
 *
 * New options:                              long form:
 *  -a ac_online_cmd                         ac_online
 *  -b ac_offline_cmd                        ac_offline
 */

/*
 * Changes to support centralized dispatch routines, generalized logging,
 * general code cleanup, add support for apm_reject and APM_CAPABILITY_CHANGE.
 * Craig Markwardt, craigm@lheamail.gsfc.nasa.gov, 1999 May 21
 */

/*
 * Reduced spurious and repeated syslog messages, as well as the number of
 * times the proxy gets called during charging/discharging.  Minor cleanup and
 * commenting.  DEPRECATED_OPTIONS support is grouped together for easier
 * removal.  Documentation updated.  Power status reporting now goes by AC
 * status only.
 * - David Brownell, db@post.harvard.edu, 14 June 1999
 */


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <time.h>
#include <syslog.h>
#include <signal.h>
#include <paths.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include "apm.h"
#include <errno.h>
#include <string.h>
#include <sys/wait.h>


#define	DEPRECATED_OPTIONS 	/* most such options came in 3.0 beta cycle */
// #define APM_TRACE 5		/* enable to compile in debug tracing */
// #define APM_REJECT_ENABLED 	/* userland can reject standby/suspend reqs? */
// #define APMD_NO_STRICT_PROXY	/* enable to prevent proxy security checks */


#ifdef APM_TRACE
#define ADEBUG(lev,args...) \
    if (APM_TRACE>=lev) syslog(LOG_DEBUG, __FUNCTION__ ": " args)
#else
#define ADEBUG(args...)
#endif

#define PID_FILE _PATH_VARRUN "apmd.pid"

#define MAX_EVENTS   8		/* Maximum events we accept from BIOS driver */
#define RESUME_HOURS 6		/* If resuming after N hours, show as days */

/* These are "synthetic" APM events generated by APMD itself.  The
 * benefit is that we can do a simple lookup into the event dispatch
 * table.  (We're trusting that the APM BIOS spec doesn't get updated
 * to use these particular "reserved" codes for anything...)
 */
#define APMD_START		0xad00	/* Daemon startup */
#define APMD_STOP		0xad01	/* Daemon shutdown */
#define APMD_BATTERY		0xad02	/* Battery charging update */
#define APMD_SYNTHETIC_CHECK	0xad03	/* Check power */
#define APMD_INVALID		0xad0f	/* A non-event for completeness */

/* This table defines how events are handled.  For each event type, as
 * listed on the left, there are two entries, one for "proxy" and one
 * for "log".  If a proxy entry is 1, then the corresponding event
 * causes the user proxy program to run (zero prevents the program
 * from running).  Similarly, if a log entry is 1 then the
 * corresponding event is logged.
 */

static int apmd_action[][3] =
{				/* proxy  log */
    {APM_SYS_STANDBY, 1, 1},	/* potentially too frequent */
    {APM_SYS_SUSPEND, 1, 1},
    {APM_NORMAL_RESUME, 1, 1},
    {APM_CRITICAL_RESUME, 1, 1},
    {APM_LOW_BATTERY, 1, 1},
    {APM_POWER_STATUS_CHANGE, 1, 1},
	/* update time -- handled by recent APM BIOS drivers, and
	 * may happen too often to fork processes or log in any case */
    {APM_UPDATE_TIME, 0, 0},
	/* critical suspend -- no time to do anything except suspend! */
    {APM_CRITICAL_SUSPEND, 0, 0},
    {APM_USER_STANDBY, 1, 1},
    {APM_USER_SUSPEND, 1, 1},
    {APM_STANDBY_RESUME, 1, 1},	/* potentially too frequent */
#ifdef APM_CAPABILITY_CHANGE
    {APM_CAPABILITY_CHANGE, 1, 1},
#endif

    /* These are synthesized by apmd */
    {APMD_START, 1, 1},
    {APMD_STOP, 1, 1},
    {APMD_BATTERY, 1, 1},
    {APMD_SYNTHETIC_CHECK, 0, 0}
};
#define apmd_num_actions (sizeof(apmd_action)/(3*sizeof(int)))

/* From parsing the command line: */
static int verbose = 0;
static int quiet = 0;
static int wall = 0;
static int percent_change = 5;		/* lot every 5% change */
static int warn_level = 10;		/* start warning at 10% remaining */
#ifdef		DEPRECATED_OPTIONS
static char *pre_suspend_cmd = NULL;
static char *post_resume_cmd = NULL;
static char *low_battery_cmd = NULL;
static char *ac_online_cmd = NULL;
static char *ac_offline_cmd = NULL;
#endif		/* DEPRECATED_OPTIONS */
static int check_power_time = -1;	/* seconds between /proc/apm checks */
static char *apmd_proxy = APMD_PROXY_NAME;

/* From initialization: */
static uid_t apmd_uid = 0;
static int apmd_fd = -1;

/* State collected during daemon operation: */
static time_t pre_suspend_time = 0;	/* Time at onset of suspend/standby */
static time_t post_suspend_time = 0;	/* Time upon resume suspend/standby */
static int pre_suspend_percentage = 0;	/* Battery level before suspend */


/* A question of notation:
 * MINOR CHECKPOINT - occurs whenever a notification is passed to
 * the daemon and a message is printed here.
 * The minor checkpoint reflects the last time
 * a log entry was printed.
 * Time and battery percentage are recorded.
 * MAJOR CHECKPOINT - occurs upon startup and APM resume functions,
 * or when charging/discharging is complete.
 * Reflects the last time a major power-related
 * status change occurred, especially when the time
 * has been disrupted by a standby or suspend.
 */
static time_t major_checkpoint = 0;	/* Updated at init/resume/full/... */
static time_t minor_checkpoint = 0;	/* Updated at power status logging */
static int major_percentage = -1;
static int minor_percentage = -1;
static int last_charging_status = -1;

#ifndef abs
#define abs(a) ((a)<0?(-(a)):(a))
#endif

#define IS_CHARGING(i) ( ((i).battery_status == 3) || ((i).battery_flags & 8) )


static void usage(void)
{
    fprintf(stderr,
	    "usage: apmd [-c seconds] [-P apmd_proxy] [-p percent] [-qVvW] "
					"[-w percent] [-?]\n"
#ifdef	DEPRECATED_OPTIONS
	    "deprecated options: [-a ac_online_cmd] [-b ac_offline_cmd]\n"
	    "        [-l low_battery_cmd] [-r post_resume_cmd] "
	    				"[-s pre_suspend_cmd] [-u]\n"
#endif
					);
    exit(1);
}

static void warn(const char *message)
{
    FILE *str;

    syslog(LOG_ALERT, "%s", message);
    if (wall)
    {
	str = popen("wall", "w");
	fprintf(str, "%s\n", message);
	pclose(str);
    }
}

static char *ac_descr (int code)
{
    if (code == 0)
	return "Battery";
    else if (code == 1)
	return "AC";
    else if (code == 2)
	return "Backup";
    else
	return "???";
}


/* Generic logging function.  Depending on the event type and dispatch
 * table, it may or may not be logged.
 */
static void apmd_log(apm_event_t event, char *msg)
{
    int i;

    /* Scan through dispatch table looking for this event type */
    for (i = 0; i < apmd_num_actions; i++)
	if ((apm_event_t) apmd_action[i][0] == event)
	    break;

    /* Event is not found */
    if (i == apmd_num_actions || !apmd_action[i][2])
	return;

    /* Success.  Log it. */
    syslog(LOG_INFO, msg);
}


/* apmd_call_proxy() is a generic outcalling dispatcher.  It calls a
 * proxy program, defined by apmd_proxy, which can do further event
 * processing.  When the kernel APM BIOS driver supports rejection of
 * suspend and standby events from userland, the return code from
 * apmd_proxy can be used as well. 
 *
 * adapted from cardmgr.c in PCMCIA package
 */
static int apmd_call_proxy(apm_event_t event, apm_info * apmi)
{
    int ret;
    char cmd[256 + 100];
    int i;
    unsigned int m;
    FILE *f;
    struct stat buf;
    char line[256];

    ADEBUG(4, "0x%04x\n", event);
    /* If the proxy flag is not set, then return 0 indicating that
     * the event is acceptable
     */
    for (i = 0; i < apmd_num_actions; i++)
	if ((apm_event_t) apmd_action[i][0] == event)
	    break;
    /* Event is not found or proxy shouldn't be called */
    if (i == apmd_num_actions || !apmd_action[i][1])
	return 0;
    
    cmd[0] = 0;

#ifdef	DEPRECATED_OPTIONS
    /* Agghh.  The special cases of legacy compatibility.  We have
     * to bypass the proxy if the user specified one of the
     * pre-existing commands. 
     */
    if ((event == APM_USER_SUSPEND || event == APM_SYS_SUSPEND)
	&& pre_suspend_cmd && *pre_suspend_cmd)
	strcpy(cmd, pre_suspend_cmd);

    else if (event == APM_NORMAL_RESUME
	     && post_resume_cmd && *post_resume_cmd)
	strcpy(cmd, post_resume_cmd);

    else if (event == APM_LOW_BATTERY
	     && low_battery_cmd && *low_battery_cmd)
	strcpy(cmd, low_battery_cmd);

    else if (event == APM_POWER_STATUS_CHANGE
	     && apmi && apmi->ac_line_status == 0
	     && ac_offline_cmd && *ac_offline_cmd)
	strcpy(cmd, ac_offline_cmd);

    else if (event == APM_POWER_STATUS_CHANGE
	     && apmi && apmi->ac_line_status == 1
	     && ac_online_cmd && *ac_online_cmd)
	strcpy(cmd, ac_online_cmd);
    
    else {
	/* OK -- no legacy to worry about, try the proxy.  */

#endif	/* DEPRECATED_OPTIONS */

    /* Check that the proxy file actually exists */
    if (stat(apmd_proxy, &buf) < 0)
	return 0;
    m = buf.st_mode;

    /* Security concerns?  The file should be owned and executable by
     * the owner of the daemon, and not writable by others.
     */
#ifndef APMD_NO_STRICT_PROXY
    if (!(buf.st_uid == apmd_uid && (m & S_IFREG) && (m & S_IXUSR))
	&& !(m & S_IWGRP) && !(m & S_IWOTH))
    {
	syslog(LOG_INFO, "%s failed (permissions)",
	       apmd_proxy);
	return 0;
    }
#endif

    strcpy(cmd, apmd_proxy);
    
    /* Add the arguments depending on event type */
    switch (event)
    {
    case APM_SYS_STANDBY:
	strcat(cmd, " standby system ");
	break;
    case APM_USER_STANDBY:
	strcat(cmd, " standby user ");
	break;
    case APM_SYS_SUSPEND:
	strcat(cmd, " suspend system ");
	break;
    case APM_USER_SUSPEND:
	strcat(cmd, " suspend user ");
	break;
    case APM_CRITICAL_SUSPEND:
	strcat(cmd, " suspend critical ");
	break;
    case APM_NORMAL_RESUME:
	strcat(cmd, " resume suspend ");
	break;
    case APM_STANDBY_RESUME:
	strcat(cmd, " resume standby ");
	break;
    case APM_CRITICAL_RESUME:
	strcat(cmd, " resume critical ");
	break;
    case APM_LOW_BATTERY:
	strcat(cmd, " change battery ");
	break;
    case APM_POWER_STATUS_CHANGE:
	strcat(cmd, " change power ");
	break;
    case APM_UPDATE_TIME:
	strcat(cmd, " change time ");
	break;
    case APMD_START:
	strcat(cmd, " start ");
	break;
    case APMD_STOP:
	strcat(cmd, " stop ");
	break;
#ifdef APM_CAPABILITY_CHANGE
    case APM_CAPABILITY_CHANGE:
	strcat(cmd, " change capability ");
	break;
#endif
    default:
	// should never happen !!
	return 0;
    }

#ifdef	DEPRECATED_OPTIONS
    }
#endif

    /* Redirect standard error to standard output */
    strcat(cmd, " 2>&1");

    /* Execute command and capture output */
    ADEBUG(5, "executing: '%s'", cmd);
    f = popen(cmd, "r");
    for (i = 0; fgets(line, 255, f) && i < 50; i++)
    {
	line[255] = '\0';
	ADEBUG(5, "+ %s", line);
    }
    ret = pclose(f);

    /* Collect the exit code */
    if (WIFEXITED(ret))
    {
	ADEBUG(5, "%s exited with status %d", cmd, WEXITSTATUS(ret));
	/* Return the exit status of the program */
	return WEXITSTATUS(ret);
    }
    else
	ADEBUG(3, "%s exited on signal %d", apmd_proxy, WTERMSIG(ret));
    return 0;
}

static inline int apmd_time(apm_info * apmi)
{
    return (apmi->battery_time * (apmi->using_minutes ? 60 : 1));
}

/* "reset" power means that we totally reinitialize our state variables
 * concerning power status
 */
static void apmd_power_reset(apm_event_t event, apm_info * apmi)
{
    char msg[512];

    ADEBUG(4, "0x%04x\n", event);
    if (last_charging_status == apmi->battery_status)
	return;

    /* Establish a major (and minor) checkpoint */
    time(&major_checkpoint);
    major_percentage = apmi->battery_percentage;
    minor_checkpoint = major_checkpoint;
    minor_percentage = major_percentage;
    last_charging_status = apmi->battery_status;

    sprintf(msg, "%s: * * * (%d%% %s)",
	    last_charging_status ? "Charge" : "Battery",
	    apmi->battery_percentage, apm_time_nosec(apmd_time(apmi)));
    apmd_log(APMD_BATTERY, msg);
}

/* apmd_init occurs once when the daemon starts up */
static void apmd_init(apm_info * apmi)
{
    syslog(LOG_INFO, "Version %s (APM BIOS %d.%d, Linux driver %s)",
	   VERSION,
	   apmi->apm_version_major, apmi->apm_version_minor,
	   apmi->driver_version);

    /* Call the proxy */
    apmd_call_proxy(APMD_START, apmi);
    /* Print battery status at beginning of run */
    apmd_power_reset(APMD_START, apmi);

}

/* Suspend handler.  It calls the dispatcher.  The dispatcher may reject
 * a suspend, in which case we must reply to the kernel driver anyway.
 */
static int apmd_suspend(apm_event_t event, apm_info * apmi)
{
    ADEBUG(4, "0x%04x\n", event);
    if (apmd_call_proxy(event, apmi))
    {
#ifdef APM_REJECT_ENABLED	/* If kernel rejection enabled */
	ADEBUG(5, "Suspend rejected\n");
	return apm_reject(apmd_fd);
#endif
    }

    /* Record current time and battery status */
    time(&pre_suspend_time);
    pre_suspend_percentage = apmi->battery_percentage;

    /* Logging is okay here since the sync() happens afterward */
    switch (event)
    {
    case APM_SYS_SUSPEND:
	apmd_log(event, "System Suspend");
	break;
    case APM_USER_SUSPEND:
	apmd_log(event, "User Suspend");
	break;
    }
    sync();
    sleep(0);			/* let syslogd write the message */
    sync();
    return apm_suspend(apmd_fd);
}

/* Standby handler.  It calls the dispatcher.  The dispatcher may reject
 * a standby, in which case we must reply to the kernel driver anyway.
 */
static int apmd_standby(apm_event_t event, apm_info * apmi)
{
    ADEBUG(4, "0x%04x\n", event);
    if (apmd_call_proxy(event, apmi))
    {
#ifdef APM_REJECT_ENABLED
	ADEBUG(5, "Standby rejected\n");
	return apm_reject(apmd_fd);
#endif
    }

    /* Record current time and battery status */
    time(&pre_suspend_time);
    pre_suspend_percentage = apmi->battery_percentage;

    /* Logging is okay here since the sync() happens afterward */
    switch (event)
    {
    case APM_SYS_STANDBY:
	apmd_log(event, "System Standby");
	break;
    case APM_USER_STANDBY:
	apmd_log(event, "User Standby");
	break;
    }
    return apm_standby(apmd_fd);
}

/* Resume handler.  Call the proxy, then update the battery status. 
 * This also handles the update time function, which does much of the
 * same thing. 
 */
static void apmd_resume(apm_event_t event, apm_info * apmi)
{
    char msg[512];
    int len = 0;

    ADEBUG(4, "0x%04x\n", event);
    apmd_call_proxy(event, apmi);

    /* Logging */
    switch (event)
    {
    case APM_NORMAL_RESUME:
	len = sprintf(msg, "Normal Resume");
	break;
    case APM_STANDBY_RESUME:
	len = sprintf(msg, "Standby Resume");
	break;
    case APM_CRITICAL_RESUME:
	len = sprintf(msg, "Critical Resume");
	break;
    case APM_UPDATE_TIME:
	len = sprintf(msg, "Update Time");
	break;
    }

    last_charging_status = apmi->battery_status;
    time(&post_suspend_time);

    /* Establish a minor and major checkpoint */
    major_checkpoint = post_suspend_time;
    major_percentage = apmi->battery_percentage;
    minor_checkpoint = post_suspend_time;
    minor_percentage = major_percentage;

    /* Update the battery status */
    if (event != APM_UPDATE_TIME && pre_suspend_time && post_suspend_time)
    {
	int dt = post_suspend_time - pre_suspend_time;
	int dp = apmi->battery_percentage - pre_suspend_percentage;

	if (dt > 0)
	    len += sprintf(msg + len, " after %s", apm_time(dt));

	/* describe in days if the suspend was for a long time */
	if (dt > 60 * 60 * RESUME_HOURS
	    && apmi->battery_percentage > 0 && pre_suspend_percentage > 0
	    && last_charging_status != 0x03 && dp < 0)
	{
	    len += sprintf(msg + len, ", %.2f%%/day",
			   ((double) dp / (double) dt) * 60. * 60. * 24.);
	}
    }

    len += sprintf(msg + len, " (%d%%%% %s) %s power",
		    apmi->battery_percentage,
		    apm_time_nosec(apmd_time(apmi)),
		    ac_descr(apmi->ac_line_status));
    apmd_log(event, msg);
}

/* "low" battery handler -- BIOS decides what "low" means, may send alert;
 * APMD has a more configurable notion, dealt with similarly.
 */
static void apmd_low_battery(apm_event_t event, apm_info * apmi)
{
    char msg[512];
    ADEBUG(4, "0x%04x\n", event);
    sprintf(msg, "Battery Low Notification from %s (%d%% %s)",
	    (event == APM_LOW_BATTERY ? "APM BIOS" : "apmd"),
	    apmi->battery_percentage, apm_time_nosec(apmd_time(apmi)));
    if (!quiet)
	warn(msg);
}

/* Check battery status, and decide whether to log or not. */
static void apmd_check_power(apm_event_t event, apm_info * apmi)
{
    int	dt, dp, logflag = 0, len = 0, warning = 0;
    int charging = IS_CHARGING(*apmi);
    char msg[512];
    double rate;

    static int last_battery_status = 0;
    static int last_ac_status = -1;

    ADEBUG(4, "0x%04x\n", event);

    /* Call proxy and perform special logging as needed */
    switch (event)
    {
    case APM_POWER_STATUS_CHANGE:
        /* Power status changes can happen a LOT ... e.g. for each change of
	 * estimated battery life (minutes, seconds, percent) when charging
	 * or discharging, battery full, empty, add/remove battery, etc.
	 * Invoking the proxy on each change can use up battery power fast,
	 *
	 * So we just report if AC status changes -- the main use for this is
	 * to make sure that power usage is reduced when running on batteries.
	 * (When the kernel tracks multiple batteries that might be a good
	 * time to update this to report just a bit more.)
	 */
	if (apmi->ac_line_status != last_ac_status) {
	    apmd_call_proxy(event, apmi);
	    sprintf(msg, "Now using %s Power", ac_descr(apmi->ac_line_status));
	    apmd_log(event, msg);
	    apmd_power_reset(event, apmi);
	    last_ac_status = apmi->ac_line_status;
	}
	break;
    case APM_LOW_BATTERY:
	apmd_call_proxy(event, apmi);
synthetic_low_battery:
	last_battery_status = 1;
	apmd_low_battery(event, apmi);
	break;
#ifdef APM_CAPABILITY_CHANGE
    case APM_CAPABILITY_CHANGE:
	apmd_call_proxy(event, apmi);
	apmd_log(event, "Capability Change");
	break;
#endif
    }

    /* Check for "low" battery status.  Note: this is not an infinite loop
     * because last_battery_status is guaranteed to change. 
     */
    if (!charging && last_battery_status != 1 && apmi->battery_status == 1)
    {
	goto synthetic_low_battery;
    }
    /* Reset the low battery state if we are now charging, etc. */
    if (charging || apmi->battery_status != 1)
    {
	last_battery_status = 0;
    }

    /* If the BIOS doesn't keep track of the battery, no point going further */
    if (apmi->battery_percentage < 0)
	return;

    /* If battery is not charging and battery percentage is changing
     * AND below apmd's threshold then give low battery warning.  Both
     * BIOS and APMD originate warnings, and generally at different
     * percentages; the BIOS one is fixed and not always useful.
     */
    if (!charging
	    && apmi->battery_percentage <= warn_level
	    && (abs(minor_percentage - apmi->battery_percentage) > 0
		|| apmi->battery_percentage == 0)
	    && !quiet)
	warning = 1;

    dt = time(0) - minor_checkpoint;
    dp = minor_percentage - apmi->battery_percentage;

    /* If no warning, or no change since last logged message, then do nothing */
    if (!warning && (dt == 0 || dp == 0))
	return;

    /* Decide whether to log the battery status.
     * First check is when battery is fully drained or charged
     */
    logflag = (apmi->battery_percentage != minor_percentage)
	&& ((apmi->battery_percentage == 0)
		|| (apmi->battery_percentage == 100));

    /* Second check is when battery percentage has decreased by 
     * percent_change.
     */
    logflag = logflag || (abs(minor_percentage - apmi->battery_percentage) >=
			  (percent_change > 1 ? percent_change : 1));
    
    /* Logging is triggered when any check is satisfied. */
    if (!logflag && !warning)
	return;

    /* We are logging (either normal, or a warning) so establish a minor
     * checkpoint -- reduces redundant logging, to no more than once per
     * percentage point change.
     */
    minor_percentage = apmi->battery_percentage;
    minor_checkpoint = time(0);
    rate = (double) dp / (double) dt;

    /* If we're warning about low batteries, do so ... */
    if (warning != 0) {
	sprintf(msg, "Battery warning (%d%% %s)",
	      apmi->battery_percentage, apm_time_nosec(apmd_time(apmi)));
	warn(msg);
	/* ... if that's all we're doing, finish */
	if (!logflag)
	    return;
    }

    /* Begin composing the log message.  First part is charging status,
     * followed by rate (in percent per minute)
     */
    len = sprintf(msg, "%s: %f",
		  charging ? "Charge" : "Battery",
		  rate * 60.0);

    /* Time from full charge, or time from last power status change (ie,
     * a major checkpoint)
     */
    if ((!charging && major_percentage == 100)
	|| (charging && major_percentage == 0))
	len += sprintf(msg + len, " %s", apm_time_nosec(dt));
    else
	len += sprintf(msg + len, " (%s)", apm_time_nosec(dt));

    /* Computation of predicted battery lifetime */
    if (charging)
    {
	len += sprintf(msg + len, " %s",
		 apm_time_nosec((int) ((100.0 - apmi->battery_percentage)
				       / -rate)));
    }
    else
    {
	len += sprintf(msg + len, " %s",
		apm_time_nosec((int) (apmi->battery_percentage / rate)));
    }

    sprintf(msg + len, " (%d%%%% %s)",
	    apmi->battery_percentage, apm_time_nosec(apmd_time(apmi)));
    apmd_log(APMD_BATTERY, msg);

    /* Establish a major checkpoint if we have become fully charged or
     * fully drained 
     */
    if ((apmi->battery_percentage == 0 && major_percentage != 0)
	|| (apmi->battery_percentage == 100 && major_percentage != 100))
    {
	major_percentage = apmi->battery_percentage;
	major_checkpoint = time(0);
    }
}

static void sig_handler(int sig)
{
    syslog(LOG_INFO, "Exiting");
    apmd_call_proxy(APMD_STOP, 0);
    unlink(PID_FILE);
    exit(0);
}

int main(int argc, char **argv)
{
    int debug = 0;
    int c;
    int fd;
    int pid;
    FILE *str;
    apm_info apminfo, *apmi;
    apm_event_t events[MAX_EVENTS];

    static struct option longopts[] =
    {
	{"verbose", 0, 0, 'v'},
	{"quiet", 0, 0, 'q'},
	{"version", 0, 0, 'V'},
	{"debug", 0, 0, 'd'},
	{"percentage", 1, 0, 'p'},
	{"warn", 1, 0, 'w'},
	{"wall", 0, 0, 'W'},
	{"check", 1, 0, 'c'},
	{"apmd_proxy", 1, 0, 'P'},
	{"help", 0, 0, '?'},
#ifdef	DEPRECATED_OPTIONS
	{"utc", 0, 0, 'u'},
	{"pre_suspend", 1, 0, 's'},
	{"post_resume", 1, 0, 'r'},
	{"low_battery", 1, 0, 'l'},
	{"ac_online", 1, 0, 'a'},
	{"ac_offline", 1, 0, 'b'},
#endif	/* DEPRECATED_OPTIONS */
	{NULL, 0, 0, 0},
    };

    switch (apm_exists())
    {
    case 1:
	fprintf(stderr, "No APM support in kernel\n");
	exit(1);
    case 2:
	fprintf(stderr, "Old APM support in kernel\n");
	exit(2);
    }

    while ((c = getopt_long(argc, argv,
#ifdef	DEPRECATED_OPTIONS
		"us:r:l:a:b:"
#endif
		/* "-d" not documented */
		"Vvqdp:w:Wc:P:?",
		longopts, NULL))
	   != -1)
	switch (c)
	{
	case 'V':
	    fprintf(stderr, "apmd version %s\n", VERSION);
	    exit(0);
	    break;
	case 'v':
	    ++verbose;
	    break;
	case 'q':
	    ++quiet;
	    break;
	case 'd':
	    ++debug;
	    break;
	case 'p':
	    percent_change = atoi(optarg);
	    break;
	case 'w':
	    warn_level = atoi(optarg);
	    break;
	case 'W':
	    ++wall;
	    break;
#ifdef	DEPRECATED_OPTIONS
	case 'u':
	    fprintf(stderr,
		    "WARNING:\n"
		    "  apmd's internal clock setting has been replaced by the "
		    "apmd_proxy!\n"
		    "  To set the clock to UTC, you must edit the apmd_proxy "
		    "program.\n\n");
	    break;
	case 's':
	    pre_suspend_cmd = optarg;
	    break;
	case 'r':
	    post_resume_cmd = optarg;
	    break;
	case 'l':
	    low_battery_cmd = optarg;
	    break;
	case 'a':
	    ac_online_cmd = optarg;
	    break;
	case 'b':
	    ac_offline_cmd = optarg;
	    break;
#endif	/* DEPRECATED_OPTIONS */
	case 'c':
	    check_power_time = atoi(optarg);
	    break;
	case 'P':
	    apmd_proxy = optarg;
	    break;
	case '?':
	default:
	    usage();
	    break;
	}

    if (!access(PID_FILE, R_OK))
    {
	if ((str = fopen(PID_FILE, "r")))
	{
	    fscanf(str, "%d", &pid);
	    fclose(str);
	    
	    if (!kill(pid, 0) || errno == EPERM)
	    {
		fprintf(stderr,
			"An apmd is already running as process %d:\n"
			"If it is no longer running, remove %s\n",
			pid, PID_FILE);
		exit(1);
	    }
	}
    }

    if ((apmd_uid = getuid()))
    {
	fprintf(stderr, "apmd: must be run as root\n");
	exit(1);
    }

    openlog("apmd", (debug ? LOG_PERROR : 0) | LOG_PID | LOG_CONS, LOG_DAEMON);
    if (signal(SIGINT, SIG_IGN) != SIG_IGN)
	signal(SIGINT, sig_handler);
    if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
	signal(SIGQUIT, sig_handler);
    if (signal(SIGTERM, SIG_IGN) != SIG_IGN)
	signal(SIGTERM, sig_handler);

    if (!debug)
    {				/* detach */
	if ((pid = fork()))
	{			/* parent */
	    if ((str = fopen(PID_FILE, "w")))
	    {
		fprintf(str, "%d\n", pid);
		fclose(str);
	    }
	    exit(0);
	}
	/* child */
	if (pid < 0)
	{
	    syslog(LOG_INFO, "fork() failed: %m");
	    unlink(PID_FILE);
	    exit(1);
	}
	
	/* Child.  Follow the daemon rules in
	 * W. Richard Stevens. Advanced Programming
	 * in the UNIX Environment (Addison-Wesley
	 * Publishing Co., 1992). Page 417.).
	 */
	if (setsid() < 0)
	{
	    syslog(LOG_INFO, "setsid() failed: %m");
	    unlink(PID_FILE);
	    exit(1);
	}
	chdir("/");
	umask(0);
    }

    if ((fd = apm_open()) < 0)
    {
	syslog(LOG_INFO, "apm_open() failed: %m");
	unlink(PID_FILE);
	exit(1);
    }

    if (!apm_read(&apminfo))
    {
	apmd_init(&apminfo);
	apmd_fd = fd;
    }
    for (;;)
    {
	int n = apm_get_events(fd, check_power_time, events, MAX_EVENTS);
	int i;

	/* If the call was timed-out, then we cause a "simulated" 
	 * UPDATE_TIME event. 
	 */
	if (n == 0)
	{
	    n = 1;
	    events[0] = APMD_SYNTHETIC_CHECK;
	}

	for (i = 0; i < n; i++)
	{

	    if (apm_read(&apminfo))
		continue;
	    apmi = &apminfo;
	    ADEBUG(4, " [event loop] 0x%04x\n", events[i]);
	    if (verbose)
		syslog(LOG_INFO, "Event 0x%04x: %s",
		       events[i], apm_event_name(events[i]));
	    switch (events[i])
	    {
	    case APM_SYS_STANDBY:
	    case APM_USER_STANDBY:
		apmd_standby(events[i], apmi);
		break;
	    case APM_SYS_SUSPEND:
	    case APM_USER_SUSPEND:
		apmd_suspend(events[i], apmi);
		break;
	    case APM_CRITICAL_SUSPEND:
		time(&pre_suspend_time);
		pre_suspend_percentage = 0;
		/* As fast as possible */
		ioctl(fd, APM_IOC_SUSPEND, NULL);
		break;
	    case APM_NORMAL_RESUME:
	    case APM_STANDBY_RESUME:
	    case APM_UPDATE_TIME:
	    case APM_CRITICAL_RESUME:
		apmd_resume(events[i], apmi);
		apmd_check_power(events[i], apmi);
		break;
	    case APMD_SYNTHETIC_CHECK:
		apmd_log(events[i], "performing APM status check");
		apmd_check_power(events[i], apmi);
		break;
	    case APM_LOW_BATTERY:
	    case APM_POWER_STATUS_CHANGE:
#ifdef        APM_CAPABILITY_CHANGE
	    case APM_CAPABILITY_CHANGE:
#endif
		apmd_check_power(events[i], apmi);
		break;
	    default:
		/* These aren't errors; see the APM BIOS 1.2 spec.
		 * 0x000d-0x00ff	reserved system events
		 * 0x0100-0x01ff	reserved device events
		 * 0x0200-0x02ff	OEM-defined
		 * 0x0300-0xffff	reserved
		 */
		syslog(LOG_ERR, "Received unknown APM event 0x%04x.",
			events[i]);
	    }
	}
    }

    return 0;
}
