/*	$NetBSD: sysmon.c,v 1.32 2022/03/28 12:33:21 riastradh Exp $	*/

/*-
 * Copyright (c) 2000 Zembu Labs, Inc.
 * All rights reserved.
 *
 * Author: Jason R. Thorpe <thorpej@zembu.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *	This product includes software developed by Zembu Labs, Inc.
 * 4. Neither the name of Zembu Labs nor the names of its employees may
 *    be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY ZEMBU LABS, INC. ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WAR-
 * RANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DIS-
 * CLAIMED.  IN NO EVENT SHALL ZEMBU LABS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Clearing house for system monitoring hardware.  We currently
 * handle environmental sensors, watchdog timers, and power management.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: sysmon.c,v 1.32 2022/03/28 12:33:21 riastradh Exp $");

#include <sys/param.h>
#include <sys/conf.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
#include <sys/callout.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/proc.h>
#include <sys/module.h>
#include <sys/mutex.h>
#include <sys/device.h>
#include <sys/once.h>

#include <dev/sysmon/sysmonvar.h>

dev_type_open(sysmonopen);
dev_type_close(sysmonclose);
dev_type_ioctl(sysmonioctl);
dev_type_read(sysmonread);
dev_type_poll(sysmonpoll);
dev_type_kqfilter(sysmonkqfilter);

const struct cdevsw sysmon_cdevsw = {
	.d_open = sysmonopen,
	.d_close = sysmonclose,
	.d_read = sysmonread,
	.d_write = nowrite,
	.d_ioctl = sysmonioctl,
	.d_stop = nostop,
	.d_tty = notty,
	.d_poll = sysmonpoll,
	.d_mmap = nommap,
	.d_kqfilter = sysmonkqfilter,
	.d_discard = nodiscard,
	.d_flag = D_OTHER | D_MPSAFE
};

static int	sysmon_modcmd(modcmd_t, void *);
static int	sm_init_once(void);

/*
 * Info about our minor "devices"
 */
static struct sysmon_opvec	*sysmon_opvec_table[] = { NULL, NULL, NULL };
static int			sysmon_refcnt[] = { 0, 0, 0 };
static const char		*sysmon_mod[] = { "sysmon_envsys",
						  "sysmon_wdog",
						  "sysmon_power" };
static kmutex_t sysmon_minor_mtx;

#ifdef _MODULE
static bool	sm_is_attached;
#endif

ONCE_DECL(once_sm);

/*
 * sysmon_attach_minor
 *
 *	Attach a minor device for wdog, power, or envsys.  Manage a
 *	reference count so we can prevent the device from being
 *	detached if there are still users with the minor device opened.
 *
 *	If the opvec argument is NULL, this is a request to detach the
 *	minor device - make sure the refcnt is zero!
 */
int
sysmon_attach_minor(int minor, struct sysmon_opvec *opvec)
{
	int ret;

	mutex_enter(&sysmon_minor_mtx);
	if (opvec) {
		if (sysmon_opvec_table[minor] == NULL) {
			sysmon_refcnt[minor] = 0;
			sysmon_opvec_table[minor] = opvec;
			ret = 0;
		} else
			ret = EEXIST;
	} else {
		if (sysmon_refcnt[minor] == 0) {
			sysmon_opvec_table[minor] = NULL;
			ret = 0;
		} else
			ret = EBUSY;
	}

	mutex_exit(&sysmon_minor_mtx);
	return ret;
}

/*
 * sysmonopen:
 *
 *	Open the system monitor device.
 */
int
sysmonopen(dev_t dev, int flag, int mode, struct lwp *l)
{
	int error;

	mutex_enter(&sysmon_minor_mtx);

	switch (minor(dev)) {
	case SYSMON_MINOR_ENVSYS:
	case SYSMON_MINOR_WDOG:
	case SYSMON_MINOR_POWER:
		if (sysmon_opvec_table[minor(dev)] == NULL) {
			mutex_exit(&sysmon_minor_mtx);
			error = module_autoload(sysmon_mod[minor(dev)],
			    MODULE_CLASS_DRIVER);
			if (error)
				return error;
			mutex_enter(&sysmon_minor_mtx);
			if (sysmon_opvec_table[minor(dev)] == NULL) {
				error = ENODEV;
				break;
			}
		}
		error = (sysmon_opvec_table[minor(dev)]->so_open)(dev, flag,
		    mode, l);
		if (error == 0)
			sysmon_refcnt[minor(dev)]++;
		break;
	default:
		error = ENODEV;
	}

	mutex_exit(&sysmon_minor_mtx);
	return error;
}

/*
 * sysmonclose:
 *
 *	Close the system monitor device.
 */
