/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (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 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

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

/*
 * System message redirection driver for Sun.
 *
 * Redirects system message output to the device designated as the underlying
 * "hardware" console, as given by the value of sysmvp.  The implementation
 * assumes that sysmvp denotes a STREAMS device; the assumption is justified
 * since consoles must be capable of effecting tty semantics.
 */

#include <sys/types.h>
#include <sys/kmem.h>
#include <sys/open.h>
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/signal.h>
#include <sys/cred.h>
#include <sys/user.h>
#include <sys/proc.h>
#include <sys/vnode.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/session.h>
#include <sys/stream.h>
#include <sys/strsubr.h>
#include <sys/poll.h>
#include <sys/debug.h>
#include <sys/sysmsg_impl.h>
#include <sys/conf.h>
#include <sys/termios.h>
#include <sys/errno.h>
#include <sys/modctl.h>
#include <sys/pathname.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/consdev.h>
#include <sys/policy.h>

/*
 * internal functions
 */
static int sysmopen(dev_t *, int, int, cred_t *);
static int sysmclose(dev_t, int, int, cred_t *);
static int sysmread(dev_t, struct uio *, cred_t *);
static int sysmwrite(dev_t, struct uio *, cred_t *);
static int sysmioctl(dev_t, int, intptr_t, int, cred_t *, int *);
static int sysmpoll(dev_t, short, int, short *, struct pollhead **);
static int sysm_info(dev_info_t *, ddi_info_cmd_t, void *, void **);
static int sysm_attach(dev_info_t *, ddi_attach_cmd_t);
static int sysm_detach(dev_info_t *, ddi_detach_cmd_t);
static void bind_consadm_conf(char *);
static int checkarg(dev_t);

static dev_info_t *sysm_dip;		/* private copy of devinfo pointer */

static struct cb_ops sysm_cb_ops = {

	sysmopen,		/* open */
	sysmclose,		/* close */
	nodev,			/* strategy */
	nodev,			/* print */
	nodev,			/* dump */
	sysmread,		/* read */
	sysmwrite,		/* write */
	sysmioctl,		/* ioctl */
	nodev,			/* devmap */
	nodev,			/* mmap */
	nodev, 			/* segmap */
	sysmpoll,		/* poll */
	ddi_prop_op,		/* cb_prop_op */
	NULL,			/* streamtab  */
	D_NEW | D_MP,		/* Driver compatibility flag */
	CB_REV,			/* cb_rev */
	nodev,			/* aread */
	nodev			/* awrite */
};

static struct dev_ops sysm_ops = {

	DEVO_REV,		/* devo_rev, */
	0,			/* refcnt  */
	sysm_info,		/* info */
	nulldev,		/* identify */
	nulldev,		/* probe */
	sysm_attach,		/* attach */
	sysm_detach,		/* detach */
	nodev,			/* reset */
	&sysm_cb_ops,		/* driver operations */
	(struct bus_ops *)0,	/* bus operations */
	nulldev			/* power */

};

/*
 * Global variables associated with the console device:
 */

#define	SYS_SYSMIN	0	/* sysmsg minor number */
#define	SYS_MSGMIN	1	/* msglog minor number */
#define	SYSPATHLEN	255	/* length of device path */

/*
 * Private driver state:
 */

#define	MAXDEVS 5

typedef struct {
	dev_t	dca_devt;
	int	dca_flags;
	vnode_t	*dca_vp;
	krwlock_t	dca_lock;
	char	dca_name[SYSPATHLEN];
} devicecache_t;

/* list of dyn. + persist. config'ed dev's */
static devicecache_t sysmcache[MAXDEVS];
static kmutex_t	dcvp_mutex;
static vnode_t	*dcvp = NULL;
static boolean_t sysmsg_opened;
static boolean_t msglog_opened;

/* flags for device cache */
#define	SYSM_DISABLED	0x0
#define	SYSM_ENABLED	0x1

/*
 * Module linkage information for the kernel.
 */

static struct modldrv modldrv = {
	&mod_driverops, /* Type of module.  This one is a pseudo driver */
	"System message redirection (fanout) driver %I%",
	&sysm_ops,	/* driver ops */
};

static struct modlinkage modlinkage = {
	MODREV_1,
	&modldrv,
	NULL
};

int
_init(void)
{
	return (mod_install(&modlinkage));
}

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

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

/*
 * DDI glue routines
 */
static int
sysm_attach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
	int i;

	switch (cmd) {
	case DDI_ATTACH:
		ASSERT(sysm_dip == NULL);

		if (ddi_create_minor_node(devi, "sysmsg", S_IFCHR,
			SYS_SYSMIN, DDI_PSEUDO, NULL) == DDI_FAILURE ||
			ddi_create_minor_node(devi, "msglog", S_IFCHR,
			SYS_MSGMIN, DDI_PSEUDO, NULL) == DDI_FAILURE) {
			ddi_remove_minor_node(devi, NULL);
			return (DDI_FAILURE);
		}

		for (i = 0; i < MAXDEVS; i++) {
			rw_init(&sysmcache[i].dca_lock, NULL, RW_DRIVER, NULL);
		}

		sysm_dip = devi;
		return (DDI_SUCCESS);
	case DDI_SUSPEND:
	case DDI_PM_SUSPEND:
		return (DDI_SUCCESS);
	default:
		return (DDI_FAILURE);
	}
}

