/*
 * 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 2006 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

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

/*
 * workstation console redirecting driver
 *
 * Redirects all I/O through a given device instance to the device designated
 * as the current target, as given by the vnode associated with the first
 * entry in the list of redirections for the given device instance.  The
 * implementation assumes that this vnode denotes a STREAMS device; this is
 * perhaps a bug.
 *
 * Supports the SRIOCSREDIR ioctl for designating a new redirection target.
 * The new target is added to the front of a list of potentially active
 * designees.  Should the device at the front of this list be closed, the new
 * front entry assumes active duty.  (Stated differently, redirection targets
 * stack, except that it's possible for entries in the interior of the stack
 * to go away.)
 *
 * Supports the SRIOCISREDIR ioctl for inquiring whether the descriptor given
 * as argument is the current front of the redirection list associated with
 * the descriptor on which the ioctl was issued.
 */

#include <sys/types.h>
#include <sys/sysmacros.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/file.h>
#include <sys/kmem.h>
#include <sys/stat.h>
#include <sys/stream.h>
#include <sys/stropts.h>
#include <sys/strsubr.h>
#include <sys/poll.h>
#include <sys/debug.h>
#include <sys/strredir.h>
#include <sys/conf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/errno.h>
#include <sys/modctl.h>
#include <sys/sunldi.h>
#include <sys/consdev.h>
#include <sys/fs/snode.h>

/*
 * Global data
 */
static dev_info_t	*iwscn_dip;

/*
 * We record the list of redirections as a linked list of iwscn_list_t
 * structures.  We need to keep track of the target's vp, so that
 * we can vector reads, writes, etc. off to the current designee.
 */
typedef struct _iwscn_list {
	struct _iwscn_list	*wl_next;	/* next entry */
	vnode_t			*wl_vp;		/* target's vnode */
	int			wl_ref_cnt;	/* operation in progress */
	boolean_t		wl_is_console;	/* is the real console */
} iwscn_list_t;
static iwscn_list_t	*iwscn_list;

/*
 * iwscn_list_lock serializes modifications to the global iwscn_list list.
 *
 * iwscn_list_cv is used when freeing an entry from iwscn_list to allow
 * the caller to wait till the wl_ref_cnt field is zero.
 *
 * iwscn_redirect_lock is used to serialize redirection requests.  This
 * is required to ensure that all active redirection streams have
 * the redirection streams module (redirmod) pushed on them.
 *
 * If both iwscn_redirect_lock and iwscn_list_lock must be held then
 * iwscn_redirect_lock must be aquired first.
 */
static kcondvar_t	iwscn_list_cv;
static kmutex_t		iwscn_list_lock;
static kmutex_t		iwscn_redirect_lock;

/*
 * Routines for managing iwscn_list
 */
static vnode_t *
str_vp(vnode_t *vp)
{
	/*
	 * Here we switch to using the vnode that is linked
	 * to from the stream queue.  (In the case of device
	 * streams this will correspond to the common vnode
	 * for the device.)  The reason we use this vnode
	 * is that when wcmclose() calls srpop(), this is the
	 * only vnode that it has access to.
	 */
	ASSERT(vp->v_stream != NULL);
	return (vp->v_stream->sd_vnode);
}

/*
 * Interrupt any operations that may be outstanding against this vnode.
 * optionally, wait for them to complete.
 */
static void
srinterrupt(iwscn_list_t *lp, boolean_t wait)
{
	ASSERT(MUTEX_HELD(&iwscn_list_lock));

	while (lp->wl_ref_cnt != 0) {
		strsetrerror(lp->wl_vp, EINTR, 0, NULL);
		strsetwerror(lp->wl_vp, EINTR, 0, NULL);
		if (!wait)
			break;
		cv_wait(&iwscn_list_cv, &iwscn_list_lock);
	}
}

/*
 * Remove vp from the redirection list rooted at iwscn_list, should it
 * be there. Return a pointer to the removed entry.
 */