int
sysmonclose(dev_t dev, int flag, int mode, struct lwp *l)
{
	int error;

	switch (minor(dev)) {
	case SYSMON_MINOR_ENVSYS:
	case SYSMON_MINOR_WDOG:
	case SYSMON_MINOR_POWER:
		if (sysmon_opvec_table[minor(dev)] == NULL)
			error = ENODEV;
		else {
			error = (sysmon_opvec_table[minor(dev)]->so_close)(dev,
			    flag, mode, l);
			if (error == 0) {
				sysmon_refcnt[minor(dev)]--;
				KASSERT(sysmon_refcnt[minor(dev)] >= 0);
			}
		}
		break;
	default:
		error = ENODEV;
	}

	return (error);
}

/*
 * sysmonioctl:
 *
 *	Perform a control request.
 */
int
sysmonioctl(dev_t dev, u_long cmd, void *data, int flag, struct lwp *l)
{
	int error;

	switch (minor(dev)) {
	case SYSMON_MINOR_ENVSYS:
	case SYSMON_MINOR_WDOG:
	case SYSMON_MINOR_POWER:
		if (sysmon_opvec_table[minor(dev)] == NULL)
			error = ENODEV;
		else
			error = (sysmon_opvec_table[minor(dev)]->so_ioctl)(dev,
			    cmd, data, flag, l);
		break;
	default:
		error = ENODEV;
	}

	return (error);
}

/*
 * sysmonread:
 *
 *	Perform a read request.
 */
int
sysmonread(dev_t dev, struct uio *uio, int flags)
{
	int error;

	switch (minor(dev)) {
	case SYSMON_MINOR_POWER:
		if (sysmon_opvec_table[minor(dev)] == NULL)
			error = ENODEV;
		else
			error = (sysmon_opvec_table[minor(dev)]->so_read)(dev,
			    uio, flags);
		break;
	default:
		error = ENODEV;
	}

	return (error);
}

/*
 * sysmonpoll:
 *
 *	Poll the system monitor device.
 */
int
sysmonpoll(dev_t dev, int events, struct lwp *l)
{
	int rv;

	switch (minor(dev)) {
	case SYSMON_MINOR_POWER:
		if (sysmon_opvec_table[minor(dev)] == NULL)
			rv = events;
		else
			rv = (sysmon_opvec_table[minor(dev)]->so_poll)(dev,
			    events, l);
		break;
	default:
		rv = events;
	}

	return (rv);
}

/*
 * sysmonkqfilter:
 *
 *	Kqueue filter for the system monitor device.
 */
int
sysmonkqfilter(dev_t dev, struct knote *kn)
{
	int error;

	switch (minor(dev)) {
	case SYSMON_MINOR_POWER:
		if (sysmon_opvec_table[minor(dev)] == NULL)
			error = ENODEV;
		else
			error = (sysmon_opvec_table[minor(dev)]->so_filter)(dev,
			    kn);
		break;
	default:
		error = 1;
	}

	return (error);
}

MODULE(MODULE_CLASS_DRIVER, sysmon, NULL);

static int
sm_init_once(void)
{

	mutex_init(&sysmon_minor_mtx, MUTEX_DEFAULT, IPL_NONE);

	return 0;
}

int
sysmon_init(void)
{
	int error;
#ifdef _MODULE
	devmajor_t bmajor, cmajor;
#endif

	error = RUN_ONCE(&once_sm, sm_init_once);

#ifdef _MODULE
	mutex_enter(&sysmon_minor_mtx);
	if (!sm_is_attached) {
		bmajor = cmajor = -1;
		error = devsw_attach("sysmon", NULL, &bmajor,
				&sysmon_cdevsw, &cmajor);
		sm_is_attached = (error != 0);
	}
	mutex_exit(&sysmon_minor_mtx);
#endif

	return error;
}

int
sysmon_fini(void)
{
	int error = 0;

	if ((sysmon_opvec_table[SYSMON_MINOR_ENVSYS] != NULL) ||
	    (sysmon_opvec_table[SYSMON_MINOR_WDOG] != NULL) ||
	    (sysmon_opvec_table[SYSMON_MINOR_POWER] != NULL))
		error = EBUSY;

#ifdef _MODULE
	if (error == 0) {
		mutex_enter(&sysmon_minor_mtx);
		sm_is_attached = false;
		devsw_detach(NULL, &sysmon_cdevsw);
		mutex_exit(&sysmon_minor_mtx);
	}
#endif

	return error;
}

static int
sysmon_modcmd(modcmd_t cmd, void *arg)
{
	int ret;

	switch (cmd) {
	case MODULE_CMD_INIT:
		ret = sysmon_init();
		break;
	case MODULE_CMD_FINI:
		ret = sysmon_fini();
		break;
	case MODULE_CMD_STAT:
	default:
		ret = ENOTTY;
	}

	return ret;
}