static int
sysm_detach(dev_info_t *devi, ddi_detach_cmd_t cmd)
{
	int i;

	switch (cmd) {
	case DDI_DETACH:
		ASSERT(sysm_dip == devi);

		for (i = 0; i < MAXDEVS; i++)
			rw_destroy(&sysmcache[i].dca_lock);

		ddi_remove_minor_node(devi, NULL);
		sysm_dip = NULL;
		return (DDI_SUCCESS);

	case DDI_SUSPEND:
	case DDI_PM_SUSPEND:
		return (DDI_SUCCESS);
	default:
		return (DDI_FAILURE);
	}

}

/* ARGSUSED */
static int
sysm_info(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **result)
{
	int rval = DDI_FAILURE;
	minor_t instance;

	instance = getminor((dev_t)arg);

	switch (infocmd) {
	case DDI_INFO_DEVT2DEVINFO:
		if (sysm_dip != NULL &&
			(instance == SYS_SYSMIN || instance == SYS_MSGMIN)) {
			*result = sysm_dip;
			rval = DDI_SUCCESS;
		}
		break;

	case DDI_INFO_DEVT2INSTANCE:
		if (instance == SYS_SYSMIN || instance == SYS_MSGMIN) {
			*result = NULL;
			rval = DDI_SUCCESS;
		}
		break;

	default:
		break;
	}

	return (rval);
}

/*
 * Parse the contents of the buffer, and bind the named
 * devices as auxiliary consoles using our own ioctl routine.
 *
 * Comments begin with '#' and are terminated only by a newline
 * Device names begin with a '/', and are terminated by a newline,
 * space, '#' or tab.
 */
static void
parse_buffer(char *buf, ssize_t fsize)
{
	char *ebuf = buf + fsize;
	char *devname = NULL;
	int eatcomments = 0;

	while (buf < ebuf) {
		if (eatcomments) {
			if (*buf++ == '\n')
				eatcomments = 0;
			continue;
		}
		switch (*buf) {
		case '/':
			if (devname == NULL)
				devname = buf;
			break;
		case '#':
			eatcomments = 1;
			/*FALLTHROUGH*/
		case ' ':
		case '\t':
		case '\n':
			*buf = '\0';
			if (devname == NULL)
				break;
			(void) sysmioctl(NODEV, CIOCSETCONSOLE,
				(intptr_t)devname, FNATIVE|FKIOCTL|FREAD|FWRITE,
				kcred, NULL);
			devname = NULL;
			break;
		default:
			break;
		}
		buf++;
	}
}

#define	CNSADM_BYTES_MAX	2000	/* XXX  nasty fixed size */

