/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Solaris x86 ACPI ThermalZone Monitor
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

#include <sys/errno.h>
#include <sys/conf.h>
#include <sys/modctl.h>
#include <sys/open.h>
#include <sys/stat.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/ksynch.h>
#include <sys/uadmin.h>
#include <sys/acpi/acpi.h>
#include <sys/acpica.h>
#include <sys/sdt.h>

#include "tzmon.h"


#define	TZMON_ENUM_TRIP_POINTS	1
#define	TZMON_ENUM_DEV_LISTS	2
#define	TZMON_ENUM_ALL		(TZMON_ENUM_TRIP_POINTS	| TZMON_ENUM_DEV_LISTS)

/*
 * TZ_TASKQ_NAME_LEN is precisely the length of the string "AcpiThermalMonitor"
 * plus a two-digit instance number plus a NULL.  If the taskq name is changed
 * (particularly if it is lengthened), then this value needs to change.
 */
#define	TZ_TASKQ_NAME_LEN	21

/*
 * Kelvin to Celsius conversion
 * The formula for converting degrees Kelvin to degrees Celsius is
 * C = K - 273.15 (we round to 273.2).  The unit for thermal zone
 * temperatures is tenths of a degree Kelvin.  Use tenth of a degree
 * to convert, then make a whole number out of it.
 */
#define	K_TO_C(temp)		(((temp) - 2732) / 10)


/* cb_ops or dev_ops forward declarations */
static	int	tzmon_getinfo(dev_info_t *dip, ddi_info_cmd_t infocmd,
    void *arg, void **result);
static	int	tzmon_attach(dev_info_t *dip, ddi_attach_cmd_t cmd);
static	int	tzmon_detach(dev_info_t *dip, ddi_detach_cmd_t cmd);

/* other forward declarations */
static void tzmon_notify_zone(ACPI_HANDLE obj, UINT32 val, void *ctx);
static void tzmon_eval_int(ACPI_HANDLE obj, char *method, int *rv);
static thermal_zone_t *tzmon_alloc_zone();
static void tzmon_free_zone_list();
static void tzmon_discard_buffers(thermal_zone_t *tzp);
static void tzmon_enumerate_zone(ACPI_HANDLE obj, thermal_zone_t *tzp,
	int enum_flag);
static ACPI_STATUS tzmon_zone_callback(ACPI_HANDLE obj, UINT32 nest,
    void *ctx, void **rv);
static void tzmon_find_zones(void);
static void tzmon_monitor(void *ctx);
static void tzmon_set_power_device(ACPI_HANDLE dev, int on_off, char *tz_name);
static void tzmon_set_power(ACPI_BUFFER devlist, int on_off, char *tz_name);
static void tzmon_eval_zone(thermal_zone_t *tzp);
static void tzmon_do_shutdown(void);

extern void halt(char *);

static struct cb_ops	tzmon_cb_ops = {
	nodev,			/* no open routine	*/
	nodev,			/* no close routine	*/
	nodev,			/* not a block driver	*/
	nodev,			/* no print routine	*/
	nodev,			/* no dump routine	*/
	nodev,			/* no read routine	*/
	nodev,			/* no write routine	*/
	nodev,			/* no ioctl routine	*/
	nodev,			/* no devmap routine	*/
	nodev,			/* no mmap routine	*/
	nodev,			/* no segmap routine	*/
	nochpoll,		/* no chpoll routine	*/
	ddi_prop_op,
	0,			/* not a STREAMS driver	*/
	D_NEW | D_MP,		/* safe for multi-thread/multi-processor */
};

static struct dev_ops tzmon_ops = {
	DEVO_REV,		/* devo_rev */
	0,			/* devo_refcnt */
	tzmon_getinfo,		/* devo_getinfo */
	nulldev,		/* devo_identify */
	nulldev,		/* devo_probe */
	tzmon_attach,		/* devo_attach */
	tzmon_detach,		/* devo_detach */
	nodev,			/* devo_reset */
	&tzmon_cb_ops,		/* devo_cb_ops */
	(struct bus_ops *)0,	/* devo_bus_ops */
	NULL,			/* devo_power */
};

extern	struct	mod_ops mod_driverops;

static	struct modldrv modldrv = {
	&mod_driverops,
	"ACPI Thermal Zone Monitor",
	&tzmon_ops,
};

static	struct modlinkage modlinkage = {
	MODREV_1,		/* MODREV_1 indicated by manual */
	(void *)&modldrv,
	NULL,			/* termination of list of linkage structures */
};

