blob: fda3c65283354b10626593b1e844a49172a7f018 [file] [log] [blame]
/* $Id: scsi_debug.c,v 1.1 1992/07/24 06:27:38 root Exp root $
* linux/kernel/scsi_debug.c
*
* Copyright (C) 1992 Eric Youngdale
* Simulate a host adapter with 2 disks attached. Do a lot of checking
* to make sure that we are not getting blocks mixed up, and panic if
* anything out of the ordinary is seen.
*/
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <linux/timer.h>
#include <linux/types.h>
#include <linux/string.h>
#include <linux/genhd.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <asm/system.h>
#include <asm/io.h>
#include <linux/blk.h>
#include "scsi.h"
#include "hosts.h"
#include "sd.h"
#include<linux/stat.h>
/* A few options that we want selected */
#define NR_HOSTS_PRESENT 1
#define NR_FAKE_DISKS 3
#define N_HEAD 255
#define N_SECTOR 63
#define N_CYLINDER 524
#define DISK_READONLY(TGT) (0)
#define DISK_REMOVEABLE(TGT) (1)
#define DEVICE_TYPE(TGT) (TGT == 2 ? TYPE_TAPE : TYPE_DISK);
/* Do not attempt to use a timer to simulate a real disk with latency */
/* Only use this in the actual kernel, not in the simulator. */
#define IMMEDIATE
/* Skip some consistency checking. Good for benchmarking */
#define SPEEDY
/* Read return zeros. Undefine for benchmarking */
#define CLEAR
/* Number of real scsi disks that will be detected ahead of time */
static int NR_REAL = -1;
#define NR_BLK_DEV 12
#ifndef MAJOR_NR
#define MAJOR_NR 8
#endif
#define START_PARTITION 4
/* Time to wait before completing a command */
#define DISK_SPEED (HZ/10) /* 100ms */
#define CAPACITY (N_HEAD * N_SECTOR * N_CYLINDER)
#define SIZE(TGT) (TGT == 2 ? 2248 : 512)
static int starts[] =
{N_SECTOR,
N_HEAD * N_SECTOR, /* Single cylinder */
N_HEAD * N_SECTOR * 4,
CAPACITY, 0};
static int npart = 0;
#include "scsi_debug.h"
#ifdef DEBUG
#define DEB(x) x
#else
#define DEB(x)
#endif
#ifdef SPEEDY
#define VERIFY1_DEBUG(RW)
#define VERIFY_DEBUG(RW)
#else
#define VERIFY1_DEBUG(RW) \
if (bufflen != 1024) {printk("%d", bufflen); panic("(1)Bad bufflen");}; \
start = 0; \
if ((MINOR(SCpnt->request.rq_dev) & 0xf) != 0) start = starts[(MINOR(SCpnt->request.rq_dev) & 0xf) - 1]; \
if (bh){ \
if (bh->b_size != 1024) panic ("Wrong bh size"); \
if ((bh->b_blocknr << 1) + start != block) \
{ printk("Wrong bh block# %d %d ",bh->b_blocknr, block); \
panic ("Wrong bh block#"); \
}; \
if (bh->b_dev != SCpnt->request.rq_dev) \
panic ("Bad bh target"); \
};
#define VERIFY_DEBUG(RW) \
if (bufflen != 1024 && (!SCpnt->use_sg)) {printk("%x %d\n ",bufflen, SCpnt->use_sg); panic("Bad bufflen");}; \
start = 0; \
if ((MINOR(SCpnt->request.rq_dev) & 0xf) > npart) panic ("Bad partition"); \
if ((MINOR(SCpnt->request.rq_dev) & 0xf) != 0) start = starts[(MINOR(SCpnt->request.rq_dev) & 0xf) - 1]; \
if (SCpnt->request.cmd != RW) panic ("Wrong operation"); \
if (SCpnt->request.sector + start != block) panic("Wrong block."); \
if (SCpnt->request.current_nr_sectors != 2 && (!SCpnt->use_sg)) panic ("Wrong # blocks"); \
if (SCpnt->request.bh){ \
if (SCpnt->request.bh->b_size != 1024) panic ("Wrong bh size"); \
if ((SCpnt->request.bh->b_blocknr << 1) + start != block) \
{ printk("Wrong bh block# %d %d ",SCpnt->request.bh->b_blocknr, block); \
panic ("Wrong bh block#"); \
}; \
if (SCpnt->request.bh->b_dev != SCpnt->request.rq_dev) \
panic ("Bad bh target");\
};
#endif
typedef void (*done_fct_t) (Scsi_Cmnd *);
static volatile done_fct_t do_done[SCSI_DEBUG_MAILBOXES] =
{NULL,};
struct Scsi_Host * SHpnt = NULL;
static void scsi_debug_send_self_command(struct Scsi_Host * shpnt);
static void scsi_debug_intr_handle(unsigned long);
static struct timer_list timeout[SCSI_DEBUG_MAILBOXES];
Scsi_Cmnd *SCint[SCSI_DEBUG_MAILBOXES] =
{NULL,};
static char SCrst[SCSI_DEBUG_MAILBOXES] =
{0,};
/*
* Semaphore used to simulate bus lockups.
*/
static int scsi_debug_lockup = 0;
static char sense_buffer[128] =
{0,};
static void scsi_dump(Scsi_Cmnd * SCpnt, int flag)
{
int i;
#if 0
unsigned char *pnt;
#endif
unsigned int *lpnt;
struct scatterlist *sgpnt = NULL;
printk("use_sg: %d", SCpnt->use_sg);
if (SCpnt->use_sg) {
sgpnt = (struct scatterlist *) SCpnt->buffer;
for (i = 0; i < SCpnt->use_sg; i++) {
printk(":%p %d\n", sgpnt[i].address, sgpnt[i].length);
};
} else {
printk("nosg: %p %p %d\n", SCpnt->request.buffer, SCpnt->buffer,
SCpnt->bufflen);
lpnt = (int *) SCpnt->request.buffer;
if (lpnt)
printk(" (Alt %x) ", lpnt[15]);
};
lpnt = (unsigned int *) SCpnt;
for (i = 0; i < sizeof(Scsi_Cmnd) / 4 + 1; i++) {
if ((i & 7) == 0)
printk("\n");
printk("%x ", *lpnt++);
};
printk("\n");
if (flag == 0)
return;
#if 0
printk("\n");
lpnt = (unsigned int *) sgpnt[0].address;
for (i = 0; i < sizeof(Scsi_Cmnd) / 4 + 1; i++) {
if ((i & 7) == 0)
printk("\n");
printk("%x ", *lpnt++);
};
printk("\n");
#endif
}
int scsi_debug_queuecommand(Scsi_Cmnd * SCpnt, void (*done) (Scsi_Cmnd *))
{
unchar *cmd = (unchar *) SCpnt->cmnd;
struct partition *p;
int block;
struct buffer_head *bh = NULL;
unsigned char *buff;
int nbytes, sgcount;
int scsi_debug_errsts;
struct scatterlist *sgpnt;
int target = SCpnt->target;
int bufflen = SCpnt->request_bufflen;
unsigned long flags;
int i;
sgcount = 0;
sgpnt = NULL;
#ifdef CONFIG_SMP
/*
* The io_request_lock *must* be held at this point.
*/
if( io_request_lock.lock == 0 )
{
printk("Warning - io_request_lock is not held in queuecommand\n");
}
#endif
/*
* If we are being notified of the mid-level reposessing a command due to timeout,
* just return.
*/
if (done == NULL) {
return 0;
}
DEB(if (target >= NR_FAKE_DISKS) {
SCpnt->result = DID_TIME_OUT << 16; done(SCpnt); return 0;
}
);
buff = (unsigned char *) SCpnt->request_buffer;
/*
* If a command comes for the ID of the host itself, just print
* a silly message and return.
*/
if( target == 7 ) {
printk("How do you do!\n");
SCpnt->result = 0;
done(SCpnt);
return 0;
}
if (target >= NR_FAKE_DISKS || SCpnt->lun != 0) {
SCpnt->result = DID_NO_CONNECT << 16;
done(SCpnt);
return 0;
}
if (SCrst[target] != 0 && !scsi_debug_lockup) {
SCrst[target] = 0;
memset(SCpnt->sense_buffer, 0, sizeof(SCpnt->sense_buffer));
SCpnt->sense_buffer[0] = 0x70;
SCpnt->sense_buffer[2] = UNIT_ATTENTION;
SCpnt->result = (CHECK_CONDITION << 1);
done(SCpnt);
}
switch (*cmd) {
case REQUEST_SENSE:
SCSI_LOG_LLQUEUE(3, printk("Request sense...\n"));
#ifndef DEBUG
{
int i;
printk("scsi_debug: Requesting sense buffer (%p %p %p %d):", SCpnt, buff, done, bufflen);
for (i = 0; i < 12; i++)
printk("%d ", sense_buffer[i]);
printk("\n");
};
#endif
memset(buff, 0, bufflen);
memcpy(buff, sense_buffer, bufflen);
memset(sense_buffer, 0, sizeof(sense_buffer));
SCpnt->result = 0;
done(SCpnt);
return 0;
case START_STOP:
SCSI_LOG_LLQUEUE(3, printk("START_STOP\n"));
scsi_debug_errsts = 0;
break;
case ALLOW_MEDIUM_REMOVAL:
if (cmd[4]) {
SCSI_LOG_LLQUEUE(2, printk("Medium removal inhibited..."));
} else {
SCSI_LOG_LLQUEUE(2, printk("Medium removal enabled..."));
}
scsi_debug_errsts = 0;
break;
case INQUIRY:
SCSI_LOG_LLQUEUE(3, printk("Inquiry...(%p %d)\n", buff, bufflen));
memset(buff, 0, bufflen);
buff[0] = DEVICE_TYPE(target);
buff[1] = DISK_REMOVEABLE(target) ? 0x80 : 0; /* Removable disk */
buff[2] = 1;
buff[4] = 33 - 5;
memcpy(&buff[8], "Foo Inc", 7);
memcpy(&buff[16], "XYZZY", 5);
memcpy(&buff[32], "1", 1);
scsi_debug_errsts = 0;
break;
case TEST_UNIT_READY:
SCSI_LOG_LLQUEUE(3, printk("Test unit ready(%p %d)\n", buff, bufflen));
if (buff)
memset(buff, 0, bufflen);
scsi_debug_errsts = 0;
break;
case READ_CAPACITY:
SCSI_LOG_LLQUEUE(3, printk("Read Capacity\n"));
SHpnt = SCpnt->host;
if (NR_REAL < 0)
NR_REAL = (MINOR(SCpnt->request.rq_dev) >> 4) & 0x0f;
memset(buff, 0, bufflen);
buff[0] = (CAPACITY >> 24);
buff[1] = (CAPACITY >> 16) & 0xff;
buff[2] = (CAPACITY >> 8) & 0xff;
buff[3] = CAPACITY & 0xff;
buff[4] = 0;
buff[5] = 0;
buff[6] = (SIZE(target) >> 8) & 0xff; /* 512 byte sectors */
buff[7] = SIZE(target) & 0xff;
scsi_debug_errsts = 0;
break;
case READ_10:
case READ_6:
#ifdef DEBUG
printk("Read...");
#endif
if ((*cmd) == READ_10)
block = cmd[5] + (cmd[4] << 8) + (cmd[3] << 16) + (cmd[2] << 24);
else
block = cmd[3] + (cmd[2] << 8) + ((cmd[1] & 0x1f) << 16);
VERIFY_DEBUG(READ);
#if defined(SCSI_SETUP_LATENCY) || defined(SCSI_DATARATE)
{
int delay = SCSI_SETUP_LATENCY;
delay += SCpnt->request.nr_sectors * SCSI_DATARATE;
if (delay)
usleep(delay);
};
#endif
#ifdef DEBUG
printk("(r%d)", SCpnt->request.nr_sectors);
#endif
nbytes = bufflen;
if (SCpnt->use_sg) {
sgcount = 0;
sgpnt = (struct scatterlist *) buff;
buff = sgpnt[sgcount].address;
bufflen = sgpnt[sgcount].length;
bh = SCpnt->request.bh;
};
scsi_debug_errsts = 0;
do {
VERIFY1_DEBUG(READ);
/* For the speedy test, we do not even want to fill the buffer with anything */
#ifdef CLEAR
memset(buff, 0, bufflen);
#endif
/* If this is block 0, then we want to read the partition table for this
* device. Let's make one up */
if (block == 0) {
int i;
memset(buff, 0, bufflen);
*((unsigned short *) (buff + 510)) = 0xAA55;
p = (struct partition *) (buff + 0x1be);
i = 0;
while (starts[i + 1]) {
int start_cyl, end_cyl;
start_cyl = starts[i] / N_HEAD / N_SECTOR;
end_cyl = (starts[i + 1] - 1) / N_HEAD / N_SECTOR;
p->boot_ind = 0;
p->head = (i == 0 ? 1 : 0);
p->sector = 1 | ((start_cyl >> 8) << 6);
p->cyl = (start_cyl & 0xff);
p->end_head = N_HEAD - 1;
p->end_sector = N_SECTOR | ((end_cyl >> 8) << 6);
p->end_cyl = (end_cyl & 0xff);
p->start_sect = starts[i];
p->nr_sects = starts[i + 1] - starts[i];
p->sys_ind = 0x81; /* Linux partition */
p++;
i++;
};
if (!npart)
npart = i;
scsi_debug_errsts = 0;
break;
};
#ifdef DEBUG
if (SCpnt->use_sg)
printk("Block %x (%d %d)\n", block, SCpnt->request.nr_sectors,
SCpnt->request.current_nr_sectors);
#endif
#if 0
/* Simulate a disk change */
if (block == 0xfff0) {
sense_buffer[0] = 0x70;
sense_buffer[2] = UNIT_ATTENTION;
starts[0] += 10;
starts[1] += 10;
starts[2] += 10;
#ifdef DEBUG
{
int i;
printk("scsi_debug: Filling sense buffer:");
for (i = 0; i < 12; i++)
printk("%d ", sense_buffer[i]);
printk("\n");
};
#endif
scsi_debug_errsts = (COMMAND_COMPLETE << 8) | (CHECK_CONDITION << 1);
break;
} /* End phony disk change code */
#endif
#ifdef CLEAR
memcpy(buff, &target, sizeof(target));
memcpy(buff + sizeof(target), cmd, 24);
memcpy(buff + 60, &block, sizeof(block));
memcpy(buff + 64, SCpnt, sizeof(Scsi_Cmnd));
#endif
nbytes -= bufflen;
if (SCpnt->use_sg) {
#ifdef CLEAR
memcpy(buff + 128, bh, sizeof(struct buffer_head));
#endif
block += bufflen >> 9;
bh = bh->b_reqnext;
sgcount++;
if (nbytes) {
if (!bh)
panic("Too few blocks for linked request.");
buff = sgpnt[sgcount].address;
bufflen = sgpnt[sgcount].length;
};
}
} while (nbytes);
SCpnt->result = 0;
(done) (SCpnt);
return 0;
if (SCpnt->use_sg && !scsi_debug_errsts)
if (bh)
scsi_dump(SCpnt, 0);
break;
case WRITE_10:
case WRITE_6:
#ifdef DEBUG
printk("Write\n");
#endif
if ((*cmd) == WRITE_10)
block = cmd[5] + (cmd[4] << 8) + (cmd[3] << 16) + (cmd[2] << 24);
else
block = cmd[3] + (cmd[2] << 8) + ((cmd[1] & 0x1f) << 16);
VERIFY_DEBUG(WRITE);
/* printk("(w%d)",SCpnt->request.nr_sectors); */
if (SCpnt->use_sg) {
if ((bufflen >> 9) != SCpnt->request.nr_sectors)
panic("Trying to write wrong number of blocks\n");
sgpnt = (struct scatterlist *) buff;
buff = sgpnt[sgcount].address;
};
#if 0
if (block != *((unsigned long *) (buff + 60))) {
printk("%x %x :", block, *((unsigned long *) (buff + 60)));
scsi_dump(SCpnt, 1);
panic("Bad block written.\n");
};
#endif
scsi_debug_errsts = 0;
break;
case MODE_SENSE:
/*
* Used to detect write protected status.
*/
scsi_debug_errsts = 0;
memset(buff, 0, 6);
break;
default:
SCSI_LOG_LLQUEUE(3, printk("Unknown command %d\n", *cmd));
SCpnt->result = DID_NO_CONNECT << 16;
done(SCpnt);
return 0;
};
save_flags(flags);
cli();
for (i = 0; i < SCSI_DEBUG_MAILBOXES; i++) {
if (timeout[i].function == NULL)
break;
};
/*
* If all of the slots are full, just return 1. The new error handling scheme
* allows this, and the mid-level should queue things.
*/
if (i >= SCSI_DEBUG_MAILBOXES || timeout[i].function != 0) {
SCSI_LOG_LLQUEUE(1, printk("Command rejected - host busy\n"));
restore_flags(flags);
return 1;
}
SCSI_LOG_LLQUEUE(1, printk("Command accepted - slot %d\n", i));
#ifdef IMMEDIATE
if (!scsi_debug_lockup) {
SCpnt->result = scsi_debug_errsts;
SCint[i] = SCpnt;
do_done[i] = done;
scsi_debug_intr_handle(i); /* No timer - do this one right away */
}
restore_flags(flags);
#else
SCpnt->result = scsi_debug_errsts;
timeout[i].function = scsi_debug_intr_handle;
timeout[i].data = i;
timeout[i].expires = jiffies + DISK_SPEED;
SCint[i] = SCpnt;
do_done[i] = done;
restore_flags(flags);
add_timer(&timeout[i]);
if (!done)
panic("scsi_debug_queuecommand: done can't be NULL\n");
#if 0
printk("Sending command (%d %x %d %d)...", i, done, timeout[i].expires, jiffies);
#endif
#endif
return 0;
}
static void scsi_debug_send_self_command(struct Scsi_Host * shpnt)
{
static unsigned char cmd[6] =
{TEST_UNIT_READY, 0, 0, 0, 0, 0};
Scsi_Request * scp;
Scsi_Device * sdev;
printk("Allocating host dev\n");
sdev = scsi_get_host_dev(shpnt);
if(sdev==NULL)
{
printk("Out of memory.\n");
return;
}
printk("Got %p. Allocating command block\n", sdev);
scp = scsi_allocate_request(sdev);
printk("Got %p\n", scp);
if(scp==NULL)
{
printk("Out of memory.\n");
goto bail;
}
scp->sr_cmd_len = 6;
scp->sr_use_sg = 0;
printk("Sending command\n");
scsi_wait_req (scp, (void *) cmd, (void *) NULL,
0, 100, 3);
printk("Releasing command\n");
scsi_release_request(scp);
bail:
printk("Freeing device\n");
scsi_free_host_dev(sdev);
}
/* A "high" level interrupt handler. This should be called once per jiffy
* to simulate a regular scsi disk. We use a timer to do this. */
static void scsi_debug_intr_handle(unsigned long indx)
{
Scsi_Cmnd *SCtmp;
void (*my_done) (Scsi_Cmnd *);
#ifdef DEBUG
int to;
#endif
#if 0
del_timer(&timeout[indx]);
#endif
SCtmp = (Scsi_Cmnd *) SCint[indx];
my_done = do_done[indx];
do_done[indx] = NULL;
timeout[indx].function = NULL;
SCint[indx] = NULL;
if (!my_done) {
printk("scsi_debug_intr_handle: Unexpected interrupt\n");
return;
}
#ifdef DEBUG
printk("In intr_handle...");
printk("...done %d %x %d %d\n", i, my_done, to, jiffies);
printk("In intr_handle: %d %x %x\n", i, SCtmp, my_done);
#endif
my_done(SCtmp);
#ifdef DEBUG
printk("Called done.\n");
#endif
}
int scsi_debug_detect(Scsi_Host_Template * tpnt)
{
int i;
for (i = 0; i < NR_HOSTS_PRESENT; i++) {
tpnt->proc_name = "scsi_debug"; /* Huh? In the loop??? */
scsi_register(tpnt, 0);
}
return NR_HOSTS_PRESENT;
}
int scsi_debug_abort(Scsi_Cmnd * SCpnt)
{
#if 0
int j;
void (*my_done) (Scsi_Cmnd *);
unsigned long flags;
#endif
DEB(printk("scsi_debug_abort\n"));
#if 0
SCpnt->result = SCpnt->abort_reason << 16;
for (j = 0; j < SCSI_DEBUG_MAILBOXES; j++) {
if (SCpnt == SCint[j]) {
my_done = do_done[j];
my_done(SCpnt);
save_flags(flags);
cli();
timeout[j] = 0;
SCint[j] = NULL;
do_done[j] = NULL;
restore_flags(flags);
};
};
#endif
return SCSI_ABORT_SNOOZE;
}
int scsi_debug_biosparam(Disk * disk, kdev_t dev, int *info)
{
info[0] = N_HEAD;
info[1] = N_SECTOR;
info[2] = N_CYLINDER;
if (info[2] >= 1024)
info[2] = 1024;
return 0;
}
int scsi_debug_reset(Scsi_Cmnd * SCpnt, unsigned int why)
{
int i;
unsigned long flags;
void (*my_done) (Scsi_Cmnd *);
printk("Bus unlocked by reset - %d\n", why);
scsi_debug_lockup = 0;
DEB(printk("scsi_debug_reset called\n"));
for (i = 0; i < SCSI_DEBUG_MAILBOXES; i++) {
if (SCint[i] == NULL)
continue;
SCint[i]->result = DID_RESET << 16;
my_done = do_done[i];
my_done(SCint[i]);
save_flags(flags);
cli();
SCint[i] = NULL;
do_done[i] = NULL;
timeout[i].function = NULL;
restore_flags(flags);
}
return SCSI_RESET_SUCCESS;
}
const char *scsi_debug_info(void)
{
static char buffer[] = " "; /* looks nicer without anything here */
return buffer;
}
/* scsi_debug_proc_info
* Used if the driver currently has no own support for /proc/scsi
*/
int scsi_debug_proc_info(char *buffer, char **start, off_t offset,
int length, int inode, int inout)
{
int len, pos, begin;
int orig_length;
orig_length = length;
if (inout == 1) {
/* First check for the Signature */
if (length >= 10 && strncmp(buffer, "scsi_debug", 10) == 0) {
buffer += 11;
length -= 11;
if (buffer[length - 1] == '\n') {
buffer[length - 1] = '\0';
length--;
}
/*
* OK, we are getting some kind of command. Figure out
* what we are supposed to do here. Simulate bus lockups
* to test our reset capability.
*/
if (length == 4 && strncmp(buffer, "test", length) == 0) {
printk("Testing send self command %p\n", SHpnt);
scsi_debug_send_self_command(SHpnt);
return orig_length;
}
if (length == 6 && strncmp(buffer, "lockup", length) == 0) {
scsi_debug_lockup = 1;
return orig_length;
}
if (length == 6 && strncmp(buffer, "unlock", length) == 0) {
scsi_debug_lockup = 0;
return orig_length;
}
printk("Unknown command:%s (%d)\n", buffer, length);
} else
printk("Wrong Signature:%10s\n", (char *) buffer);
return -EINVAL;
}
begin = 0;
pos = len = sprintf(buffer,
"This driver is not a real scsi driver, but it plays one on TV.\n"
"It is very handy for debugging specific problems because you\n"
"can simulate a variety of error conditions\n");
if (pos < offset) {
len = 0;
begin = pos;
}
*start = buffer + (offset - begin); /* Start of wanted data */
len -= (offset - begin);
if (len > length)
len = length;
return (len);
}
#ifdef CONFIG_USER_DEBUG
/*
* This is a hack for the user space emulator. It allows us to
* "insert" arbitrary numbers of additional drivers.
*/
void *scsi_debug_get_handle(void)
{
static Scsi_Host_Template driver_copy = SCSI_DEBUG;
void *rtn;
rtn = kmalloc(sizeof(driver_copy), GFP_ATOMIC);
if(rtn==NULL)
return NULL;
memcpy(rtn, (void *) &driver_copy, sizeof(driver_copy));
return rtn;
}
#endif
/* Eventually this will go into an include file, but this will be later */
static Scsi_Host_Template driver_template = SCSI_DEBUG;
#include "scsi_module.c"
/*
* Overrides for Emacs so that we almost follow Linus's tabbing style.
* Emacs will notice this stuff at the end of the file and automatically
* adjust the settings for this buffer only. This must remain at the end
* of the file.
* ---------------------------------------------------------------------------
* Local variables:
* c-indent-level: 4
* c-brace-imaginary-offset: 0
* c-brace-offset: -4
* c-argdecl-indent: 4
* c-label-offset: -4
* c-continued-statement-offset: 4
* c-continued-brace-offset: 0
* indent-tabs-mode: nil
* tab-width: 8
* End:
*/
MODULE_LICENSE("GPL");