static void
bind_consadm_conf(char *path)
{
	struct vattr vattr;
	vnode_t *vp;
	void *buf;
	size_t size;
	ssize_t resid;
	int err = 0;

	if (vn_open(path, UIO_SYSSPACE, FREAD, 0, &vp, 0, 0) != 0)
		return;
	vattr.va_mask = AT_SIZE;
	if ((err = VOP_GETATTR(vp, &vattr, 0, kcred)) != 0) {
		cmn_err(CE_WARN, "sysmsg: getattr: '%s': error %d",
			path, err);
		goto closevp;
	}

	size = vattr.va_size > CNSADM_BYTES_MAX ?
		CNSADM_BYTES_MAX : (ssize_t)vattr.va_size;
	buf = kmem_alloc(size, KM_SLEEP);

	if ((err = vn_rdwr(UIO_READ, vp, buf, size, (offset_t)0,
		UIO_SYSSPACE, 0, (rlim64_t)0, kcred, &resid)) != 0)
		cmn_err(CE_WARN, "sysmsg: vn_rdwr: '%s': error %d",
		    path, err);
	else
		parse_buffer(buf, size - resid);

	kmem_free(buf, size);
closevp:
	(void) VOP_CLOSE(vp, FREAD, 1, (offset_t)0, kcred);
	VN_RELE(vp);
}

/* ARGSUSED */
static int
sysmopen(dev_t *dev, int flag, int state, cred_t *cred)
{
	int	i;
	vnode_t	*vp;
	minor_t instance;
	static boolean_t initialized;

	instance = getminor(*dev);

	if (state != OTYP_CHR || (instance != 0 && instance != 1))
		return (ENXIO);

	mutex_enter(&dcvp_mutex);
	if ((dcvp == NULL) && (vn_open("/dev/console",
		UIO_SYSSPACE, FWRITE, 0, &dcvp, 0, 0) != 0)) {
		mutex_exit(&dcvp_mutex);
		return (ENXIO);
	}

	if (instance == SYS_SYSMIN)
		sysmsg_opened = B_TRUE;
	else
		msglog_opened = B_TRUE;

	if (!initialized) {
		bind_consadm_conf("/etc/consadm.conf");
		initialized = B_TRUE;
	}
	mutex_exit(&dcvp_mutex);

	for (i = 0; i < MAXDEVS; i++) {
		rw_enter(&sysmcache[i].dca_lock, RW_WRITER);
		if ((sysmcache[i].dca_flags & SYSM_ENABLED) &&
			sysmcache[i].dca_vp == NULL) {
			/*
			 * 4196476 - FTRUNC was causing E10K to return EINVAL
			 * on open
			 */
			flag = flag & ~FTRUNC;
			/*
			 * Open failures on the auxiliary consoles are
			 * not returned because we don't care if some
			 * subset get an error. We know the default console
			 * is okay, and preserve the semantics of the
			 * open for the default console.
			 * Set NONBLOCK|NDELAY in case there's no carrier.
			 */
			if (vn_open(sysmcache[i].dca_name, UIO_SYSSPACE,
				flag | FNONBLOCK | FNDELAY, 0, &vp, 0, 0) == 0)
				sysmcache[i].dca_vp = vp;
		}
		rw_exit(&sysmcache[i].dca_lock);
	}

	return (0);
}

/* ARGSUSED */
static int
sysmclose(dev_t dev, int flag, int state, cred_t *cred)
{
	int	i;
	minor_t instance;

	ASSERT(dcvp != NULL);

	if (state != OTYP_CHR)
		return (ENXIO);

	instance = getminor(dev);

	mutex_enter(&dcvp_mutex);
	if (instance == SYS_SYSMIN)
		sysmsg_opened = B_FALSE;
	else
		msglog_opened = B_FALSE;

	if (sysmsg_opened || msglog_opened) {
		mutex_exit(&dcvp_mutex);
		return (0);
	}

	(void) VOP_CLOSE(dcvp, FWRITE, 1, (offset_t)0, kcred);
	VN_RELE(dcvp);
	dcvp = NULL;
	mutex_exit(&dcvp_mutex);

	/*
	 * Close the auxiliary consoles, we're not concerned with
	 * passing up the errors.
	 */
	for (i = 0; i < MAXDEVS; i++) {
		rw_enter(&sysmcache[i].dca_lock, RW_WRITER);
		if (sysmcache[i].dca_vp != NULL) {
			(void) VOP_CLOSE(sysmcache[i].dca_vp, flag,
				1, (offset_t)0, cred);
			VN_RELE(sysmcache[i].dca_vp);
			sysmcache[i].dca_vp = NULL;
		}
		rw_exit(&sysmcache[i].dca_lock);
	}

	return (0);
}

