/*
 * drivers/s390/char/vmlogrdr.c
 *	character device driver for reading z/VM system service records
 *
 *
 *	Copyright (C) 2004 IBM Corporation
 *	character device driver for reading z/VM system service records,
 *	Version 1.0
 *	Author(s): Xenia Tkatschow <xenia@us.ibm.com>
 *		   Stefan Weinhuber <wein@de.ibm.com>
 *
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <asm/atomic.h>
#include <asm/uaccess.h>
#include <asm/cpcmd.h>
#include <asm/debug.h>
#include <asm/ebcdic.h>
#include "../net/iucv.h"
#include <linux/kmod.h>



#define MAXSERVICES 1

enum vmlogrdr_hotplug_action { 
	VMLOGRDR_HOTPLUG_ADD=0,
	VMLOGRDR_HOTPLUG_REMOVE=1
};

MODULE_AUTHOR
	("(C) 2004 IBM Corporation by Xenia Tkatschow (xenia@us.ibm.com)\n"
	 "                            Stefan Weinhuber (wein@de.ibm.com)");
MODULE_DESCRIPTION ("Character device driver for reading z/VM "
		    "system service records.");
MODULE_PARM (services, "1-"__MODULE_STRING(MAXSERVICES)"s");
MODULE_PARM_DESC (services,
		"Specify the system services\n"
		"services=system_service0,system_service1,..,"
		"system_serviceN\n");
MODULE_LICENSE("GPL");

static char *services[MAXSERVICES];

char userid[9];

static char FENCE[] = {"EOR"};
static int logreader_major = 0;

static int logreader_open(struct inode *, struct file *);
static int logreader_release(struct inode *, struct file *);
static ssize_t logreader_read (struct file *filp, char *data, size_t count,
			       loff_t * ppos);



/*
 * File operation structures for logreader devices
 */
static struct file_operations logreader_fops = {
	.owner=THIS_MODULE,
	.open =	logreader_open,
	.release = logreader_release,
	.read =	logreader_read,
};


static __u8 iucvMagic[16] = {
	0xF0, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40,
	0xF0, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40
};

static __u8 mask[] = {
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};

static __u8 iucv_host[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };


static void
logreader_iucv_ConnectionComplete(iucv_ConnectionComplete *eib,
			void *pgm_data);

static void
logreader_iucv_ConnectionSevered(iucv_ConnectionSevered *eib,
			void *pgm_data);

static void
logreader_iucv_MessagePending(iucv_MessagePending *eib,
			void *pgm_data);

/*
 * Name: iucv_interrupt_ops_t
 * Descriptor: Defines the iucv interrupt routines that the IUCV driver
 *	will call when there is an interrupt for the logreader
 *	character device driver
 * Members:
 *	ConnectionSevered: This routine will be called by the IUCV driver
 *		if the system service severes the iucv connection.
 *	MessagePending: This routine will be called be the IUCV driver
 *		when it receives a message pending interrupt.
 * Note: The remaining routines are not defined as we do not expect to
 *	handle these situations.
 */
static iucv_interrupt_ops_t logreader_iucvops = {
	.ConnectionComplete=	logreader_iucv_ConnectionComplete,
	.ConnectionSevered=	logreader_iucv_ConnectionSevered,
	.MessagePending=	logreader_iucv_MessagePending,
};



#define buflen	4088

struct logreader_priv_t {
	char system_service[8];
	u16 pathid;
	int connection_established;
	int iucv_path_severed;
	iucv_MessagePending local_interrupt_buffer;
	atomic_t receive_ready;
	iucv_handle_t iucv_handle;
	int minor_num;
	char * buffer;
	char * current_position;
	int remaining;
	ulong residual_length;
	int buffer_free;
	int dev_in_use; /* 1: already opened, 0: not opened*/
	spinlock_t priv_lock;
};

DECLARE_WAIT_QUEUE_HEAD(conn_wait_queue);
DECLARE_WAIT_QUEUE_HEAD(read_wait_queue);

/*
 * pointer to system service private structure
 * minor number 0 --> logrec
 * minor number 1 --> account	 -> not yet implemented
 * minor number 2 --> symptom    -> not yet implemented
 */
#define MAXMINOR	1
static struct logreader_priv_t * sys_ser[MAXMINOR];