static iwscn_list_t *
srrm(vnode_t *vp)
{
	iwscn_list_t	*lp, **lpp;

	ASSERT(MUTEX_HELD(&iwscn_list_lock));

	/* Get the stream vnode */
	vp = str_vp(vp);
	ASSERT(vp);

	/* Look for this vnode on the redirection list */
	for (lpp = &iwscn_list; (lp = *lpp) != NULL; lpp = &lp->wl_next) {
		if (lp->wl_vp == vp)
			break;
	}
	if (lp != NULL)
		/* Found it, remove this entry from the redirection list */
		*lpp = lp->wl_next;

	return (lp);
}

/*
 * Push vp onto the redirection list.
 * If it's already there move it to the front position.
 */
static void
srpush(vnode_t *vp, boolean_t is_console)
{
	iwscn_list_t	*lp;

	ASSERT(MUTEX_HELD(&iwscn_list_lock));

	/* Get the stream vnode */
	vp = str_vp(vp);
	ASSERT(vp);

	/* Check if it's already on the redirection list */
	if ((lp = srrm(vp)) == NULL) {
		lp = kmem_zalloc(sizeof (*lp), KM_SLEEP);
		lp->wl_vp = vp;
		lp->wl_is_console = is_console;
	}
	/*
	 * Note that if this vnode was already somewhere on the redirection
	 * list then we removed it above and are now bumping it up to the
	 * front of the redirection list.
	 */
	lp->wl_next = iwscn_list;
	iwscn_list = lp;
}

/*
 * This vnode is no longer a valid redirection target. Terminate any current
 * operations. If closing, wait for them to complete, then free the entry.
 * If called because a hangup has occurred, just deprecate the entry to ensure
 * it won't become the target again.
 */
void
srpop(vnode_t *vp, boolean_t close)
{
	iwscn_list_t	*tlp;		/* This target's entry */
	iwscn_list_t	*lp, **lpp;

	mutex_enter(&iwscn_list_lock);

	/*
	 * Ensure no further operations are directed at the target
	 * by removing it from the redirection list.
	 */
	if ((tlp = srrm(vp)) == NULL) {
		/* vnode wasn't in the list */
		mutex_exit(&iwscn_list_lock);
		return;
	}
	/*
	 * Terminate any current operations.
	 * If we're closing, wait until they complete.
	 */
	srinterrupt(tlp, close);

	if (close) {
		/* We're finished with this target */
		kmem_free(tlp, sizeof (*tlp));
	} else {
		/*
		 * Deprecate the entry. There's no need for a flag to indicate
		 * this state, it just needs to be moved to the back of the list
		 * behind the underlying console device. Since the underlying
		 * device anchors the list and is never removed, this entry can
		 * never return to the front again to become the target.
		 */
		for (lpp = &iwscn_list; (lp = *lpp) != NULL; )
			lpp = &lp->wl_next;
		tlp->wl_next = NULL;
		*lpp = tlp;
	}
	mutex_exit(&iwscn_list_lock);
}

/* Get a hold on the current target */
static iwscn_list_t *
srhold()
{
	iwscn_list_t	*lp;

	mutex_enter(&iwscn_list_lock);
	ASSERT(iwscn_list != NULL);
	lp = iwscn_list;
	ASSERT(lp->wl_ref_cnt >= 0);
	lp->wl_ref_cnt++;
	mutex_exit(&iwscn_list_lock);

	return (lp);
}

/* Release a hold on an entry from the redirection list */
static void
srrele(iwscn_list_t *lp)
{
	ASSERT(lp != NULL);
	mutex_enter(&iwscn_list_lock);
	ASSERT(lp->wl_ref_cnt > 0);
	lp->wl_ref_cnt--;
	cv_broadcast(&iwscn_list_cv);
	mutex_exit(&iwscn_list_lock);
}