/* globals for this module */
static dev_info_t	*tzmon_dip;
static thermal_zone_t	*zone_list;
static int		zone_count;
static kmutex_t		zone_list_lock;
static kcondvar_t	zone_list_condvar;


/*
 * _init, _info, and _fini support loading and unloading the driver.
 */
int
_init(void)
{
	return (mod_install(&modlinkage));
}


int
_info(struct modinfo *modinfop)
{
	return (mod_info(&modlinkage, modinfop));
}


int
_fini(void)
{
	return (mod_remove(&modlinkage));
}


static int
tzmon_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
	if (cmd != DDI_ATTACH)
		return (DDI_FAILURE);

	if (tzmon_dip != NULL)
		return (DDI_FAILURE);

	/*
	 * Check to see if ACPI CA services are available
	 */
	if (AcpiSubsystemStatus() != AE_OK)
		return (DDI_FAILURE);

	mutex_init(&zone_list_lock, NULL, MUTEX_DRIVER, NULL);
	cv_init(&zone_list_condvar, NULL, CV_DRIVER, NULL);

	tzmon_find_zones();
	mutex_enter(&zone_list_lock);
	if (zone_count < 1) {
		mutex_exit(&zone_list_lock);
		mutex_destroy(&zone_list_lock);
		cv_destroy(&zone_list_condvar);
		return (DDI_FAILURE);
	}
	mutex_exit(&zone_list_lock);

	if (ddi_create_minor_node(dip, ddi_get_name(dip), S_IFCHR, 0,
	    DDI_PSEUDO, 0) == DDI_FAILURE) {
		tzmon_free_zone_list();
		mutex_destroy(&zone_list_lock);
		cv_destroy(&zone_list_condvar);
		return (DDI_FAILURE);
	}

	tzmon_dip = dip;

	ddi_report_dev(dip);

	return (DDI_SUCCESS);
}


/*ARGSUSED*/
static int
tzmon_getinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
{
	int error;

	switch (infocmd) {
	case DDI_INFO_DEVT2DEVINFO:
		*result = tzmon_dip;
		if (tzmon_dip == NULL)
			error = DDI_FAILURE;
		else
			error = DDI_SUCCESS;
		break;
	case DDI_INFO_DEVT2INSTANCE:
		*result = 0;
		error = DDI_SUCCESS;
		break;
	default:
		*result = NULL;
		error = DDI_FAILURE;
	}

	return (error);
}


static int
tzmon_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
	thermal_zone_t *tzp = zone_list;

	if (cmd != DDI_DETACH)
		return (DDI_FAILURE);

	/* free allocated thermal zone name(s) */
	while (tzp != NULL) {
		AcpiOsFree(tzp->zone_name);
		tzp = tzp->next;
	}

	/* discard zone list assets */
	tzmon_free_zone_list();

	ddi_remove_minor_node(dip, NULL);
	tzmon_dip = NULL;

	mutex_destroy(&zone_list_lock);
	cv_destroy(&zone_list_condvar);

	return (DDI_SUCCESS);
}


/*
 * tzmon_notify_zone
 * Thermal zone notification handler.
 */
static void
tzmon_notify_zone(ACPI_HANDLE obj, UINT32 val, void *ctx)
{
	thermal_zone_t *tzp = (thermal_zone_t *)ctx;

	switch (val) {
	case 0x80:	/* Thermal Zone status changed */
		tzmon_eval_zone(tzp);
		break;
	case 0x81:	/* Thermal Zone trip points changed */
		tzmon_enumerate_zone(obj, tzp, TZMON_ENUM_TRIP_POINTS);
		break;
	case 0x82:	/* Device Lists changed */
		tzmon_enumerate_zone(obj, tzp, TZMON_ENUM_DEV_LISTS);
		break;
	case 0x83:	/* Thermal Relationship Table changed */
		/* not handling _TRT objects, so not handling this event */
		DTRACE_PROBE1(trt__change, char *, (char *)tzp->zone_name);
		break;
	default:
		break;
	}
}


/*
 * tzmon_eval_int
 * Evaluate the object/method as an integer.
 */
static void
tzmon_eval_int(ACPI_HANDLE obj, char *method, int *rv)
{

	if (acpica_eval_int(obj, method, rv) != AE_OK)
		*rv = -1;
}


/*
 * tzmon_alloc_zone
 * Allocate memory for the zone structure and initialize it lock mutex.
 */
static thermal_zone_t *
tzmon_alloc_zone()
{
	thermal_zone_t *tzp;

	tzp = kmem_zalloc(sizeof (thermal_zone_t), KM_SLEEP);
	mutex_init(&tzp->lock, NULL, MUTEX_DRIVER, NULL);

	return (tzp);
}