static void
logreader_iucv_ConnectionComplete (iucv_ConnectionComplete * eib,
				   void * pgm_data)
{
	struct logreader_priv_t * logptr =
		(struct logreader_priv_t *) pgm_data;
	spin_lock(&logptr->priv_lock);
	logptr->connection_established = 1;
	spin_unlock(&logptr->priv_lock);
	wake_up(&conn_wait_queue);
	return;
}


static void
logreader_iucv_ConnectionSevered (iucv_ConnectionSevered * eib, void * pgm_data)
{
	
	u8 reason = (u8) eib->ipuser[8];
	struct logreader_priv_t * logptr = (struct logreader_priv_t *) pgm_data;

	printk (KERN_ERR "vmlogrdr: connection severed with"
		" reason %i/n", reason);

	spin_lock(&logptr->priv_lock);	
	logptr->connection_established = 0;
	logptr->iucv_path_severed = 1;
	spin_unlock(&logptr->priv_lock);

	wake_up(&conn_wait_queue);
	/* just in case we're sleeping waiting for a record */
	wake_up_interruptible(&read_wait_queue);
	
	return;
}


static void
logreader_iucv_MessagePending (iucv_MessagePending * eib, void * pgm_data)
{
	struct logreader_priv_t * logptr = (struct logreader_priv_t *) pgm_data;
			
	/*
	 * This function is the bottom half so it should be quick.
	 * Copy the external interrupt data into our local eib and increment
	 * the usage count
	 */
	spin_lock(&logptr->priv_lock);
	memcpy(&(logptr->local_interrupt_buffer), eib,
	       sizeof(iucv_MessagePending));	
	atomic_inc(&logptr->receive_ready);
	spin_unlock(&logptr->priv_lock);
	
	wake_up_interruptible(&read_wait_queue);

	return;
}

static int
logreader_open (struct inode *inode, struct file *filp)
{
	int dev_num = 0;
	struct logreader_priv_t * logptr = NULL;
	int connect_rc = 0;

	char cp_command[80];
	char cp_response[80];
	unsigned long flags;

	/* extract the minor number */
	dev_num = MINOR(inode->i_rdev);


	/* located the private structure of dev_num */
	if (dev_num > MAXMINOR)
		return -ENODEV;

	logptr = sys_ser[dev_num];
	if (logptr == NULL)
		return -ENODEV;

	/*
	 * only allow for blocking reads to be open
	 */
	if (filp->f_flags & O_NONBLOCK)
		return -ENOSYS;

	/* Besure this device hasn't already been opened */
	spin_lock_irqsave(&logptr->priv_lock, flags);
	if (logptr->dev_in_use)	{
		spin_unlock_irqrestore(&logptr->priv_lock, flags);
		return -EBUSY;
	} else {
		logptr->dev_in_use = 1;
		spin_unlock_irqrestore(&logptr->priv_lock, flags);
	}

	/* set the file options */
	filp->private_data = logptr;
	filp->f_op = &logreader_fops;


	/*
	 * Purge "old" CP records that have been collected but not retrieved and
	 * request CP to begin collecting records on behalf of this virtual
	 * machine.
	 * "RECORDING EREP ON QID userid PURGE"
	 */
	memset(cp_command, 0x00, sizeof(cp_command));
	memset(cp_response, 0x00, sizeof(cp_response));

	/* Set up CP command depending on the system service */
	switch(dev_num) {
		case 0:
			sprintf(cp_command, "RECORDING EREP ON QID %s PURGE",
				userid);
			break;
		default:
			return -ENODEV;
	}
	
	cpcmd(cp_command, cp_response, 80);
	
	/* Register with iucv driver */
	logptr->iucv_handle = iucv_register_program(iucvMagic,
			logptr->system_service, mask, &logreader_iucvops,
			logptr);

	if (logptr->iucv_handle == NULL) {
		printk (KERN_ERR "vmlogrdr: failed to register with"
			"iucv driver\n");
		
		/* Set up CP command depending on the system service */
		memset(cp_command, 0x00, sizeof(cp_command));
		memset(cp_response, 0x00, sizeof(cp_response));
		switch(dev_num) {
			case 0:
				sprintf(cp_command,"RECORDING EREP OFF QID %s ",
					userid);
				break;
			default:
				return -ENODEV;
		}
		cpcmd(cp_command, cp_response, 80);
		logptr->dev_in_use = 0;
		return -EIO;
	}

	/* create connection to the system service */
	spin_lock_irqsave(&logptr->priv_lock, flags);
	logptr->connection_established = 0;
	logptr->iucv_path_severed = 0;
	spin_unlock_irqrestore(&logptr->priv_lock, flags);
	
	connect_rc = iucv_connect (&(logptr->pathid), 10, iucvMagic,
					logptr->system_service, iucv_host, 0,
					NULL, NULL,
					logptr->iucv_handle, NULL);
	if (connect_rc) {
		printk (KERN_ERR "vmlogrdr: iucv connection to %s "
			"failed with rc %i \n", logptr->system_service,
			connect_rc);

		/* Set up CP command depending on the system service */
		memset(cp_command, 0x00, sizeof(cp_command));
		memset(cp_response, 0x00, sizeof(cp_response));
		switch(dev_num) {
			case 0:
				sprintf(cp_command, "RECORDING EREP OFF QID %s ",
					userid);
				break;
			default:
				return -ENODEV;
		}
		cpcmd(cp_command, cp_response, 80);

		iucv_unregister_program(logptr->iucv_handle);
		logptr->iucv_handle = NULL;
		logptr->dev_in_use = 0;
		return -EIO;
	}

	/* We've issued the connect and now we must wait for a
	 * ConnectionComplete or ConnectinSevered Interrupt
	 * before we can continue to process.
	 * note: When the condition in the second parameter is true,
	 *       we'll wake up
	 */
	wait_event(conn_wait_queue, (logptr->connection_established)
		|| (logptr->iucv_path_severed));

	
	if (logptr->iucv_path_severed) {
		iucv_unregister_program(logptr->iucv_handle);
		logptr->iucv_handle = NULL;

		memset(cp_command, 0x00, sizeof(cp_command));
		memset(cp_response, 0x00, sizeof(cp_response));

		switch(dev_num) {
			case 0:
				sprintf(cp_command, "RECORDING EREP OFF QID %s ",
					userid);
				break;
			default:
				return -ENODEV;
		}

		cpcmd(cp_command, cp_response, 80);

		logptr->dev_in_use = 0;
		return -EIO;
	}

	 	
	return 0;
}