static int
iwscnread(dev_t dev, uio_t *uio, cred_t *cred)
{
	iwscn_list_t	*lp;
	int		error;

	ASSERT(getminor(dev) == 0);

	lp = srhold();
	error = strread(lp->wl_vp, uio, cred);
	srrele(lp);

	return (error);
}

static int
iwscnwrite(dev_t dev, uio_t *uio, cred_t *cred)
{
	iwscn_list_t	*lp;
	int		error;

	ASSERT(getminor(dev) == 0);

	lp = srhold();
	error = strwrite(lp->wl_vp, uio, cred);
	srrele(lp);

	return (error);
}

static int
iwscnpoll(dev_t dev, short events, int anyyet, short *reventsp,
    struct pollhead **phpp)
{
	iwscn_list_t	*lp;
	int		error;

	ASSERT(getminor(dev) == 0);

	lp = srhold();
	error = VOP_POLL(lp->wl_vp, events, anyyet, reventsp, phpp);
	srrele(lp);

	return (error);
}

static int
iwscnioctl(dev_t dev, int cmd, intptr_t arg, int flag,
    cred_t *cred, int *rvalp)
{
	iwscn_list_t	*lp;
	file_t		*f;
	char		modname[FMNAMESZ + 1] = " ";
	int		error = 0;

	ASSERT(getminor(dev) == 0);

	switch (cmd) {
	case SRIOCSREDIR:
		/* Serialize all pushes of the redirection module */
		mutex_enter(&iwscn_redirect_lock);

		/*
		 * Find the vnode corresponding to the file descriptor
		 * argument and verify that it names a stream.
		 */
		if ((f = getf((int)arg)) == NULL) {
			mutex_exit(&iwscn_redirect_lock);
			return (EBADF);
		}
		if (f->f_vnode->v_stream == NULL) {
			releasef((int)arg);
			mutex_exit(&iwscn_redirect_lock);
			return (ENOSTR);
		}

		/*
		 * If the user is trying to redirect console output
		 * back to the underlying console via SRIOCSREDIR
		 * then they are evil and we'll stop them here.
		 */
		if (str_vp(f->f_vnode) == str_vp(rwsconsvp)) {
			releasef((int)arg);
			mutex_exit(&iwscn_redirect_lock);
			return (EINVAL);
		}

		/*
		 * Check if this stream already has the redirection
		 * module pushed onto it.  I_LOOK returns an error
		 * if there are no modules pushed onto the stream.
		 */
		(void) strioctl(f->f_vnode, I_LOOK, (intptr_t)modname,
		    FKIOCTL, K_TO_K, cred, rvalp);
		if (strcmp(modname, STRREDIR_MOD) != 0) {

			/*
			 * Push a new instance of the redirecting module onto
			 * the stream, so that its close routine can notify
			 * us when the overall stream is closed.  (In turn,
			 * we'll then remove it from the redirection list.)
			 */
			error = strioctl(f->f_vnode, I_PUSH,
			    (intptr_t)STRREDIR_MOD, FKIOCTL, K_TO_K,
			    cred, rvalp);

			if (error != 0) {
				releasef((int)arg);
				mutex_exit(&iwscn_redirect_lock);
				return (error);
			}
		}

		/* Push it onto the redirection stack */
		mutex_enter(&iwscn_list_lock);
		srpush(f->f_vnode, B_FALSE);
		mutex_exit(&iwscn_list_lock);

		releasef((int)arg);
		mutex_exit(&iwscn_redirect_lock);
		return (0);

	case SRIOCISREDIR:
		/*
		 * Find the vnode corresponding to the file descriptor
		 * argument and verify that it names a stream.
		 */
		if ((f = getf((int)arg)) == NULL) {
			return (EBADF);
		}
		if (f->f_vnode->v_stream == NULL) {
			releasef((int)arg);
			return (ENOSTR);
		}

		lp = srhold();
		*rvalp = (str_vp(f->f_vnode) == lp->wl_vp);
		srrele(lp);
		releasef((int)arg);
		return (0);

	case I_POP:
		/*
		 * We need to serialize I_POP operations with
		 * SRIOCSREDIR operations so we don't accidently
		 * remove the redirection module from a stream.
		 */
		mutex_enter(&iwscn_redirect_lock);
		lp = srhold();

		/*
		 * Here we need to protect against process that might
		 * try to pop off the redirection module from the
		 * redirected stream.  Popping other modules is allowed.
		 *
		 * It's ok to hold iwscn_list_lock while doing the
		 * I_LOOK since it's such a simple operation.
		 */
		(void) strioctl(lp->wl_vp, I_LOOK, (intptr_t)modname,
		    FKIOCTL, K_TO_K, cred, rvalp);

		if (strcmp(STRREDIR_MOD, modname) == 0) {
			srrele(lp);
			mutex_exit(&iwscn_redirect_lock);
			return (EINVAL);
		}

		/* Process the ioctl normally */
		error = VOP_IOCTL(lp->wl_vp, cmd, arg, flag, cred, rvalp);

		srrele(lp);
		mutex_exit(&iwscn_redirect_lock);
		return (error);
	}

	/* Process the ioctl normally */
	lp = srhold();
	error = VOP_IOCTL(lp->wl_vp, cmd, arg, flag, cred, rvalp);
	srrele(lp);
	return (error);
}