/* Reads occur only on the default console */

/* ARGSUSED */
static int
sysmread(dev_t dev, struct uio *uio, cred_t *cred)
{
	ASSERT(dcvp != NULL);
	return (VOP_READ(dcvp, uio, 0, cred, NULL));
}

/* ARGSUSED */
static int
sysmwrite(dev_t dev, struct uio *uio, cred_t *cred)
{
	int	i = 0;
	iovec_t	uio_iov;
	struct uio	tuio;

	ASSERT(dcvp != NULL);
	ASSERT(uio != NULL);

	for (i = 0; i < MAXDEVS; i++) {
		rw_enter(&sysmcache[i].dca_lock, RW_READER);
		if (sysmcache[i].dca_vp != NULL &&
			(sysmcache[i].dca_flags & SYSM_ENABLED)) {
			tuio = *uio;
			uio_iov = *(uio->uio_iov);
			tuio.uio_iov = &uio_iov;
			(void) VOP_WRITE(sysmcache[i].dca_vp, &tuio, 0, cred,
				NULL);
		}
		rw_exit(&sysmcache[i].dca_lock);
	}
	return (VOP_WRITE(dcvp, uio, 0, cred, NULL));
}

/* ARGSUSED */
static int
sysmioctl(dev_t dev, int cmd, intptr_t arg, int flag, cred_t *cred, int *rvalp)
{
	int	rval = 0;
	int	error = 0;
	size_t	size = 0;
	int	i;
	char	*infop;
	char	found = 0;
	dev_t	newdevt = (dev_t)NODEV;	/* because 0 == /dev/console */
	vnode_t	*vp;

	switch (cmd) {
	case CIOCGETCONSOLE:
		/* Sum over the number of enabled devices */
		for (i = 0; i < MAXDEVS; i++) {
			if (sysmcache[i].dca_flags & SYSM_ENABLED)
				/* list is space separated, followed by NULL */
				size += strlen(sysmcache[i].dca_name) + 1;
		}
		if (size == 0)
			return (0);
		break;
	case CIOCSETCONSOLE:
	case CIOCRMCONSOLE:
		size = sizeof (sysmcache[0].dca_name);
		break;
	case CIOCTTYCONSOLE:
	{
		dev_t	d;
		dev32_t	d32;
		extern dev_t rwsconsdev, rconsdev, uconsdev;
		proc_t	*p;

		if (drv_getparm(UPROCP, &p) != 0)
			return (ENODEV);
		else
			d = cttydev(p);
		/*
		 * If the controlling terminal is the real
		 * or workstation console device, map to what the
		 * user thinks is the console device.
		 */
		if (d == rwsconsdev || d == rconsdev)
			d = uconsdev;
		if ((flag & FMODELS) != FNATIVE) {
			if (!cmpldev(&d32, d))
				return (EOVERFLOW);
			if (ddi_copyout(&d32, (caddr_t)arg, sizeof (d32),
			    flag))
				return (EFAULT);
		} else {
			if (ddi_copyout(&d, (caddr_t)arg, sizeof (d), flag))
				return (EFAULT);
		}
		return (0);
	}
	default:
		/* everything else is sent to the console device */
		return (VOP_IOCTL(dcvp, cmd, arg, flag, cred, rvalp));
	}

	if ((rval = secpolicy_console(cred)) != 0)
		return (EPERM);

	infop = kmem_alloc(size, KM_SLEEP);
	if (flag & FKIOCTL)
		error = copystr((caddr_t)arg, infop, size, NULL);
	else
		error = copyinstr((caddr_t)arg, infop, size, NULL);

	if (error) {
		switch (cmd) {
		case CIOCGETCONSOLE:
			/*
			 * If the buffer is null, then return a byte count
			 * to user land.
			 */
			*rvalp = size;
			goto err_exit;
		default:
			rval = EFAULT;
			goto err_exit;
		}
	}

	if (infop[0] != NULL) {
		if ((rval = lookupname(infop, UIO_SYSSPACE, FOLLOW,
			NULLVPP, &vp)) == 0) {
			if (vp->v_type != VCHR) {
				VN_RELE(vp);
				rval = EINVAL;
				goto err_exit;
			}
			newdevt = vp->v_rdev;
			VN_RELE(vp);
		} else
			goto err_exit;
	}

	switch (cmd) {
	case CIOCGETCONSOLE:
		/*
		 * Return the list of device names that are enabled.
		 */
		for (i = 0; i < MAXDEVS; i++) {
			rw_enter(&sysmcache[i].dca_lock, RW_READER);
			if (sysmcache[i].dca_flags & SYSM_ENABLED) {
				if (infop[0] != NULL)
					(void) strcat(infop, " ");
				(void) strcat(infop, sysmcache[i].dca_name);
			}
			rw_exit(&sysmcache[i].dca_lock);
		}
		if (rval == 0 && copyoutstr(infop, (void *)arg, size, NULL))
			rval = EFAULT;
		break;

	case CIOCSETCONSOLE:
		if ((rval = checkarg(newdevt)) != 0)
			break;
		/*
		 * The device does not have to be open or disabled to
		 * perform the set console.
		 */
		for (i = 0; i < MAXDEVS; i++) {
			rw_enter(&sysmcache[i].dca_lock, RW_WRITER);
			if (sysmcache[i].dca_devt == newdevt &&
				(sysmcache[i].dca_flags & SYSM_ENABLED)) {
				(void) strcpy(sysmcache[i].dca_name, infop);
				rval = EEXIST;
				rw_exit(&sysmcache[i].dca_lock);
				break;
			} else if (sysmcache[i].dca_devt == newdevt &&
				sysmcache[i].dca_flags == SYSM_DISABLED) {
				sysmcache[i].dca_flags |= SYSM_ENABLED;
				(void) strcpy(sysmcache[i].dca_name, infop);
				rw_exit(&sysmcache[i].dca_lock);
				found = 1;
				break;
			} else if (sysmcache[i].dca_devt == 0) {
				ASSERT(sysmcache[i].dca_vp == NULL &&
				sysmcache[i].dca_flags == SYSM_DISABLED);
				(void) strcpy(sysmcache[i].dca_name, infop);
				sysmcache[i].dca_flags = SYSM_ENABLED;
				sysmcache[i].dca_devt = newdevt;
				rw_exit(&sysmcache[i].dca_lock);
				found = 1;
				break;
			}
			rw_exit(&sysmcache[i].dca_lock);
		}
		if (found == 0 && rval == 0)
			rval = ENOENT;
		break;

	case CIOCRMCONSOLE:
		for (i = 0; i < MAXDEVS; i++) {
			rw_enter(&sysmcache[i].dca_lock, RW_WRITER);
			if (sysmcache[i].dca_devt == newdevt) {
				sysmcache[i].dca_flags = SYSM_DISABLED;
				sysmcache[i].dca_name[0] = '\0';
				rw_exit(&sysmcache[i].dca_lock);
				found = 1;
				break;
			}
			rw_exit(&sysmcache[i].dca_lock);
		}
		if (found == 0)
			rval = ENOENT;
		break;

	default:
		break;
	}

err_exit:
	kmem_free(infop, size);
	return (rval);
}

/* As with the read, we poll only the default console */

/* ARGSUSED */
static int
sysmpoll(dev_t dev, short events, int anyyet, short *reventsp,
	struct pollhead **phpp)
{
	return (VOP_POLL(dcvp, events, anyyet, reventsp, phpp));
}

/* Sanity check that the device is good */
static int
checkarg(dev_t devt)
{
	int rval = 0;
	dev_t sysmsg_dev, msglog_dev;
	extern dev_t rwsconsdev, rconsdev, uconsdev;

	if (devt == rconsdev || devt == rwsconsdev || devt == uconsdev) {
		rval = EBUSY;
	} else {
		sysmsg_dev = makedevice(ddi_driver_major(sysm_dip), SYS_SYSMIN);
		msglog_dev = makedevice(ddi_driver_major(sysm_dip), SYS_MSGMIN);
		if (devt == sysmsg_dev || devt == msglog_dev)
			rval = EINVAL;
	}

	return (rval);
}