static int
logreader_release (struct inode *inode, struct file *filp)
{
	int dev_num = 0;
	struct logreader_priv_t * logptr =
		(struct logreader_priv_t *) filp->private_data;

	char cp_command[80];
	char cp_response[80];

	if (logptr == NULL )
		return -ENODEV;
	
	if (logptr->iucv_handle == NULL)
		return 0;


	 /* extract the minor number */
	dev_num = MINOR(inode->i_rdev);

	iucv_unregister_program(logptr->iucv_handle);
	
	/* Set data structures back to original state
	 * 1. private struct
	 * 2. buffer pointer structure (binary, hex buffers)
	 * 3. buffer
	 */
	logptr->iucv_handle = NULL;
	
	memset(&(logptr->local_interrupt_buffer), 0x00,
		sizeof(iucv_MessagePending));
	atomic_set(&logptr->receive_ready, 0);
	logptr->current_position = logptr->buffer;
	logptr->remaining = 0;
	logptr->residual_length = 0;
	logptr->buffer_free = 1;	


	/* Purge any records remaining in CP storage and turn recording off
	 * "RECORDING record_type OFF QID userid PURGE"
	 */
	memset(cp_command, 0x00, sizeof(cp_command));
	memset(cp_response, 0x00, sizeof(cp_response));

	/* Set up the CP command according to system service */
	switch(dev_num) {
		case 0:
			sprintf(cp_command, "RECORDING EREP OFF QID %s PURGE",
				userid);
			break;
		default:
			return -ENODEV;
	}
	cpcmd(cp_command, cp_response, 80);

	logptr->dev_in_use = 0;

	return 0;
}