/*
 * tzmon_free_zone_list
 * Free the zone list, either because attach failed or detach initiated.
 */
static void
tzmon_free_zone_list()
{
	thermal_zone_t *tzp = zone_list;

	while (tzp != NULL) {
		thermal_zone_t *next;

		mutex_enter(&tzp->lock);

		/*
		 * Remove the notify handler for the zone.  Not much to
		 * do if this fails (since we are on our way out), so
		 * just ignore failure.
		 */
		(void) AcpiRemoveNotifyHandler(tzp->obj, ACPI_DEVICE_NOTIFY,
		    tzmon_notify_zone);

		/* Shut down monitor thread, if running */
		if (tzp->taskq != NULL) {
			tzp->polling_period = 0;
			cv_broadcast(&zone_list_condvar);

			/* Drop mutex to allow the thread to run */
			mutex_exit(&tzp->lock);
			ddi_taskq_destroy(tzp->taskq);
			mutex_enter(&tzp->lock);
		}

		tzmon_discard_buffers(tzp);
		mutex_exit(&tzp->lock);
		mutex_destroy(&tzp->lock);

		next = tzp->next;
		kmem_free(tzp, sizeof (thermal_zone_t));
		tzp = next;
	}
}


static void
tzmon_discard_buffers(thermal_zone_t *tzp)
{
	int level;

	for (level = 0; level < TZ_NUM_LEVELS; level++) {
		if (tzp->al[level].Pointer != NULL)
			AcpiOsFree(tzp->al[level].Pointer);
	}

	if (tzp->psl.Pointer != NULL)
		AcpiOsFree(tzp->psl.Pointer);
}


/*
 * tzmon_enumerate_zone
 * Enumerates the contents of a thermal zone and updates passed-in
 * thermal_zone or creates a new one if tzp is NULL. Newly-created
 * zones are linked into the global zone_list.
 */
static void
tzmon_enumerate_zone(ACPI_HANDLE obj, thermal_zone_t *tzp, int enum_flag)
{
	ACPI_STATUS status;
	ACPI_BUFFER zone_name;
	int	level;
	int	instance;
	char	abuf[5];

	/*
	 * Newly-created zones and existing zones both require
	 * some individual attention.
	 */
	if (tzp == NULL) {
		/* New zone required */
		tzp = tzmon_alloc_zone();
		mutex_enter(&zone_list_lock);
		tzp->next = zone_list;
		zone_list = tzp;

		/*
		 * It is exceedingly unlikely that instance will exceed 99.
		 * However, if it does, this will cause problems when
		 * creating the taskq for this thermal zone.
		 */
		instance = zone_count;
		zone_count++;
		mutex_exit(&zone_list_lock);
		mutex_enter(&tzp->lock);
		tzp->obj = obj;

		/*
		 * Set to a low level.  Will get set to the actual
		 * current power level when the thread monitor polls
		 * the current temperature.
		 */
		tzp->current_level = 0;

		/* Get the zone name in case we need to display it later */
		zone_name.Length = ACPI_ALLOCATE_BUFFER;
		zone_name.Pointer = NULL;

		status = AcpiGetName(obj, ACPI_FULL_PATHNAME, &zone_name);
		ASSERT(status == AE_OK);

		tzp->zone_name = zone_name.Pointer;

		status = AcpiInstallNotifyHandler(obj, ACPI_DEVICE_NOTIFY,
		    tzmon_notify_zone, (void *)tzp);
		ASSERT(status == AE_OK);
	} else {
		/* Existing zone - toss out allocated items */
		mutex_enter(&tzp->lock);
		ASSERT(tzp->obj == obj);

		if (enum_flag & TZMON_ENUM_DEV_LISTS)
			tzmon_discard_buffers(tzp);
	}

	if (enum_flag & TZMON_ENUM_TRIP_POINTS) {
		for (level = 0; level < TZ_NUM_LEVELS; level++) {
			(void) snprintf(abuf, 5, "_AC%d", level);
			tzmon_eval_int(obj, abuf, &tzp->ac[level]);

		}

		tzmon_eval_int(obj, "_CRT", &tzp->crt);
		tzmon_eval_int(obj, "_HOT", &tzp->hot);
		tzmon_eval_int(obj, "_PSV", &tzp->psv);
	}

	if (enum_flag & TZMON_ENUM_DEV_LISTS) {
		for (level = 0; level < TZ_NUM_LEVELS; level++) {
			if (tzp->ac[level] == -1) {
				tzp->al[level].Length = 0;
				tzp->al[level].Pointer = NULL;
			} else {
				(void) snprintf(abuf, 5, "_AL%d", level);
				tzp->al[level].Length = ACPI_ALLOCATE_BUFFER;
				tzp->al[level].Pointer = NULL;
				if (AcpiEvaluateObject(obj, abuf, NULL,
				    &tzp->al[level]) != AE_OK) {
					DTRACE_PROBE2(alx__missing, int, level,
					    char *, (char *)tzp->zone_name);

					tzp->al[level].Length = 0;
					tzp->al[level].Pointer = NULL;
				}
			}
		}

		tzp->psl.Length = ACPI_ALLOCATE_BUFFER;
		tzp->psl.Pointer = NULL;
		(void) AcpiEvaluateObject(obj, "_PSL", NULL, &tzp->psl);
	}

	tzmon_eval_int(obj, "_TC1", &tzp->tc1);
	tzmon_eval_int(obj, "_TC2", &tzp->tc2);
	tzmon_eval_int(obj, "_TSP", &tzp->tsp);
	tzmon_eval_int(obj, "_TZP", &tzp->tzp);

	if (tzp->tzp == 0) {
		tzp->polling_period = 0;
	} else {
		if (tzp->tzp < 0)
			tzp->polling_period = TZ_DEFAULT_PERIOD;
		else
			tzp->polling_period = tzp->tzp/10;

		/* start monitor thread if needed */
		if (tzp->taskq == NULL) {
			char taskq_name[TZ_TASKQ_NAME_LEN];

			(void) snprintf(taskq_name, TZ_TASKQ_NAME_LEN,
			    "AcpiThermalMonitor%02d", instance);
			tzp->taskq = ddi_taskq_create(tzmon_dip,
			    taskq_name, 1, TASKQ_DEFAULTPRI, 0);
			if (tzp->taskq == NULL) {
				tzp->polling_period = 0;
				cmn_err(CE_WARN, "tzmon: could not create "
				    "monitor thread for thermal zone %s - "
				    "monitor by notify only",
				    (char *)tzp->zone_name);
			} else {
				(void) ddi_taskq_dispatch(tzp->taskq,
				    tzmon_monitor, tzp, DDI_SLEEP);
			}
		}
	}

	mutex_exit(&tzp->lock);
}