/* ARGSUSED */
static int
iwscnopen(dev_t *devp, int flag, int state, cred_t *cred)
{
	iwscn_list_t	*lp;
	vnode_t		*vp = rwsconsvp;

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

	if (getminor(*devp) != 0)
		return (ENXIO);

	/*
	 * You can't really open us until the console subsystem
	 * has been configured.
	 */
	if (rwsconsvp == NULL)
		return (ENXIO);

	/*
	 * Check if this is the first open of this device or if
	 * there is currently no redirection going on.  (Ie, we're
	 * sending output to underlying console device.)
	 */
	mutex_enter(&iwscn_list_lock);
	if ((iwscn_list == NULL) || (iwscn_list->wl_vp == str_vp(vp))) {
		int		error = 0;

		/* Don't hold the list lock across an VOP_OPEN */
		mutex_exit(&iwscn_list_lock);

		/*
		 * There is currently no redirection going on.
		 * pass this open request onto the console driver
		 */
		error = VOP_OPEN(&vp, flag, cred);
		if (error != 0)
			return (error);

		/* Re-aquire the list lock */
		mutex_enter(&iwscn_list_lock);

		if (iwscn_list == NULL) {
			/* Save this vnode on the redirection list */
			srpush(vp, B_TRUE);
		} else {
			/*
			 * In this case there must already be a copy of
			 * this vnode on the list, so we can free up this one.
			 */
			(void) VOP_CLOSE(vp, flag, 1, (offset_t)0, cred);
		}
	}

	/*
	 * XXX This is an ugly legacy hack that has been around
	 * forever.  This code is here because this driver (the
	 * iwscn driver) is a character driver layered over a
	 * streams driver.
	 *
	 * Normally streams recieve notification whenever a process
	 * closes its last reference to that stream so that it can
	 * clean up any signal handling related configuration.  (Ie,
	 * when a stream is configured to deliver a signal to a
	 * process upon certain events.)  This is a feature supported
	 * by the streams framework.
	 *
	 * But character/block drivers don't recieve this type
	 * of notification.  A character/block driver's close routine
	 * is only invoked upon the last close of the device.  This
	 * is an artifact of the multiple open/single close driver
	 * model currently supported by solaris.
	 *
	 * So a problem occurs when a character driver layers itself
	 * on top of a streams driver.  Since this driver doesn't always
	 * receive a close notification when a process closes its
	 * last reference to it, this driver can't tell the stream
	 * it's layered upon to clean up any signal handling
	 * configuration for that process.
	 *
	 * So here we hack around that by manually cleaning up the
	 * signal handling list upon each open.  It doesn't guarantee
	 * that the signaling handling data stored in the stream will
	 * always be up to date, but it'll be more up to date than
	 * it would be if we didn't do this.
	 *
	 * The real way to solve this problem would be to change
	 * the device framework from an multiple open/single close
	 * model to a multiple open/multiple close model.  Then
	 * character/block drivers could pass on close requests
	 * to streams layered underneath.
	 */
	str_cn_clean(VTOS(rwsconsvp)->s_commonvp);
	for (lp = iwscn_list; lp != NULL; lp = lp->wl_next) {
		ASSERT(lp->wl_vp->v_stream != NULL);
		str_cn_clean(lp->wl_vp);
	}

	mutex_exit(&iwscn_list_lock);
	return (0);
}