static ssize_t
logreader_read (struct file *filp, char *data, size_t count, loff_t * ppos)
{
	int rc = 0;
	struct logreader_priv_t * logptr =
		(struct logreader_priv_t *) filp->private_data;
	int new_record = 0;
	int total_record_length = 0;
	unsigned long	flags;
	while (logptr->buffer_free) {

		/* assume we're not going to receive a a record by
		 * setting rc != 0
		 */
		rc = 1;
		spin_lock_irqsave(&logptr->priv_lock, flags);
		if (atomic_read(&logptr->receive_ready)) {
			/*
		 	 * first check whether we need to receive the second
			 * half of a record (residual_length != 0).
		 	 */
		
			if (logptr->residual_length){
				/* collecting part of the record */
				logptr->remaining = logptr->residual_length;
				logptr->current_position = logptr->buffer;
			} else {
				/* beginning a new record */
				logptr->remaining = (int)
					logptr->local_interrupt_buffer.ln1msg2.ipbfln1f;
				logptr->current_position = logptr->buffer + 4;
				total_record_length = (int)
					logptr->local_interrupt_buffer.ln1msg2.ipbfln1f;
				new_record = 1;
			}
			if (logptr->remaining > buflen)
				logptr->remaining = buflen;

			
			rc = iucv_receive(logptr->pathid,
					  logptr->local_interrupt_buffer.ipmsgid,
					  logptr->local_interrupt_buffer.iptrgcls,
					  logptr->current_position,
					  logptr->remaining,
					  NULL,
					  NULL,
					  &logptr->residual_length);

			if (!logptr->residual_length)
				atomic_dec(&logptr->receive_ready);
		}
		spin_unlock_irqrestore(&logptr->priv_lock, flags);

		if (rc) {
			wait_event_interruptible(read_wait_queue,
					 atomic_read(&logptr->receive_ready));
			if (atomic_read(&logptr->receive_ready) == 0)
				return -ERESTARTSYS; /* woken up by signal */

		} else {
			/*
			 * just received some data so we must mark the
			 * buffer busy
			 */
			logptr->buffer_free = 0;
			if (new_record) {
				/*
				 * if we just received a new record then we
				 * need to add the header length.
				 * To the first 4 bytes of the buffer we add
				 * the length field, which is record + fence
				 */
				int * total_record_length_ptr =
					(int *)logptr->buffer;
	
				*total_record_length_ptr =
					total_record_length + 4;
	
				logptr->remaining += 4;
				logptr->current_position = logptr->buffer;
				
				new_record = 0;
			}

			if (logptr->residual_length == 0){
				/* the whole record has been captured,
				 * now add the fence */
				char * temp_position = logptr->current_position
						       + logptr->remaining;
				memcpy(temp_position, FENCE, sizeof(FENCE));
				logptr->remaining += 4;
			}
		}				

	}
	/* copy only up to end of record */
	if (count > logptr->remaining)
		count = logptr->remaining;
	
	if (copy_to_user(data, logptr->current_position, count))
		return -EFAULT;

	*ppos += count;
	logptr->current_position += count;
	logptr->remaining -= count;

	/* if all data has been transferred, set buffer free */
	if (logptr->remaining == 0)
		logptr->buffer_free = 1;

	return count;


}


static void
get_vm_usrid (char * userid)
{
	char cp_response[81];
	cpcmd("Q USERID", cp_response, 80);
	cp_response[80]=0;
	memcpy(userid, cp_response, 8);
	printk (KERN_DEBUG "vmlogrdr: VM guest id: %s\n", userid);
	return;
}


static void
logreader_hotplug_event(int devmaj, struct logreader_priv_t *logptr, 
			enum vmlogrdr_hotplug_action action) {
#ifdef CONFIG_HOTPLUG
	char *argv[3];
	char *envp[8];
	char  major[20];
	char  minor[20];
	char  service[20];
	char *firstblank;	
				 
	sprintf(major, "MAJOR=%d",   devmaj);
	sprintf(minor, "MINOR=%d",   logptr->minor_num);
	// remember: system_service starts with an * and padded with blanks
	sprintf(service, "SERVICE=%.7s", logptr->system_service+1);
	firstblank = strchr(service, ' ');
	if (firstblank != NULL) {
		*firstblank=0;
	}

	argv[0] = hotplug_path;
	argv[1] = "vmlogrdr";
	argv[2] = NULL;

	envp[0] = "HOME=/";
	envp[1] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";

	switch(action) {
	case VMLOGRDR_HOTPLUG_ADD: 
		envp[2] = "ACTION=add";
		break;
	case VMLOGRDR_HOTPLUG_REMOVE: 
		envp[2] = "ACTION=remove";
		break;
	default:
		BUG();
	}
	envp[3] = major;
	envp[4] = minor;
	envp[5] = service;
	envp[6] = NULL;

	call_usermodehelper(argv[0], argv, envp);
#endif
}



