Revision fc3501d411d34823fb9be248a95a0c44f945866f authored by Sven Wegener on 11 February 2009, 21:04:23 UTC, committed by Linus Torvalds on 11 February 2009, 22:25:35 UTC
We need to pass an unsigned long as the minimum, because it gets casted to an unsigned long in the sysctl handler. If we pass an int, we'll access four more bytes on 64bit arches, resulting in a random minimum value. [rientjes@google.com: fix type of `old_bytes'] Signed-off-by: Sven Wegener <sven.wegener@stealer.net> Cc: Peter Zijlstra <peterz@infradead.org> Cc: Dave Chinner <david@fromorbit.com> Cc: Christoph Lameter <cl@linux-foundation.org> Cc: David Rientjes <rientjes@google.com> Signed-off-by: David Rientjes <rientjes@google.com> Signed-off-by: Andrew Morton <akpm@linux-foundation.org> Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
1 parent 35887b1
isdnloop.c
/* $Id: isdnloop.c,v 1.11.6.7 2001/11/11 19:54:31 kai Exp $
*
* ISDN low-level module implementing a dummy loop driver.
*
* Copyright 1997 by Fritz Elfert (fritz@isdn4linux.de)
*
* This software may be used and distributed according to the terms
* of the GNU General Public License, incorporated herein by reference.
*
*/
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/sched.h>
#include "isdnloop.h"
static char *revision = "$Revision: 1.11.6.7 $";
static char *isdnloop_id = "loop0";
MODULE_DESCRIPTION("ISDN4Linux: Pseudo Driver that simulates an ISDN card");
MODULE_AUTHOR("Fritz Elfert");
MODULE_LICENSE("GPL");
module_param(isdnloop_id, charp, 0);
MODULE_PARM_DESC(isdnloop_id, "ID-String of first card");
static int isdnloop_addcard(char *);
/*
* Free queue completely.
*
* Parameter:
* card = pointer to card struct
* channel = channel number
*/
static void
isdnloop_free_queue(isdnloop_card * card, int channel)
{
struct sk_buff_head *queue = &card->bqueue[channel];
skb_queue_purge(queue);
card->sndcount[channel] = 0;
}
/*
* Send B-Channel data to another virtual card.
* This routine is called via timer-callback from isdnloop_pollbchan().
*
* Parameter:
* card = pointer to card struct.
* ch = channel number (0-based)
*/
static void
isdnloop_bchan_send(isdnloop_card * card, int ch)
{
isdnloop_card *rcard = card->rcard[ch];
int rch = card->rch[ch], len, ack;
struct sk_buff *skb;
isdn_ctrl cmd;
while (card->sndcount[ch]) {
if ((skb = skb_dequeue(&card->bqueue[ch]))) {
len = skb->len;
card->sndcount[ch] -= len;
ack = *(skb->head); /* used as scratch area */
cmd.driver = card->myid;
cmd.arg = ch;
if (rcard){
rcard->interface.rcvcallb_skb(rcard->myid, rch, skb);
} else {
printk(KERN_WARNING "isdnloop: no rcard, skb dropped\n");
dev_kfree_skb(skb);
};
cmd.command = ISDN_STAT_BSENT;
cmd.parm.length = len;
card->interface.statcallb(&cmd);
} else
card->sndcount[ch] = 0;
}
}
/*
* Send/Receive Data to/from the B-Channel.
* This routine is called via timer-callback.
* It schedules itself while any B-Channel is open.
*
* Parameter:
* data = pointer to card struct, set by kernel timer.data
*/
static void
isdnloop_pollbchan(unsigned long data)
{
isdnloop_card *card = (isdnloop_card *) data;
unsigned long flags;
if (card->flags & ISDNLOOP_FLAGS_B1ACTIVE)
isdnloop_bchan_send(card, 0);
if (card->flags & ISDNLOOP_FLAGS_B2ACTIVE)
isdnloop_bchan_send(card, 1);
if (card->flags & (ISDNLOOP_FLAGS_B1ACTIVE | ISDNLOOP_FLAGS_B2ACTIVE)) {
/* schedule b-channel polling again */
spin_lock_irqsave(&card->isdnloop_lock, flags);
card->rb_timer.expires = jiffies + ISDNLOOP_TIMER_BCREAD;
add_timer(&card->rb_timer);
card->flags |= ISDNLOOP_FLAGS_RBTIMER;
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
} else
card->flags &= ~ISDNLOOP_FLAGS_RBTIMER;
}
/*
* Parse ICN-type setup string and fill fields of setup-struct
* with parsed data.
*
* Parameter:
* setup = setup string, format: [caller-id],si1,si2,[called-id]
* cmd = pointer to struct to be filled.
*/
static void
isdnloop_parse_setup(char *setup, isdn_ctrl * cmd)
{
char *t = setup;
char *s = strchr(t, ',');
*s++ = '\0';
strlcpy(cmd->parm.setup.phone, t, sizeof(cmd->parm.setup.phone));
s = strchr(t = s, ',');
*s++ = '\0';
if (!strlen(t))
cmd->parm.setup.si1 = 0;
else
cmd->parm.setup.si1 = simple_strtoul(t, NULL, 10);
s = strchr(t = s, ',');
*s++ = '\0';
if (!strlen(t))
cmd->parm.setup.si2 = 0;
else
cmd->parm.setup.si2 =
simple_strtoul(t, NULL, 10);
strlcpy(cmd->parm.setup.eazmsn, s, sizeof(cmd->parm.setup.eazmsn));
cmd->parm.setup.plan = 0;
cmd->parm.setup.screen = 0;
}
typedef struct isdnloop_stat {
char *statstr;
int command;
int action;
} isdnloop_stat;
/* *INDENT-OFF* */
static isdnloop_stat isdnloop_stat_table[] =
{
{"BCON_", ISDN_STAT_BCONN, 1}, /* B-Channel connected */
{"BDIS_", ISDN_STAT_BHUP, 2}, /* B-Channel disconnected */
{"DCON_", ISDN_STAT_DCONN, 0}, /* D-Channel connected */
{"DDIS_", ISDN_STAT_DHUP, 0}, /* D-Channel disconnected */
{"DCAL_I", ISDN_STAT_ICALL, 3}, /* Incoming call dialup-line */
{"DSCA_I", ISDN_STAT_ICALL, 3}, /* Incoming call 1TR6-SPV */
{"FCALL", ISDN_STAT_ICALL, 4}, /* Leased line connection up */
{"CIF", ISDN_STAT_CINF, 5}, /* Charge-info, 1TR6-type */
{"AOC", ISDN_STAT_CINF, 6}, /* Charge-info, DSS1-type */
{"CAU", ISDN_STAT_CAUSE, 7}, /* Cause code */
{"TEI OK", ISDN_STAT_RUN, 0}, /* Card connected to wallplug */
{"E_L1: ACT FAIL", ISDN_STAT_BHUP, 8}, /* Layer-1 activation failed */
{"E_L2: DATA LIN", ISDN_STAT_BHUP, 8}, /* Layer-2 data link lost */
{"E_L1: ACTIVATION FAILED",
ISDN_STAT_BHUP, 8}, /* Layer-1 activation failed */
{NULL, 0, -1}
};
/* *INDENT-ON* */
/*
* Parse Status message-strings from virtual card.
* Depending on status, call statcallb for sending messages to upper
* levels. Also set/reset B-Channel active-flags.
*
* Parameter:
* status = status string to parse.
* channel = channel where message comes from.
* card = card where message comes from.
*/
static void
isdnloop_parse_status(u_char * status, int channel, isdnloop_card * card)
{
isdnloop_stat *s = isdnloop_stat_table;
int action = -1;
isdn_ctrl cmd;
while (s->statstr) {
if (!strncmp(status, s->statstr, strlen(s->statstr))) {
cmd.command = s->command;
action = s->action;
break;
}
s++;
}
if (action == -1)
return;
cmd.driver = card->myid;
cmd.arg = channel;
switch (action) {
case 1:
/* BCON_x */
card->flags |= (channel) ?
ISDNLOOP_FLAGS_B2ACTIVE : ISDNLOOP_FLAGS_B1ACTIVE;
break;
case 2:
/* BDIS_x */
card->flags &= ~((channel) ?
ISDNLOOP_FLAGS_B2ACTIVE : ISDNLOOP_FLAGS_B1ACTIVE);
isdnloop_free_queue(card, channel);
break;
case 3:
/* DCAL_I and DSCA_I */
isdnloop_parse_setup(status + 6, &cmd);
break;
case 4:
/* FCALL */
sprintf(cmd.parm.setup.phone, "LEASED%d", card->myid);
sprintf(cmd.parm.setup.eazmsn, "%d", channel + 1);
cmd.parm.setup.si1 = 7;
cmd.parm.setup.si2 = 0;
cmd.parm.setup.plan = 0;
cmd.parm.setup.screen = 0;
break;
case 5:
/* CIF */
strlcpy(cmd.parm.num, status + 3, sizeof(cmd.parm.num));
break;
case 6:
/* AOC */
snprintf(cmd.parm.num, sizeof(cmd.parm.num), "%d",
(int) simple_strtoul(status + 7, NULL, 16));
break;
case 7:
/* CAU */
status += 3;
if (strlen(status) == 4)
snprintf(cmd.parm.num, sizeof(cmd.parm.num), "%s%c%c",
status + 2, *status, *(status + 1));
else
strlcpy(cmd.parm.num, status + 1, sizeof(cmd.parm.num));
break;
case 8:
/* Misc Errors on L1 and L2 */
card->flags &= ~ISDNLOOP_FLAGS_B1ACTIVE;
isdnloop_free_queue(card, 0);
cmd.arg = 0;
cmd.driver = card->myid;
card->interface.statcallb(&cmd);
cmd.command = ISDN_STAT_DHUP;
cmd.arg = 0;
cmd.driver = card->myid;
card->interface.statcallb(&cmd);
cmd.command = ISDN_STAT_BHUP;
card->flags &= ~ISDNLOOP_FLAGS_B2ACTIVE;
isdnloop_free_queue(card, 1);
cmd.arg = 1;
cmd.driver = card->myid;
card->interface.statcallb(&cmd);
cmd.command = ISDN_STAT_DHUP;
cmd.arg = 1;
cmd.driver = card->myid;
break;
}
card->interface.statcallb(&cmd);
}
/*
* Store a cwcharacter into ringbuffer for reading from /dev/isdnctrl
*
* Parameter:
* card = pointer to card struct.
* c = char to store.
*/
static void
isdnloop_putmsg(isdnloop_card * card, unsigned char c)
{
ulong flags;
spin_lock_irqsave(&card->isdnloop_lock, flags);
*card->msg_buf_write++ = (c == 0xff) ? '\n' : c;
if (card->msg_buf_write == card->msg_buf_read) {
if (++card->msg_buf_read > card->msg_buf_end)
card->msg_buf_read = card->msg_buf;
}
if (card->msg_buf_write > card->msg_buf_end)
card->msg_buf_write = card->msg_buf;
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
/*
* Poll a virtual cards message queue.
* If there are new status-replies from the card, copy them to
* ringbuffer for reading on /dev/isdnctrl and call
* isdnloop_parse_status() for processing them. Watch for special
* Firmware bootmessage and parse it, to get the D-Channel protocol.
* If there are B-Channels open, initiate a timer-callback to
* isdnloop_pollbchan().
* This routine is called periodically via timer interrupt.
*
* Parameter:
* data = pointer to card struct
*/
static void
isdnloop_polldchan(unsigned long data)
{
isdnloop_card *card = (isdnloop_card *) data;
struct sk_buff *skb;
int avail;
int left;
u_char c;
int ch;
unsigned long flags;
u_char *p;
isdn_ctrl cmd;
if ((skb = skb_dequeue(&card->dqueue)))
avail = skb->len;
else
avail = 0;
for (left = avail; left > 0; left--) {
c = *skb->data;
skb_pull(skb, 1);
isdnloop_putmsg(card, c);
card->imsg[card->iptr] = c;
if (card->iptr < 59)
card->iptr++;
if (!skb->len) {
avail++;
isdnloop_putmsg(card, '\n');
card->imsg[card->iptr] = 0;
card->iptr = 0;
if (card->imsg[0] == '0' && card->imsg[1] >= '0' &&
card->imsg[1] <= '2' && card->imsg[2] == ';') {
ch = (card->imsg[1] - '0') - 1;
p = &card->imsg[3];
isdnloop_parse_status(p, ch, card);
} else {
p = card->imsg;
if (!strncmp(p, "DRV1.", 5)) {
printk(KERN_INFO "isdnloop: (%s) %s\n", CID, p);
if (!strncmp(p + 7, "TC", 2)) {
card->ptype = ISDN_PTYPE_1TR6;
card->interface.features |= ISDN_FEATURE_P_1TR6;
printk(KERN_INFO
"isdnloop: (%s) 1TR6-Protocol loaded and running\n", CID);
}
if (!strncmp(p + 7, "EC", 2)) {
card->ptype = ISDN_PTYPE_EURO;
card->interface.features |= ISDN_FEATURE_P_EURO;
printk(KERN_INFO
"isdnloop: (%s) Euro-Protocol loaded and running\n", CID);
}
continue;
}
}
}
}
if (avail) {
cmd.command = ISDN_STAT_STAVAIL;
cmd.driver = card->myid;
cmd.arg = avail;
card->interface.statcallb(&cmd);
}
if (card->flags & (ISDNLOOP_FLAGS_B1ACTIVE | ISDNLOOP_FLAGS_B2ACTIVE))
if (!(card->flags & ISDNLOOP_FLAGS_RBTIMER)) {
/* schedule b-channel polling */
card->flags |= ISDNLOOP_FLAGS_RBTIMER;
spin_lock_irqsave(&card->isdnloop_lock, flags);
del_timer(&card->rb_timer);
card->rb_timer.function = isdnloop_pollbchan;
card->rb_timer.data = (unsigned long) card;
card->rb_timer.expires = jiffies + ISDNLOOP_TIMER_BCREAD;
add_timer(&card->rb_timer);
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
/* schedule again */
spin_lock_irqsave(&card->isdnloop_lock, flags);
card->st_timer.expires = jiffies + ISDNLOOP_TIMER_DCREAD;
add_timer(&card->st_timer);
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
/*
* Append a packet to the transmit buffer-queue.
*
* Parameter:
* channel = Number of B-channel
* skb = packet to send.
* card = pointer to card-struct
* Return:
* Number of bytes transferred, -E??? on error
*/
static int
isdnloop_sendbuf(int channel, struct sk_buff *skb, isdnloop_card * card)
{
int len = skb->len;
unsigned long flags;
struct sk_buff *nskb;
if (len > 4000) {
printk(KERN_WARNING
"isdnloop: Send packet too large\n");
return -EINVAL;
}
if (len) {
if (!(card->flags & (channel) ? ISDNLOOP_FLAGS_B2ACTIVE : ISDNLOOP_FLAGS_B1ACTIVE))
return 0;
if (card->sndcount[channel] > ISDNLOOP_MAX_SQUEUE)
return 0;
spin_lock_irqsave(&card->isdnloop_lock, flags);
nskb = dev_alloc_skb(skb->len);
if (nskb) {
skb_copy_from_linear_data(skb,
skb_put(nskb, len), len);
skb_queue_tail(&card->bqueue[channel], nskb);
dev_kfree_skb(skb);
} else
len = 0;
card->sndcount[channel] += len;
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
return len;
}
/*
* Read the messages from the card's ringbuffer
*
* Parameter:
* buf = pointer to buffer.
* len = number of bytes to read.
* user = flag, 1: called from userlevel 0: called from kernel.
* card = pointer to card struct.
* Return:
* number of bytes actually transferred.
*/
static int
isdnloop_readstatus(u_char __user *buf, int len, isdnloop_card * card)
{
int count;
u_char __user *p;
for (p = buf, count = 0; count < len; p++, count++) {
if (card->msg_buf_read == card->msg_buf_write)
return count;
if (put_user(*card->msg_buf_read++, p))
return -EFAULT;
if (card->msg_buf_read > card->msg_buf_end)
card->msg_buf_read = card->msg_buf;
}
return count;
}
/*
* Simulate a card's response by appending it to the cards
* message queue.
*
* Parameter:
* card = pointer to card struct.
* s = pointer to message-string.
* ch = channel: 0 = generic messages, 1 and 2 = D-channel messages.
* Return:
* 0 on success, 1 on memory squeeze.
*/
static int
isdnloop_fake(isdnloop_card * card, char *s, int ch)
{
struct sk_buff *skb;
int len = strlen(s) + ((ch >= 0) ? 3 : 0);
if (!(skb = dev_alloc_skb(len))) {
printk(KERN_WARNING "isdnloop: Out of memory in isdnloop_fake\n");
return 1;
}
if (ch >= 0)
sprintf(skb_put(skb, 3), "%02d;", ch);
memcpy(skb_put(skb, strlen(s)), s, strlen(s));
skb_queue_tail(&card->dqueue, skb);
return 0;
}
/* *INDENT-OFF* */
static isdnloop_stat isdnloop_cmd_table[] =
{
{"BCON_R", 0, 1}, /* B-Channel connect */
{"BCON_I", 0, 17}, /* B-Channel connect ind */
{"BDIS_R", 0, 2}, /* B-Channel disconnect */
{"DDIS_R", 0, 3}, /* D-Channel disconnect */
{"DCON_R", 0, 16}, /* D-Channel connect */
{"DSCA_R", 0, 4}, /* Dial 1TR6-SPV */
{"DCAL_R", 0, 5}, /* Dial */
{"EAZC", 0, 6}, /* Clear EAZ listener */
{"EAZ", 0, 7}, /* Set EAZ listener */
{"SEEAZ", 0, 8}, /* Get EAZ listener */
{"MSN", 0, 9}, /* Set/Clear MSN listener */
{"MSALL", 0, 10}, /* Set multi MSN listeners */
{"SETSIL", 0, 11}, /* Set SI list */
{"SEESIL", 0, 12}, /* Get SI list */
{"SILC", 0, 13}, /* Clear SI list */
{"LOCK", 0, -1}, /* LOCK channel */
{"UNLOCK", 0, -1}, /* UNLOCK channel */
{"FV2ON", 1, 14}, /* Leased mode on */
{"FV2OFF", 1, 15}, /* Leased mode off */
{NULL, 0, -1}
};
/* *INDENT-ON* */
/*
* Simulate an error-response from a card.
*
* Parameter:
* card = pointer to card struct.
*/
static void
isdnloop_fake_err(isdnloop_card * card)
{
char buf[60];
sprintf(buf, "E%s", card->omsg);
isdnloop_fake(card, buf, -1);
isdnloop_fake(card, "NAK", -1);
}
static u_char ctable_eu[] =
{0x00, 0x11, 0x01, 0x12};
static u_char ctable_1t[] =
{0x00, 0x3b, 0x01, 0x3a};
/*
* Assemble a simplified cause message depending on the
* D-channel protocol used.
*
* Parameter:
* card = pointer to card struct.
* loc = location: 0 = local, 1 = remote.
* cau = cause: 1 = busy, 2 = nonexistent callerid, 3 = no user responding.
* Return:
* Pointer to buffer containing the assembled message.
*/
static char *
isdnloop_unicause(isdnloop_card * card, int loc, int cau)
{
static char buf[6];
switch (card->ptype) {
case ISDN_PTYPE_EURO:
sprintf(buf, "E%02X%02X", (loc) ? 4 : 2, ctable_eu[cau]);
break;
case ISDN_PTYPE_1TR6:
sprintf(buf, "%02X44", ctable_1t[cau]);
break;
default:
return ("0000");
}
return (buf);
}
/*
* Release a virtual connection. Called from timer interrupt, when
* called party did not respond.
*
* Parameter:
* card = pointer to card struct.
* ch = channel (0-based)
*/
static void
isdnloop_atimeout(isdnloop_card * card, int ch)
{
unsigned long flags;
char buf[60];
spin_lock_irqsave(&card->isdnloop_lock, flags);
if (card->rcard) {
isdnloop_fake(card->rcard[ch], "DDIS_I", card->rch[ch] + 1);
card->rcard[ch]->rcard[card->rch[ch]] = NULL;
card->rcard[ch] = NULL;
}
isdnloop_fake(card, "DDIS_I", ch + 1);
/* No user responding */
sprintf(buf, "CAU%s", isdnloop_unicause(card, 1, 3));
isdnloop_fake(card, buf, ch + 1);
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
/*
* Wrapper for isdnloop_atimeout().
*/
static void
isdnloop_atimeout0(unsigned long data)
{
isdnloop_card *card = (isdnloop_card *) data;
isdnloop_atimeout(card, 0);
}
/*
* Wrapper for isdnloop_atimeout().
*/
static void
isdnloop_atimeout1(unsigned long data)
{
isdnloop_card *card = (isdnloop_card *) data;
isdnloop_atimeout(card, 1);
}
/*
* Install a watchdog for a user, not responding.
*
* Parameter:
* card = pointer to card struct.
* ch = channel to watch for.
*/
static void
isdnloop_start_ctimer(isdnloop_card * card, int ch)
{
unsigned long flags;
spin_lock_irqsave(&card->isdnloop_lock, flags);
init_timer(&card->c_timer[ch]);
card->c_timer[ch].expires = jiffies + ISDNLOOP_TIMER_ALERTWAIT;
if (ch)
card->c_timer[ch].function = isdnloop_atimeout1;
else
card->c_timer[ch].function = isdnloop_atimeout0;
card->c_timer[ch].data = (unsigned long) card;
add_timer(&card->c_timer[ch]);
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
/*
* Kill a pending channel watchdog.
*
* Parameter:
* card = pointer to card struct.
* ch = channel (0-based).
*/
static void
isdnloop_kill_ctimer(isdnloop_card * card, int ch)
{
unsigned long flags;
spin_lock_irqsave(&card->isdnloop_lock, flags);
del_timer(&card->c_timer[ch]);
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
static u_char si2bit[] =
{0, 1, 0, 0, 0, 2, 0, 4, 0, 0};
static u_char bit2si[] =
{1, 5, 7};
/*
* Try finding a listener for an outgoing call.
*
* Parameter:
* card = pointer to calling card.
* p = pointer to ICN-type setup-string.
* lch = channel of calling card.
* cmd = pointer to struct to be filled when parsing setup.
* Return:
* 0 = found match, alerting should happen.
* 1 = found matching number but it is busy.
* 2 = no matching listener.
* 3 = found matching number but SI does not match.
*/
static int
isdnloop_try_call(isdnloop_card * card, char *p, int lch, isdn_ctrl * cmd)
{
isdnloop_card *cc = cards;
unsigned long flags;
int ch;
int num_match;
int i;
char *e;
char nbuf[32];
isdnloop_parse_setup(p, cmd);
while (cc) {
for (ch = 0; ch < 2; ch++) {
/* Exclude ourself */
if ((cc == card) && (ch == lch))
continue;
num_match = 0;
switch (cc->ptype) {
case ISDN_PTYPE_EURO:
for (i = 0; i < 3; i++)
if (!(strcmp(cc->s0num[i], cmd->parm.setup.phone)))
num_match = 1;
break;
case ISDN_PTYPE_1TR6:
e = cc->eazlist[ch];
while (*e) {
sprintf(nbuf, "%s%c", cc->s0num[0], *e);
if (!(strcmp(nbuf, cmd->parm.setup.phone)))
num_match = 1;
e++;
}
}
if (num_match) {
spin_lock_irqsave(&card->isdnloop_lock, flags);
/* channel idle? */
if (!(cc->rcard[ch])) {
/* Check SI */
if (!(si2bit[cmd->parm.setup.si1] & cc->sil[ch])) {
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return 3;
}
/* ch is idle, si and number matches */
cc->rcard[ch] = card;
cc->rch[ch] = lch;
card->rcard[lch] = cc;
card->rch[lch] = ch;
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return 0;
} else {
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
/* num matches, but busy */
if (ch == 1)
return 1;
}
}
}
cc = cc->next;
}
return 2;
}
/*
* Depending on D-channel protocol and caller/called, modify
* phone number.
*
* Parameter:
* card = pointer to card struct.
* phone = pointer phone number.
* caller = flag: 1 = caller, 0 = called.
* Return:
* pointer to new phone number.
*/
static char *
isdnloop_vstphone(isdnloop_card * card, char *phone, int caller)
{
int i;
static char nphone[30];
if (!card) {
printk("BUG!!!\n");
return "";
}
switch (card->ptype) {
case ISDN_PTYPE_EURO:
if (caller) {
for (i = 0; i < 2; i++)
if (!(strcmp(card->s0num[i], phone)))
return (phone);
return (card->s0num[0]);
}
return (phone);
break;
case ISDN_PTYPE_1TR6:
if (caller) {
sprintf(nphone, "%s%c", card->s0num[0], phone[0]);
return (nphone);
} else
return (&phone[strlen(phone) - 1]);
break;
}
return "";
}
/*
* Parse an ICN-type command string sent to the 'card'.
* Perform misc. actions depending on the command.
*
* Parameter:
* card = pointer to card struct.
*/
static void
isdnloop_parse_cmd(isdnloop_card * card)
{
char *p = card->omsg;
isdn_ctrl cmd;
char buf[60];
isdnloop_stat *s = isdnloop_cmd_table;
int action = -1;
int i;
int ch;
if ((card->omsg[0] != '0') && (card->omsg[2] != ';')) {
isdnloop_fake_err(card);
return;
}
ch = card->omsg[1] - '0';
if ((ch < 0) || (ch > 2)) {
isdnloop_fake_err(card);
return;
}
p += 3;
while (s->statstr) {
if (!strncmp(p, s->statstr, strlen(s->statstr))) {
action = s->action;
if (s->command && (ch != 0)) {
isdnloop_fake_err(card);
return;
}
break;
}
s++;
}
if (action == -1)
return;
switch (action) {
case 1:
/* 0x;BCON_R */
if (card->rcard[ch - 1]) {
isdnloop_fake(card->rcard[ch - 1], "BCON_I",
card->rch[ch - 1] + 1);
isdnloop_fake(card, "BCON_C", ch);
}
break;
case 17:
/* 0x;BCON_I */
if (card->rcard[ch - 1]) {
isdnloop_fake(card->rcard[ch - 1], "BCON_C",
card->rch[ch - 1] + 1);
}
break;
case 2:
/* 0x;BDIS_R */
isdnloop_fake(card, "BDIS_C", ch);
if (card->rcard[ch - 1]) {
isdnloop_fake(card->rcard[ch - 1], "BDIS_I",
card->rch[ch - 1] + 1);
}
break;
case 16:
/* 0x;DCON_R */
isdnloop_kill_ctimer(card, ch - 1);
if (card->rcard[ch - 1]) {
isdnloop_kill_ctimer(card->rcard[ch - 1], card->rch[ch - 1]);
isdnloop_fake(card->rcard[ch - 1], "DCON_C",
card->rch[ch - 1] + 1);
isdnloop_fake(card, "DCON_C", ch);
}
break;
case 3:
/* 0x;DDIS_R */
isdnloop_kill_ctimer(card, ch - 1);
if (card->rcard[ch - 1]) {
isdnloop_kill_ctimer(card->rcard[ch - 1], card->rch[ch - 1]);
isdnloop_fake(card->rcard[ch - 1], "DDIS_I",
card->rch[ch - 1] + 1);
card->rcard[ch - 1] = NULL;
}
isdnloop_fake(card, "DDIS_C", ch);
break;
case 4:
/* 0x;DSCA_Rdd,yy,zz,oo */
if (card->ptype != ISDN_PTYPE_1TR6) {
isdnloop_fake_err(card);
return;
}
/* Fall through */
case 5:
/* 0x;DCAL_Rdd,yy,zz,oo */
p += 6;
switch (isdnloop_try_call(card, p, ch - 1, &cmd)) {
case 0:
/* Alerting */
sprintf(buf, "D%s_I%s,%02d,%02d,%s",
(action == 4) ? "SCA" : "CAL",
isdnloop_vstphone(card, cmd.parm.setup.eazmsn, 1),
cmd.parm.setup.si1,
cmd.parm.setup.si2,
isdnloop_vstphone(card->rcard[ch - 1],
cmd.parm.setup.phone, 0));
isdnloop_fake(card->rcard[ch - 1], buf, card->rch[ch - 1] + 1);
/* Fall through */
case 3:
/* si1 does not match, don't alert but start timer */
isdnloop_start_ctimer(card, ch - 1);
break;
case 1:
/* Remote busy */
isdnloop_fake(card, "DDIS_I", ch);
sprintf(buf, "CAU%s", isdnloop_unicause(card, 1, 1));
isdnloop_fake(card, buf, ch);
break;
case 2:
/* No such user */
isdnloop_fake(card, "DDIS_I", ch);
sprintf(buf, "CAU%s", isdnloop_unicause(card, 1, 2));
isdnloop_fake(card, buf, ch);
break;
}
break;
case 6:
/* 0x;EAZC */
card->eazlist[ch - 1][0] = '\0';
break;
case 7:
/* 0x;EAZ */
p += 3;
strcpy(card->eazlist[ch - 1], p);
break;
case 8:
/* 0x;SEEAZ */
sprintf(buf, "EAZ-LIST: %s", card->eazlist[ch - 1]);
isdnloop_fake(card, buf, ch + 1);
break;
case 9:
/* 0x;MSN */
break;
case 10:
/* 0x;MSNALL */
break;
case 11:
/* 0x;SETSIL */
p += 6;
i = 0;
while (strchr("0157", *p)) {
if (i)
card->sil[ch - 1] |= si2bit[*p - '0'];
i = (*p++ == '0');
}
if (*p)
isdnloop_fake_err(card);
break;
case 12:
/* 0x;SEESIL */
sprintf(buf, "SIN-LIST: ");
p = buf + 10;
for (i = 0; i < 3; i++)
if (card->sil[ch - 1] & (1 << i))
p += sprintf(p, "%02d", bit2si[i]);
isdnloop_fake(card, buf, ch + 1);
break;
case 13:
/* 0x;SILC */
card->sil[ch - 1] = 0;
break;
case 14:
/* 00;FV2ON */
break;
case 15:
/* 00;FV2OFF */
break;
}
}
/*
* Put command-strings into the of the 'card'. In reality, execute them
* right in place by calling isdnloop_parse_cmd(). Also copy every
* command to the read message ringbuffer, preceeding it with a '>'.
* These mesagges can be read at /dev/isdnctrl.
*
* Parameter:
* buf = pointer to command buffer.
* len = length of buffer data.
* user = flag: 1 = called form userlevel, 0 called from kernel.
* card = pointer to card struct.
* Return:
* number of bytes transferred (currently always equals len).
*/
static int
isdnloop_writecmd(const u_char * buf, int len, int user, isdnloop_card * card)
{
int xcount = 0;
int ocount = 1;
isdn_ctrl cmd;
while (len) {
int count = len;
u_char *p;
u_char msg[0x100];
if (count > 255)
count = 255;
if (user) {
if (copy_from_user(msg, buf, count))
return -EFAULT;
} else
memcpy(msg, buf, count);
isdnloop_putmsg(card, '>');
for (p = msg; count > 0; count--, p++) {
len--;
xcount++;
isdnloop_putmsg(card, *p);
card->omsg[card->optr] = *p;
if (*p == '\n') {
card->omsg[card->optr] = '\0';
card->optr = 0;
isdnloop_parse_cmd(card);
if (len) {
isdnloop_putmsg(card, '>');
ocount++;
}
} else {
if (card->optr < 59)
card->optr++;
}
ocount++;
}
}
cmd.command = ISDN_STAT_STAVAIL;
cmd.driver = card->myid;
cmd.arg = ocount;
card->interface.statcallb(&cmd);
return xcount;
}
/*
* Delete card's pending timers, send STOP to linklevel
*/
static void
isdnloop_stopcard(isdnloop_card * card)
{
unsigned long flags;
isdn_ctrl cmd;
spin_lock_irqsave(&card->isdnloop_lock, flags);
if (card->flags & ISDNLOOP_FLAGS_RUNNING) {
card->flags &= ~ISDNLOOP_FLAGS_RUNNING;
del_timer(&card->st_timer);
del_timer(&card->rb_timer);
del_timer(&card->c_timer[0]);
del_timer(&card->c_timer[1]);
cmd.command = ISDN_STAT_STOP;
cmd.driver = card->myid;
card->interface.statcallb(&cmd);
}
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
}
/*
* Stop all cards before unload.
*/
static void
isdnloop_stopallcards(void)
{
isdnloop_card *p = cards;
while (p) {
isdnloop_stopcard(p);
p = p->next;
}
}
/*
* Start a 'card'. Simulate card's boot message and set the phone
* number(s) of the virtual 'S0-Interface'. Install D-channel
* poll timer.
*
* Parameter:
* card = pointer to card struct.
* sdefp = pointer to struct holding ioctl parameters.
* Return:
* 0 on success, -E??? otherwise.
*/
static int
isdnloop_start(isdnloop_card * card, isdnloop_sdef * sdefp)
{
unsigned long flags;
isdnloop_sdef sdef;
int i;
if (card->flags & ISDNLOOP_FLAGS_RUNNING)
return -EBUSY;
if (copy_from_user((char *) &sdef, (char *) sdefp, sizeof(sdef)))
return -EFAULT;
spin_lock_irqsave(&card->isdnloop_lock, flags);
switch (sdef.ptype) {
case ISDN_PTYPE_EURO:
if (isdnloop_fake(card, "DRV1.23EC-Q.931-CAPI-CNS-BASIS-20.02.96",
-1)) {
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return -ENOMEM;
}
card->sil[0] = card->sil[1] = 4;
if (isdnloop_fake(card, "TEI OK", 0)) {
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return -ENOMEM;
}
for (i = 0; i < 3; i++)
strcpy(card->s0num[i], sdef.num[i]);
break;
case ISDN_PTYPE_1TR6:
if (isdnloop_fake(card, "DRV1.04TC-1TR6-CAPI-CNS-BASIS-29.11.95",
-1)) {
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return -ENOMEM;
}
card->sil[0] = card->sil[1] = 4;
if (isdnloop_fake(card, "TEI OK", 0)) {
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return -ENOMEM;
}
strcpy(card->s0num[0], sdef.num[0]);
card->s0num[1][0] = '\0';
card->s0num[2][0] = '\0';
break;
default:
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
printk(KERN_WARNING "isdnloop: Illegal D-channel protocol %d\n",
sdef.ptype);
return -EINVAL;
}
init_timer(&card->st_timer);
card->st_timer.expires = jiffies + ISDNLOOP_TIMER_DCREAD;
card->st_timer.function = isdnloop_polldchan;
card->st_timer.data = (unsigned long) card;
add_timer(&card->st_timer);
card->flags |= ISDNLOOP_FLAGS_RUNNING;
spin_unlock_irqrestore(&card->isdnloop_lock, flags);
return 0;
}
/*
* Main handler for commands sent by linklevel.
*/
static int
isdnloop_command(isdn_ctrl * c, isdnloop_card * card)
{
ulong a;
int i;
char cbuf[60];
isdn_ctrl cmd;
isdnloop_cdef cdef;
switch (c->command) {
case ISDN_CMD_IOCTL:
memcpy(&a, c->parm.num, sizeof(ulong));
switch (c->arg) {
case ISDNLOOP_IOCTL_DEBUGVAR:
return (ulong) card;
case ISDNLOOP_IOCTL_STARTUP:
if (!access_ok(VERIFY_READ, (void *) a, sizeof(isdnloop_sdef)))
return -EFAULT;
return (isdnloop_start(card, (isdnloop_sdef *) a));
break;
case ISDNLOOP_IOCTL_ADDCARD:
if (copy_from_user((char *)&cdef,
(char *)a,
sizeof(cdef)))
return -EFAULT;
return (isdnloop_addcard(cdef.id1));
break;
case ISDNLOOP_IOCTL_LEASEDCFG:
if (a) {
if (!card->leased) {
card->leased = 1;
while (card->ptype == ISDN_PTYPE_UNKNOWN)
schedule_timeout_interruptible(10);
schedule_timeout_interruptible(10);
sprintf(cbuf, "00;FV2ON\n01;EAZ1\n02;EAZ2\n");
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
printk(KERN_INFO
"isdnloop: (%s) Leased-line mode enabled\n",
CID);
cmd.command = ISDN_STAT_RUN;
cmd.driver = card->myid;
cmd.arg = 0;
card->interface.statcallb(&cmd);
}
} else {
if (card->leased) {
card->leased = 0;
sprintf(cbuf, "00;FV2OFF\n");
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
printk(KERN_INFO
"isdnloop: (%s) Leased-line mode disabled\n",
CID);
cmd.command = ISDN_STAT_RUN;
cmd.driver = card->myid;
cmd.arg = 0;
card->interface.statcallb(&cmd);
}
}
return 0;
default:
return -EINVAL;
}
break;
case ISDN_CMD_DIAL:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if (card->leased)
break;
if ((c->arg & 255) < ISDNLOOP_BCH) {
char *p;
char dial[50];
char dcode[4];
a = c->arg;
p = c->parm.setup.phone;
if (*p == 's' || *p == 'S') {
/* Dial for SPV */
p++;
strcpy(dcode, "SCA");
} else
/* Normal Dial */
strcpy(dcode, "CAL");
strcpy(dial, p);
sprintf(cbuf, "%02d;D%s_R%s,%02d,%02d,%s\n", (int) (a + 1),
dcode, dial, c->parm.setup.si1,
c->parm.setup.si2, c->parm.setup.eazmsn);
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
}
break;
case ISDN_CMD_ACCEPTD:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if (c->arg < ISDNLOOP_BCH) {
a = c->arg + 1;
cbuf[0] = 0;
switch (card->l2_proto[a - 1]) {
case ISDN_PROTO_L2_X75I:
sprintf(cbuf, "%02d;BX75\n", (int) a);
break;
#ifdef CONFIG_ISDN_X25
case ISDN_PROTO_L2_X25DTE:
sprintf(cbuf, "%02d;BX2T\n", (int) a);
break;
case ISDN_PROTO_L2_X25DCE:
sprintf(cbuf, "%02d;BX2C\n", (int) a);
break;
#endif
case ISDN_PROTO_L2_HDLC:
sprintf(cbuf, "%02d;BTRA\n", (int) a);
break;
}
if (strlen(cbuf))
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
sprintf(cbuf, "%02d;DCON_R\n", (int) a);
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
}
break;
case ISDN_CMD_ACCEPTB:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if (c->arg < ISDNLOOP_BCH) {
a = c->arg + 1;
switch (card->l2_proto[a - 1]) {
case ISDN_PROTO_L2_X75I:
sprintf(cbuf, "%02d;BCON_R,BX75\n", (int) a);
break;
#ifdef CONFIG_ISDN_X25
case ISDN_PROTO_L2_X25DTE:
sprintf(cbuf, "%02d;BCON_R,BX2T\n", (int) a);
break;
case ISDN_PROTO_L2_X25DCE:
sprintf(cbuf, "%02d;BCON_R,BX2C\n", (int) a);
break;
#endif
case ISDN_PROTO_L2_HDLC:
sprintf(cbuf, "%02d;BCON_R,BTRA\n", (int) a);
break;
default:
sprintf(cbuf, "%02d;BCON_R\n", (int) a);
}
printk(KERN_DEBUG "isdnloop writecmd '%s'\n", cbuf);
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
break;
case ISDN_CMD_HANGUP:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if (c->arg < ISDNLOOP_BCH) {
a = c->arg + 1;
sprintf(cbuf, "%02d;BDIS_R\n%02d;DDIS_R\n", (int) a, (int) a);
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
}
break;
case ISDN_CMD_SETEAZ:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if (card->leased)
break;
if (c->arg < ISDNLOOP_BCH) {
a = c->arg + 1;
if (card->ptype == ISDN_PTYPE_EURO) {
sprintf(cbuf, "%02d;MS%s%s\n", (int) a,
c->parm.num[0] ? "N" : "ALL", c->parm.num);
} else
sprintf(cbuf, "%02d;EAZ%s\n", (int) a,
c->parm.num[0] ? c->parm.num : (u_char *) "0123456789");
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
}
break;
case ISDN_CMD_CLREAZ:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if (card->leased)
break;
if (c->arg < ISDNLOOP_BCH) {
a = c->arg + 1;
if (card->ptype == ISDN_PTYPE_EURO)
sprintf(cbuf, "%02d;MSNC\n", (int) a);
else
sprintf(cbuf, "%02d;EAZC\n", (int) a);
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
}
break;
case ISDN_CMD_SETL2:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
if ((c->arg & 255) < ISDNLOOP_BCH) {
a = c->arg;
switch (a >> 8) {
case ISDN_PROTO_L2_X75I:
sprintf(cbuf, "%02d;BX75\n", (int) (a & 255) + 1);
break;
#ifdef CONFIG_ISDN_X25
case ISDN_PROTO_L2_X25DTE:
sprintf(cbuf, "%02d;BX2T\n", (int) (a & 255) + 1);
break;
case ISDN_PROTO_L2_X25DCE:
sprintf(cbuf, "%02d;BX2C\n", (int) (a & 255) + 1);
break;
#endif
case ISDN_PROTO_L2_HDLC:
sprintf(cbuf, "%02d;BTRA\n", (int) (a & 255) + 1);
break;
case ISDN_PROTO_L2_TRANS:
sprintf(cbuf, "%02d;BTRA\n", (int) (a & 255) + 1);
break;
default:
return -EINVAL;
}
i = isdnloop_writecmd(cbuf, strlen(cbuf), 0, card);
card->l2_proto[a & 255] = (a >> 8);
}
break;
case ISDN_CMD_SETL3:
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
return 0;
default:
return -EINVAL;
}
}
return 0;
}
/*
* Find card with given driverId
*/
static inline isdnloop_card *
isdnloop_findcard(int driverid)
{
isdnloop_card *p = cards;
while (p) {
if (p->myid == driverid)
return p;
p = p->next;
}
return (isdnloop_card *) 0;
}
/*
* Wrapper functions for interface to linklevel
*/
static int
if_command(isdn_ctrl * c)
{
isdnloop_card *card = isdnloop_findcard(c->driver);
if (card)
return (isdnloop_command(c, card));
printk(KERN_ERR
"isdnloop: if_command called with invalid driverId!\n");
return -ENODEV;
}
static int
if_writecmd(const u_char __user *buf, int len, int id, int channel)
{
isdnloop_card *card = isdnloop_findcard(id);
if (card) {
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
return (isdnloop_writecmd(buf, len, 1, card));
}
printk(KERN_ERR
"isdnloop: if_writecmd called with invalid driverId!\n");
return -ENODEV;
}
static int
if_readstatus(u_char __user *buf, int len, int id, int channel)
{
isdnloop_card *card = isdnloop_findcard(id);
if (card) {
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
return (isdnloop_readstatus(buf, len, card));
}
printk(KERN_ERR
"isdnloop: if_readstatus called with invalid driverId!\n");
return -ENODEV;
}
static int
if_sendbuf(int id, int channel, int ack, struct sk_buff *skb)
{
isdnloop_card *card = isdnloop_findcard(id);
if (card) {
if (!(card->flags & ISDNLOOP_FLAGS_RUNNING))
return -ENODEV;
/* ack request stored in skb scratch area */
*(skb->head) = ack;
return (isdnloop_sendbuf(channel, skb, card));
}
printk(KERN_ERR
"isdnloop: if_sendbuf called with invalid driverId!\n");
return -ENODEV;
}
/*
* Allocate a new card-struct, initialize it
* link it into cards-list and register it at linklevel.
*/
static isdnloop_card *
isdnloop_initcard(char *id)
{
isdnloop_card *card;
int i;
if (!(card = kzalloc(sizeof(isdnloop_card), GFP_KERNEL))) {
printk(KERN_WARNING
"isdnloop: (%s) Could not allocate card-struct.\n", id);
return (isdnloop_card *) 0;
}
card->interface.owner = THIS_MODULE;
card->interface.channels = ISDNLOOP_BCH;
card->interface.hl_hdrlen = 1; /* scratch area for storing ack flag*/
card->interface.maxbufsize = 4000;
card->interface.command = if_command;
card->interface.writebuf_skb = if_sendbuf;
card->interface.writecmd = if_writecmd;
card->interface.readstat = if_readstatus;
card->interface.features = ISDN_FEATURE_L2_X75I |
#ifdef CONFIG_ISDN_X25
ISDN_FEATURE_L2_X25DTE |
ISDN_FEATURE_L2_X25DCE |
#endif
ISDN_FEATURE_L2_HDLC |
ISDN_FEATURE_L3_TRANS |
ISDN_FEATURE_P_UNKNOWN;
card->ptype = ISDN_PTYPE_UNKNOWN;
strlcpy(card->interface.id, id, sizeof(card->interface.id));
card->msg_buf_write = card->msg_buf;
card->msg_buf_read = card->msg_buf;
card->msg_buf_end = &card->msg_buf[sizeof(card->msg_buf) - 1];
for (i = 0; i < ISDNLOOP_BCH; i++) {
card->l2_proto[i] = ISDN_PROTO_L2_X75I;
skb_queue_head_init(&card->bqueue[i]);
}
skb_queue_head_init(&card->dqueue);
spin_lock_init(&card->isdnloop_lock);
card->next = cards;
cards = card;
if (!register_isdn(&card->interface)) {
cards = cards->next;
printk(KERN_WARNING
"isdnloop: Unable to register %s\n", id);
kfree(card);
return (isdnloop_card *) 0;
}
card->myid = card->interface.channels;
return card;
}
static int
isdnloop_addcard(char *id1)
{
isdnloop_card *card;
if (!(card = isdnloop_initcard(id1))) {
return -EIO;
}
printk(KERN_INFO
"isdnloop: (%s) virtual card added\n",
card->interface.id);
return 0;
}
static int __init
isdnloop_init(void)
{
char *p;
char rev[10];
if ((p = strchr(revision, ':'))) {
strcpy(rev, p + 1);
p = strchr(rev, '$');
*p = 0;
} else
strcpy(rev, " ??? ");
printk(KERN_NOTICE "isdnloop-ISDN-driver Rev%s\n", rev);
if (isdnloop_id)
return (isdnloop_addcard(isdnloop_id));
return 0;
}
static void __exit
isdnloop_exit(void)
{
isdn_ctrl cmd;
isdnloop_card *card = cards;
isdnloop_card *last;
int i;
isdnloop_stopallcards();
while (card) {
cmd.command = ISDN_STAT_UNLOAD;
cmd.driver = card->myid;
card->interface.statcallb(&cmd);
for (i = 0; i < ISDNLOOP_BCH; i++)
isdnloop_free_queue(card, i);
card = card->next;
}
card = cards;
while (card) {
last = card;
skb_queue_purge(&card->dqueue);
card = card->next;
kfree(last);
}
printk(KERN_NOTICE "isdnloop-ISDN-driver unloaded\n");
}
module_init(isdnloop_init);
module_exit(isdnloop_exit);
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...