/*
 * tzmon_zone_callback
 * Enumerate the thermal zone if it has a _TMP (current thermal zone
 * operating temperature) method.
 */
/*ARGSUSED*/
static ACPI_STATUS
tzmon_zone_callback(ACPI_HANDLE obj, UINT32 nest, void *ctx, void **rv)
{
	ACPI_HANDLE tmpobj;

	/*
	 * We get both ThermalZone() and Scope(\_TZ) objects here;
	 * look for _TMP (without which a zone is invalid) to pick
	 * between them (and ignore invalid zones)
	 */
	if (AcpiGetHandle(obj, "_TMP", &tmpobj) == AE_OK) {
		tzmon_enumerate_zone(obj, NULL, TZMON_ENUM_ALL);
	}

	return (AE_OK);
}


/*
 * tzmon_find_zones
 * Find all of the thermal zones by calling a ACPICA function that
 * walks the ACPI namespace and invokes a callback for each thermal
 * object found.
 */
static void
tzmon_find_zones()
{
	ACPI_STATUS status;
	int retval;

	status = AcpiWalkNamespace(ACPI_TYPE_THERMAL, ACPI_ROOT_OBJECT,
	    8, tzmon_zone_callback, NULL, (void **)&retval);

	ASSERT(status == AE_OK);
}


/*
 * tzmon_monitor
 * Run as a separate thread, this wakes according to polling period and
 * checks particular objects in the thermal zone.  One instance per
 * thermal zone.
 */
static void
tzmon_monitor(void *ctx)
{
	thermal_zone_t *tzp = (thermal_zone_t *)ctx;
	clock_t ticks;

	do {
		/* Check out the zone */
		tzmon_eval_zone(tzp);

		/* Go back to sleep */
		mutex_enter(&tzp->lock);
		ticks = drv_usectohz(tzp->polling_period * 1000000);
		if (ticks > 0)
			(void) cv_timedwait(&zone_list_condvar, &tzp->lock,
			    ddi_get_lbolt() + ticks);
		mutex_exit(&tzp->lock);
	} while (ticks > 0);
}


/*
 * tzmon_set_power_device
 */