static int
logreader_init(void)
{
	char temp_system[9];
	struct logreader_priv_t * logptr = NULL;
	int rc=0;
	int i;

	if (! MACHINE_IS_VM) {
		printk (KERN_ERR "vmlogrdr: not running under VM, "
				"driver not loaded.\n");
		return -ENODEV;
	}
	
	get_vm_usrid(userid);
	userid[8] = '\0';

	logreader_major = register_chrdev(logreader_major,"vmlogrdr",
					  &logreader_fops);
	if (logreader_major < 0 ) {
		printk (KERN_ERR "vmlogrdr: can't get major %d\n",
		logreader_major);	
		return -EIO;			
	}

	int parm_no;
	int minor;
	for (parm_no=0; parm_no < MAXSERVICES; ++parm_no ) {
		printk (KERN_DEBUG "vmlogrdr: services[%d] == %s \n",parm_no,
			services[parm_no]);
		// compare to the know system services (excluding the leading *)
		// important: the system name needs to be 8 characters,
		//            padded with blanks and not 0.
		if (memcmp (services[parm_no], "LOGREC", 7) == 0) {
			memcpy(temp_system, "*LOGREC ", 8);
			minor = 0;
		} else if (services[parm_no] == 0) {
			break;
		} else {
			printk (KERN_ERR "vmlogrdr: unknown service: %s \n",
				services[parm_no] );
			rc=-EINVAL;
			goto free_sys_ser;
		}
	
		if (sys_ser[minor] == NULL) {	
			
			/* Allocate memory for:
			 * 1 logreader_priv_t
			 * 1 buffer (4096 bytes)
			 */
			logptr = kmalloc (sizeof(struct logreader_priv_t),
					  GFP_KERNEL);
			if (!logptr) {
				rc=-ENOMEM;
				goto free_sys_ser;
			}
			memset(logptr, 0x00, sizeof(struct logreader_priv_t));
			sys_ser[minor] = logptr;

			/* set all structures */
			memcpy(logptr->system_service, temp_system, 8);	
			logptr->minor_num = minor;
			logptr->priv_lock = SPIN_LOCK_UNLOCKED;
			logptr->buffer_free = 1;
			logptr->buffer = kmalloc (4096, GFP_KERNEL);
			if (!logptr->buffer) {
				kfree(logptr);
				sys_ser[minor]=0;
				rc=-ENOMEM;
				goto free_sys_ser;
			}
			memset(logptr->buffer, 0x00, buflen);
			logptr->current_position = logptr->buffer;
			
			logreader_hotplug_event(logreader_major, logptr, 
						VMLOGRDR_HOTPLUG_ADD);
		
		} else {
			printk (KERN_WARNING "vmlogrdr: Service %s defined more"
				" then once -> ignore \n", services[parm_no]);
		}
	}
	printk (KERN_INFO "vmlogrdr: driver loaded\n");

	return 0;
free_sys_ser:
	for (i=0; i < MAXMINOR; ++i ) {
		if ( sys_ser[i] != 0 ) {
			logreader_hotplug_event(logreader_major, sys_ser[i], 
						VMLOGRDR_HOTPLUG_REMOVE);			
			kfree (sys_ser[i]->buffer);
			kfree (sys_ser[i]);
		}
	}
	
	unregister_chrdev(logreader_major, "vmlogrdr");
	printk (KERN_ERR "vmlogrdr: driver not loaded.\n");

	return rc;	
}


static void
logreader_exit(void)
{
	/* return all storage and unregister driver */
	int index = 0;
	struct logreader_priv_t * logptr = NULL;

	for (index = 0; index < MAXMINOR; index++) {
		logptr = sys_ser[index];
		if (logptr) {
			kfree(logptr->buffer);
			kfree(logptr);
			sys_ser[index] = NULL;
			logreader_hotplug_event(logreader_major, logptr,
						VMLOGRDR_HOTPLUG_REMOVE);
		}
	}
	unregister_chrdev(logreader_major, "vmlogrdr");
	printk (KERN_INFO "vmlogrdr: driver unloaded\n");
	return;
}

module_init(logreader_init);
module_exit(logreader_exit);





