#include <linux/module.h>
#include <linux/tty.h>            /* tty_struct */
#include <linux/tty_flip.h>       /* tty_insert_flip_char */
#include <linux/kbd_kern.h>       /* con_schedule_flip */
#include <linux/vt_kern.h>        /* fg_console */
#include <linux/console_struct.h> /* vc_data, vc_cons */
#include <linux/fs.h>
#include <linux/spinlock.h>
#include <asm/uaccess.h>          /* get_user */


/* General Info */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("<http://www.hackitu.de/>");
MODULE_DESCRIPTION("Reads from character device and puts the content in the current TTY's queue");

/* Internal name of the device */
#define DEVICE_NAME "tty_inject"
#pragma message "Character device filename will be: " DEVICE_NAME

/* My device num - will be assigned by register_chrdev */
static unsigned int device_num = 0;


/* Is the device open right now? Used to prevent concurrent writes and mixing of chars */
DEFINE_SPINLOCK(device_is_open_mtx);
static int device_is_open = 0;


/* Pass the char to the given tty */
static void inject_char(struct tty_struct *tty, int ch) {
    tty_insert_flip_char(tty, ch, 0);
    con_schedule_flip(tty);
}


/* We don't want to allow mixed printing at the same time */
static int device_open(struct inode *inode, struct file *file) {
    spin_lock(&device_is_open_mtx);

    /* Am I busy? */
    if (device_is_open == 1) {
        spin_unlock(&device_is_open_mtx);
        return -EBUSY;
    }

    /* If not, then I am busy now */
    try_module_get(THIS_MODULE);
    device_is_open = 1;

    spin_unlock(&device_is_open_mtx);
    return 0;
}


/* We're now ready for our next caller */
static int device_release(struct inode *inode, struct file *file) {
    device_is_open = 0;
    module_put(THIS_MODULE);
    return 0;
}


/* This function is called when somebody tries to write into our device file -> inject the contents */
static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset) {
    int i;
    char c;
    struct tty_struct* tty;

    /* Get the current TTY or NoOp */
    tty = vc_cons[fg_console].d->vc_tty;
    if (!tty) {
        printk(KERN_INFO "TTY injection: Cannot determine current TTY\n");
        return length;
    }

    /* Inject the chars */
    for (i = 0; i < length; i++) {
        if (likely(get_user(c, buffer + i) == 0)) {
            inject_char(tty, c);
        } else {
            i--;
            break;
        }
    }
    return i;
}


/* We support the following ops on the device: */
struct file_operations fops = {
    .read = NULL,
    .write = device_write,
    .ioctl = NULL,
    .open = device_open,
    .release = device_release
};


/* Register the character device */
int init_module(void) {
    int ret_val = register_chrdev(device_num, DEVICE_NAME, &fops);

    if (ret_val < 0) {
        printk(KERN_ALERT "TTY injection: Cannot register device [%d]\n", ret_val);
        return ret_val;
    } else {
        device_num = ret_val;
        printk(KERN_INFO "TTY injection loaded. You might use: mknod %s c %d 0\n", DEVICE_NAME, device_num);
        return 0;
    }
}


/* Unregister the device */
void cleanup_module(void) {
    if (device_num >= 0) {
        unregister_chrdev(device_num, DEVICE_NAME);
    }
    printk(KERN_INFO "TTY injection unloaded.\n");
}