static void
tzmon_set_power_device(ACPI_HANDLE dev, int on_off, char *tz_name)
{
	ACPI_BUFFER rb;
	ACPI_OBJECT *pr0;
	ACPI_STATUS status;
	int i;

	rb.Length = ACPI_ALLOCATE_BUFFER;
	rb.Pointer = NULL;
	status = AcpiEvaluateObject(dev, "_PR0", NULL, &rb);
	if (status != AE_OK) {
		DTRACE_PROBE2(alx__error, int, 2, char *, tz_name);
		return;
	}

	pr0 = ((ACPI_OBJECT *)rb.Pointer);
	if (pr0->Type != ACPI_TYPE_PACKAGE) {
		DTRACE_PROBE2(alx__error, int, 3, char *, tz_name);
		AcpiOsFree(rb.Pointer);
		return;
	}

	for (i = 0; i < pr0->Package.Count; i++) {
		status = AcpiEvaluateObject(
		    pr0->Package.Elements[i].Reference.Handle,
		    on_off ? "_ON" : "_OFF", NULL, NULL);
		if (status != AE_OK) {
			DTRACE_PROBE2(alx__error, int, 4, char *, tz_name);
		}
	}

	AcpiOsFree(rb.Pointer);
}


/*
 * tzmon_set_power
 * Turn on or turn off all devices in the supplied list.
 */
static void
tzmon_set_power(ACPI_BUFFER devlist, int on_off, char *tz_name)
{
	ACPI_OBJECT *devs;
	int i;

	devs = ((ACPI_OBJECT *)devlist.Pointer);
	if (devs->Type != ACPI_TYPE_PACKAGE) {
		DTRACE_PROBE2(alx__error, int, 1, char *, tz_name);
		return;
	}

	for (i = 0; i < devs->Package.Count; i++)
		tzmon_set_power_device(
		    devs->Package.Elements[i].Reference.Handle, on_off,
		    tz_name);
}


/*
 * tzmon_eval_zone
 * Evaluate the current conditions within the thermal zone.
 */
static void
tzmon_eval_zone(thermal_zone_t *tzp)
{
	int tmp, new_level, level;

	mutex_enter(&tzp->lock);

	/* get the current temperature from ACPI */
	tzmon_eval_int(tzp->obj, "_TMP", &tmp);
	DTRACE_PROBE4(tz__temp, int, tmp, int, tzp->crt, int, tzp->hot,
	    char *, (char *)tzp->zone_name);

	/* _HOT handling */
	if (tzp->hot > 0 && tmp >= tzp->hot) {
		cmn_err(CE_WARN,
		    "tzmon: Thermal zone (%s) is too hot (%d C); "
		    "initiating shutdown\n",
		    (char *)tzp->zone_name, K_TO_C(tmp));

		tzmon_do_shutdown();
	}

	/* _CRT handling */
	if (tzp->crt > 0 && tmp >= tzp->crt) {
		cmn_err(CE_WARN,
		    "tzmon: Thermal zone (%s) is critically hot (%d C); "
		    "initiating rapid shutdown\n",
		    (char *)tzp->zone_name, K_TO_C(tmp));

		/* shut down (fairly) immediately */
		mdboot(A_REBOOT, AD_HALT, NULL, B_FALSE);
	}

	/*
	 * use the temperature to determine whether the thermal zone
	 * is at a new active cooling threshold level
	 */
	for (level = 0, new_level = -1; level < TZ_NUM_LEVELS; level++) {
		if (tzp->ac[level] >= 0 && (tmp >= tzp->ac[level])) {
			new_level = level;
			break;
		}
	}

	/*
	 * if the active cooling threshold has changed, turn off the
	 * devices associated with the old one and turn on the new one
	 */
	if (tzp->current_level != new_level) {
		if ((tzp->current_level >= 0) &&
		    (tzp->al[tzp->current_level].Length != 0))
			tzmon_set_power(tzp->al[tzp->current_level], 0,
			    (char *)tzp->zone_name);

		if ((new_level >= 0) &&
		    (tzp->al[new_level].Length != 0))
			tzmon_set_power(tzp->al[new_level], 1,
			    (char *)tzp->zone_name);

		tzp->current_level = new_level;
	}

	mutex_exit(&tzp->lock);
}


/*
 * tzmon_do_shutdown
 * Initiates shutdown by sending a SIGPWR signal to init.
 */
static void
tzmon_do_shutdown(void)
{
	proc_t *initpp;

	mutex_enter(&pidlock);
	initpp = prfind(P_INITPID);
	mutex_exit(&pidlock);

	/* if we can't find init, just halt */
	if (initpp == NULL) {
		mdboot(A_REBOOT, AD_HALT, NULL, B_FALSE);
	}

	/* graceful shutdown with inittab and all getting involved */
	psignal(initpp, SIGPWR);
}