/* ARGSUSED */
static int
iwscnclose(dev_t dev, int flag, int state, cred_t *cred)
{
	iwscn_list_t	*lp;

	ASSERT(getminor(dev) == 0);

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

	mutex_enter(&iwscn_list_lock);
	/*
	 * Remove each entry from the redirection list, terminate any
	 * current operations, wait for them to finish, then free the entry.
	 */
	while (iwscn_list != NULL) {
		lp = srrm(iwscn_list->wl_vp);
		ASSERT(lp != NULL);
		srinterrupt(lp, B_TRUE);

		if (lp->wl_is_console == B_TRUE)
			/* Close the underlying console device. */
			(void) VOP_CLOSE(lp->wl_vp, 0, 1, (offset_t)0, kcred);

		kmem_free(lp, sizeof (*lp));
	}
	mutex_exit(&iwscn_list_lock);
	return (0);
}

/*ARGSUSED*/
static int
iwscnattach(dev_info_t *devi, ddi_attach_cmd_t cmd)
{
	/*
	 * This is a pseudo device so there will never be more than
	 * one instance attached at a time
	 */
	ASSERT(iwscn_dip == NULL);

	if (ddi_create_minor_node(devi, "iwscn", S_IFCHR,
	    0, DDI_PSEUDO, NULL) == DDI_FAILURE) {
		return (DDI_FAILURE);
	}

	iwscn_dip = devi;
	mutex_init(&iwscn_list_lock, NULL, MUTEX_DRIVER, NULL);
	mutex_init(&iwscn_redirect_lock, NULL, MUTEX_DRIVER, NULL);
	cv_init(&iwscn_list_cv, NULL, CV_DRIVER, NULL);

	return (DDI_SUCCESS);
}

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

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

struct cb_ops	iwscn_cb_ops = {
	iwscnopen,		/* open */
	iwscnclose,		/* close */
	nodev,			/* strategy */
	nodev,			/* print */
	nodev,			/* dump */
	iwscnread,		/* read */
	iwscnwrite,		/* write */
	iwscnioctl,		/* ioctl */
	nodev,			/* devmap */
	nodev,			/* mmap */
	nodev, 			/* segmap */
	iwscnpoll,		/* poll */
	ddi_prop_op,		/* cb_prop_op */
	NULL,			/* streamtab  */
	D_MP			/* Driver compatibility flag */
};

struct dev_ops	iwscn_ops = {
	DEVO_REV,		/* devo_rev, */
	0,			/* refcnt  */
	iwscninfo,		/* info */
	nulldev,		/* identify */
	nulldev,		/* probe */
	iwscnattach,		/* attach */
	nodev,			/* detach */
	nodev,			/* reset */
	&iwscn_cb_ops,		/* driver operations */
	NULL			/* bus operations */
};

/*
 * Module linkage information for the kernel.
 */
static struct modldrv modldrv = {
	&mod_driverops, /* Type of module.  This one is a pseudo driver */
	"Workstation Redirection driver %I%",
	&iwscn_ops,	/* driver ops */
};

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

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

int
_fini(void)
{
	return (EBUSY);
}

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