33f0f88f1c
The API and code have been through various bits of initial review by serial driver people but they definitely need to live somewhere for a while so the unconverted drivers can get knocked into shape, existing drivers that have been updated can be better tuned and bugs whacked out. This replaces the tty flip buffers with kmalloc objects in rings. In the normal situation for an IRQ driven serial port at typical speeds the behaviour is pretty much the same, two buffers end up allocated and the kernel cycles between them as before. When there are delays or at high speed we now behave far better as the buffer pool can grow a bit rather than lose characters. This also means that we can operate at higher speeds reliably. For drivers that receive characters in blocks (DMA based, USB and especially virtualisation) the layer allows a lot of driver specific code that works around the tty layer with private secondary queues to be removed. The IBM folks need this sort of layer, the smart serial port people do, the virtualisers do (because a virtualised tty typically operates at infinite speed rather than emulating 9600 baud). Finally many drivers had invalid and unsafe attempts to avoid buffer overflows by directly invoking tty methods extracted out of the innards of work queue structs. These are no longer needed and all go away. That fixes various random hangs with serial ports on overflow. The other change in here is to optimise the receive_room path that is used by some callers. It turns out that only one ldisc uses receive room except asa constant and it updates it far far less than the value is read. We thus make it a variable not a function call. I expect the code to contain bugs due to the size alone but I'll be watching and squashing them and feeding out new patches as it goes. Because the buffers now dynamically expand you should only run out of buffering when the kernel runs out of memory for real. That means a lot of the horrible hacks high performance drivers used to do just aren't needed any more. Description: tty_insert_flip_char is an old API and continues to work as before, as does tty_flip_buffer_push() [this is why many drivers dont need modification]. It does now also return the number of chars inserted There are also tty_buffer_request_room(tty, len) which asks for a buffer block of the length requested and returns the space found. This improves efficiency with hardware that knows how much to transfer. and tty_insert_flip_string_flags(tty, str, flags, len) to insert a string of characters and flags For a smart interface the usual code is len = tty_request_buffer_room(tty, amount_hardware_says); tty_insert_flip_string(tty, buffer_from_card, len); More description! At the moment tty buffers are attached directly to the tty. This is causing a lot of the problems related to tty layer locking, also problems at high speed and also with bursty data (such as occurs in virtualised environments) I'm working on ripping out the flip buffers and replacing them with a pool of dynamically allocated buffers. This allows both for old style "byte I/O" devices and also helps virtualisation and smart devices where large blocks of data suddenely materialise and need storing. So far so good. Lots of drivers reference tty->flip.*. Several of them also call directly and unsafely into function pointers it provides. This will all break. Most drivers can use tty_insert_flip_char which can be kept as an API but others need more. At the moment I've added the following interfaces, if people think more will be needed now is a good time to say int tty_buffer_request_room(tty, size) Try and ensure at least size bytes are available, returns actual room (may be zero). At the moment it just uses the flipbuf space but that will change. Repeated calls without characters being added are not cumulative. (ie if you call it with 1, 1, 1, and then 4 you'll have four characters of space. The other functions will also try and grow buffers in future but this will be a more efficient way when you know block sizes. int tty_insert_flip_char(tty, ch, flag) As before insert a character if there is room. Now returns 1 for success, 0 for failure. int tty_insert_flip_string(tty, str, len) Insert a block of non error characters. Returns the number inserted. int tty_prepare_flip_string(tty, strptr, len) Adjust the buffer to allow len characters to be added. Returns a buffer pointer in strptr and the length available. This allows for hardware that needs to use functions like insl or mencpy_fromio. Signed-off-by: Alan Cox <alan@redhat.com> Cc: Paul Fulghum <paulkf@microgate.com> Signed-off-by: Hirokazu Takata <takata@linux-m32r.org> Signed-off-by: Serge Hallyn <serue@us.ibm.com> Signed-off-by: Jeff Dike <jdike@addtoit.com> Signed-off-by: John Hawkes <hawkes@sgi.com> Signed-off-by: Martin Schwidefsky <schwidefsky@de.ibm.com> Signed-off-by: Adrian Bunk <bunk@stusta.de> Signed-off-by: Andrew Morton <akpm@osdl.org> Signed-off-by: Linus Torvalds <torvalds@osdl.org>
2365 lines
57 KiB
C
2365 lines
57 KiB
C
/* $Id: isdn_common.c,v 1.1.2.3 2004/02/10 01:07:13 keil Exp $
|
|
*
|
|
* Linux ISDN subsystem, common used functions (linklevel).
|
|
*
|
|
* Copyright 1994-1999 by Fritz Elfert (fritz@isdn4linux.de)
|
|
* Copyright 1995,96 Thinking Objects Software GmbH Wuerzburg
|
|
* Copyright 1995,96 by Michael Hipp (Michael.Hipp@student.uni-tuebingen.de)
|
|
*
|
|
* This software may be used and distributed according to the terms
|
|
* of the GNU General Public License, incorporated herein by reference.
|
|
*
|
|
*/
|
|
|
|
#include <linux/config.h>
|
|
#include <linux/module.h>
|
|
#include <linux/init.h>
|
|
#include <linux/poll.h>
|
|
#include <linux/vmalloc.h>
|
|
#include <linux/isdn.h>
|
|
#include <linux/smp_lock.h>
|
|
#include "isdn_common.h"
|
|
#include "isdn_tty.h"
|
|
#include "isdn_net.h"
|
|
#include "isdn_ppp.h"
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
#include "isdn_audio.h"
|
|
#endif
|
|
#ifdef CONFIG_ISDN_DIVERSION_MODULE
|
|
#define CONFIG_ISDN_DIVERSION
|
|
#endif
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
#include <linux/isdn_divertif.h>
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
#include "isdn_v110.h"
|
|
|
|
/* Debugflags */
|
|
#undef ISDN_DEBUG_STATCALLB
|
|
|
|
MODULE_DESCRIPTION("ISDN4Linux: link layer");
|
|
MODULE_AUTHOR("Fritz Elfert");
|
|
MODULE_LICENSE("GPL");
|
|
|
|
isdn_dev *dev;
|
|
|
|
static char *isdn_revision = "$Revision: 1.1.2.3 $";
|
|
|
|
extern char *isdn_net_revision;
|
|
extern char *isdn_tty_revision;
|
|
#ifdef CONFIG_ISDN_PPP
|
|
extern char *isdn_ppp_revision;
|
|
#else
|
|
static char *isdn_ppp_revision = ": none $";
|
|
#endif
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
extern char *isdn_audio_revision;
|
|
#else
|
|
static char *isdn_audio_revision = ": none $";
|
|
#endif
|
|
extern char *isdn_v110_revision;
|
|
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
static isdn_divert_if *divert_if; /* = NULL */
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
|
|
|
|
static int isdn_writebuf_stub(int, int, const u_char __user *, int);
|
|
static void set_global_features(void);
|
|
static int isdn_wildmat(char *s, char *p);
|
|
static int isdn_add_channels(isdn_driver_t *d, int drvidx, int n, int adding);
|
|
|
|
static inline void
|
|
isdn_lock_driver(isdn_driver_t *drv)
|
|
{
|
|
try_module_get(drv->interface->owner);
|
|
drv->locks++;
|
|
}
|
|
|
|
void
|
|
isdn_lock_drivers(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++) {
|
|
if (!dev->drv[i])
|
|
continue;
|
|
isdn_lock_driver(dev->drv[i]);
|
|
}
|
|
}
|
|
|
|
static inline void
|
|
isdn_unlock_driver(isdn_driver_t *drv)
|
|
{
|
|
if (drv->locks > 0) {
|
|
drv->locks--;
|
|
module_put(drv->interface->owner);
|
|
}
|
|
}
|
|
|
|
void
|
|
isdn_unlock_drivers(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++) {
|
|
if (!dev->drv[i])
|
|
continue;
|
|
isdn_unlock_driver(dev->drv[i]);
|
|
}
|
|
}
|
|
|
|
#if defined(ISDN_DEBUG_NET_DUMP) || defined(ISDN_DEBUG_MODEM_DUMP)
|
|
void
|
|
isdn_dumppkt(char *s, u_char * p, int len, int dumplen)
|
|
{
|
|
int dumpc;
|
|
|
|
printk(KERN_DEBUG "%s(%d) ", s, len);
|
|
for (dumpc = 0; (dumpc < dumplen) && (len); len--, dumpc++)
|
|
printk(" %02x", *p++);
|
|
printk("\n");
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* I picked the pattern-matching-functions from an old GNU-tar version (1.10)
|
|
* It was originally written and put to PD by rs@mirror.TMC.COM (Rich Salz)
|
|
*/
|
|
static int
|
|
isdn_star(char *s, char *p)
|
|
{
|
|
while (isdn_wildmat(s, p)) {
|
|
if (*++s == '\0')
|
|
return (2);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
/*
|
|
* Shell-type Pattern-matching for incoming caller-Ids
|
|
* This function gets a string in s and checks, if it matches the pattern
|
|
* given in p.
|
|
*
|
|
* Return:
|
|
* 0 = match.
|
|
* 1 = no match.
|
|
* 2 = no match. Would eventually match, if s would be longer.
|
|
*
|
|
* Possible Patterns:
|
|
*
|
|
* '?' matches one character
|
|
* '*' matches zero or more characters
|
|
* [xyz] matches the set of characters in brackets.
|
|
* [^xyz] matches any single character not in the set of characters
|
|
*/
|
|
|
|
static int
|
|
isdn_wildmat(char *s, char *p)
|
|
{
|
|
register int last;
|
|
register int matched;
|
|
register int reverse;
|
|
register int nostar = 1;
|
|
|
|
if (!(*s) && !(*p))
|
|
return(1);
|
|
for (; *p; s++, p++)
|
|
switch (*p) {
|
|
case '\\':
|
|
/*
|
|
* Literal match with following character,
|
|
* fall through.
|
|
*/
|
|
p++;
|
|
default:
|
|
if (*s != *p)
|
|
return (*s == '\0')?2:1;
|
|
continue;
|
|
case '?':
|
|
/* Match anything. */
|
|
if (*s == '\0')
|
|
return (2);
|
|
continue;
|
|
case '*':
|
|
nostar = 0;
|
|
/* Trailing star matches everything. */
|
|
return (*++p ? isdn_star(s, p) : 0);
|
|
case '[':
|
|
/* [^....] means inverse character class. */
|
|
if ((reverse = (p[1] == '^')))
|
|
p++;
|
|
for (last = 0, matched = 0; *++p && (*p != ']'); last = *p)
|
|
/* This next line requires a good C compiler. */
|
|
if (*p == '-' ? *s <= *++p && *s >= last : *s == *p)
|
|
matched = 1;
|
|
if (matched == reverse)
|
|
return (1);
|
|
continue;
|
|
}
|
|
return (*s == '\0')?0:nostar;
|
|
}
|
|
|
|
int isdn_msncmp( const char * msn1, const char * msn2 )
|
|
{
|
|
char TmpMsn1[ ISDN_MSNLEN ];
|
|
char TmpMsn2[ ISDN_MSNLEN ];
|
|
char *p;
|
|
|
|
for ( p = TmpMsn1; *msn1 && *msn1 != ':'; ) // Strip off a SPID
|
|
*p++ = *msn1++;
|
|
*p = '\0';
|
|
|
|
for ( p = TmpMsn2; *msn2 && *msn2 != ':'; ) // Strip off a SPID
|
|
*p++ = *msn2++;
|
|
*p = '\0';
|
|
|
|
return isdn_wildmat( TmpMsn1, TmpMsn2 );
|
|
}
|
|
|
|
int
|
|
isdn_dc2minor(int di, int ch)
|
|
{
|
|
int i;
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if (dev->chanmap[i] == ch && dev->drvmap[i] == di)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
static int isdn_timer_cnt1 = 0;
|
|
static int isdn_timer_cnt2 = 0;
|
|
static int isdn_timer_cnt3 = 0;
|
|
|
|
static void
|
|
isdn_timer_funct(ulong dummy)
|
|
{
|
|
int tf = dev->tflags;
|
|
if (tf & ISDN_TIMER_FAST) {
|
|
if (tf & ISDN_TIMER_MODEMREAD)
|
|
isdn_tty_readmodem();
|
|
if (tf & ISDN_TIMER_MODEMPLUS)
|
|
isdn_tty_modem_escape();
|
|
if (tf & ISDN_TIMER_MODEMXMIT)
|
|
isdn_tty_modem_xmit();
|
|
}
|
|
if (tf & ISDN_TIMER_SLOW) {
|
|
if (++isdn_timer_cnt1 >= ISDN_TIMER_02SEC) {
|
|
isdn_timer_cnt1 = 0;
|
|
if (tf & ISDN_TIMER_NETDIAL)
|
|
isdn_net_dial();
|
|
}
|
|
if (++isdn_timer_cnt2 >= ISDN_TIMER_1SEC) {
|
|
isdn_timer_cnt2 = 0;
|
|
if (tf & ISDN_TIMER_NETHANGUP)
|
|
isdn_net_autohup();
|
|
if (++isdn_timer_cnt3 >= ISDN_TIMER_RINGING) {
|
|
isdn_timer_cnt3 = 0;
|
|
if (tf & ISDN_TIMER_MODEMRING)
|
|
isdn_tty_modem_ring();
|
|
}
|
|
if (tf & ISDN_TIMER_CARRIER)
|
|
isdn_tty_carrier_timeout();
|
|
}
|
|
}
|
|
if (tf)
|
|
mod_timer(&dev->timer, jiffies+ISDN_TIMER_RES);
|
|
}
|
|
|
|
void
|
|
isdn_timer_ctrl(int tf, int onoff)
|
|
{
|
|
unsigned long flags;
|
|
int old_tflags;
|
|
|
|
spin_lock_irqsave(&dev->timerlock, flags);
|
|
if ((tf & ISDN_TIMER_SLOW) && (!(dev->tflags & ISDN_TIMER_SLOW))) {
|
|
/* If the slow-timer wasn't activated until now */
|
|
isdn_timer_cnt1 = 0;
|
|
isdn_timer_cnt2 = 0;
|
|
}
|
|
old_tflags = dev->tflags;
|
|
if (onoff)
|
|
dev->tflags |= tf;
|
|
else
|
|
dev->tflags &= ~tf;
|
|
if (dev->tflags && !old_tflags)
|
|
mod_timer(&dev->timer, jiffies+ISDN_TIMER_RES);
|
|
spin_unlock_irqrestore(&dev->timerlock, flags);
|
|
}
|
|
|
|
/*
|
|
* Receive a packet from B-Channel. (Called from low-level-module)
|
|
*/
|
|
static void
|
|
isdn_receive_skb_callback(int di, int channel, struct sk_buff *skb)
|
|
{
|
|
int i;
|
|
|
|
if ((i = isdn_dc2minor(di, channel)) == -1) {
|
|
dev_kfree_skb(skb);
|
|
return;
|
|
}
|
|
/* Update statistics */
|
|
dev->ibytes[i] += skb->len;
|
|
|
|
/* First, try to deliver data to network-device */
|
|
if (isdn_net_rcv_skb(i, skb))
|
|
return;
|
|
|
|
/* V.110 handling
|
|
* makes sense for async streams only, so it is
|
|
* called after possible net-device delivery.
|
|
*/
|
|
if (dev->v110[i]) {
|
|
atomic_inc(&dev->v110use[i]);
|
|
skb = isdn_v110_decode(dev->v110[i], skb);
|
|
atomic_dec(&dev->v110use[i]);
|
|
if (!skb)
|
|
return;
|
|
}
|
|
|
|
/* No network-device found, deliver to tty or raw-channel */
|
|
if (skb->len) {
|
|
if (isdn_tty_rcv_skb(i, di, channel, skb))
|
|
return;
|
|
wake_up_interruptible(&dev->drv[di]->rcv_waitq[channel]);
|
|
} else
|
|
dev_kfree_skb(skb);
|
|
}
|
|
|
|
/*
|
|
* Intercept command from Linklevel to Lowlevel.
|
|
* If layer 2 protocol is V.110 and this is not supported by current
|
|
* lowlevel-driver, use driver's transparent mode and handle V.110 in
|
|
* linklevel instead.
|
|
*/
|
|
int
|
|
isdn_command(isdn_ctrl *cmd)
|
|
{
|
|
if (cmd->driver == -1) {
|
|
printk(KERN_WARNING "isdn_command command(%x) driver -1\n", cmd->command);
|
|
return(1);
|
|
}
|
|
if (cmd->command == ISDN_CMD_SETL2) {
|
|
int idx = isdn_dc2minor(cmd->driver, cmd->arg & 255);
|
|
unsigned long l2prot = (cmd->arg >> 8) & 255;
|
|
unsigned long features = (dev->drv[cmd->driver]->interface->features
|
|
>> ISDN_FEATURE_L2_SHIFT) &
|
|
ISDN_FEATURE_L2_MASK;
|
|
unsigned long l2_feature = (1 << l2prot);
|
|
|
|
switch (l2prot) {
|
|
case ISDN_PROTO_L2_V11096:
|
|
case ISDN_PROTO_L2_V11019:
|
|
case ISDN_PROTO_L2_V11038:
|
|
/* If V.110 requested, but not supported by
|
|
* HL-driver, set emulator-flag and change
|
|
* Layer-2 to transparent
|
|
*/
|
|
if (!(features & l2_feature)) {
|
|
dev->v110emu[idx] = l2prot;
|
|
cmd->arg = (cmd->arg & 255) |
|
|
(ISDN_PROTO_L2_TRANS << 8);
|
|
} else
|
|
dev->v110emu[idx] = 0;
|
|
}
|
|
}
|
|
return dev->drv[cmd->driver]->interface->command(cmd);
|
|
}
|
|
|
|
void
|
|
isdn_all_eaz(int di, int ch)
|
|
{
|
|
isdn_ctrl cmd;
|
|
|
|
if (di < 0)
|
|
return;
|
|
cmd.driver = di;
|
|
cmd.arg = ch;
|
|
cmd.command = ISDN_CMD_SETEAZ;
|
|
cmd.parm.num[0] = '\0';
|
|
isdn_command(&cmd);
|
|
}
|
|
|
|
/*
|
|
* Begin of a CAPI like LL<->HL interface, currently used only for
|
|
* supplementary service (CAPI 2.0 part III)
|
|
*/
|
|
#include <linux/isdn/capicmd.h>
|
|
|
|
static int
|
|
isdn_capi_rec_hl_msg(capi_msg *cm) {
|
|
|
|
int di;
|
|
int ch;
|
|
|
|
di = (cm->adr.Controller & 0x7f) -1;
|
|
ch = isdn_dc2minor(di, (cm->adr.Controller>>8)& 0x7f);
|
|
switch(cm->Command) {
|
|
case CAPI_FACILITY:
|
|
/* in the moment only handled in tty */
|
|
return(isdn_tty_capi_facility(cm));
|
|
default:
|
|
return(-1);
|
|
}
|
|
}
|
|
|
|
static int
|
|
isdn_status_callback(isdn_ctrl * c)
|
|
{
|
|
int di;
|
|
u_long flags;
|
|
int i;
|
|
int r;
|
|
int retval = 0;
|
|
isdn_ctrl cmd;
|
|
isdn_net_dev *p;
|
|
|
|
di = c->driver;
|
|
i = isdn_dc2minor(di, c->arg);
|
|
switch (c->command) {
|
|
case ISDN_STAT_BSENT:
|
|
if (i < 0)
|
|
return -1;
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
if (isdn_net_stat_callback(i, c))
|
|
return 0;
|
|
if (isdn_v110_stat_callback(i, c))
|
|
return 0;
|
|
if (isdn_tty_stat_callback(i, c))
|
|
return 0;
|
|
wake_up_interruptible(&dev->drv[di]->snd_waitq[c->arg]);
|
|
break;
|
|
case ISDN_STAT_STAVAIL:
|
|
dev->drv[di]->stavail += c->arg;
|
|
wake_up_interruptible(&dev->drv[di]->st_waitq);
|
|
break;
|
|
case ISDN_STAT_RUN:
|
|
dev->drv[di]->flags |= DRV_FLAG_RUNNING;
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if (dev->drvmap[i] == di)
|
|
isdn_all_eaz(di, dev->chanmap[i]);
|
|
set_global_features();
|
|
break;
|
|
case ISDN_STAT_STOP:
|
|
dev->drv[di]->flags &= ~DRV_FLAG_RUNNING;
|
|
break;
|
|
case ISDN_STAT_ICALL:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "ICALL (net): %d %ld %s\n", di, c->arg, c->parm.num);
|
|
#endif
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED) {
|
|
cmd.driver = di;
|
|
cmd.arg = c->arg;
|
|
cmd.command = ISDN_CMD_HANGUP;
|
|
isdn_command(&cmd);
|
|
return 0;
|
|
}
|
|
/* Try to find a network-interface which will accept incoming call */
|
|
r = ((c->command == ISDN_STAT_ICALLW) ? 0 : isdn_net_find_icall(di, c->arg, i, &c->parm.setup));
|
|
switch (r) {
|
|
case 0:
|
|
/* No network-device replies.
|
|
* Try ttyI's.
|
|
* These return 0 on no match, 1 on match and
|
|
* 3 on eventually match, if CID is longer.
|
|
*/
|
|
if (c->command == ISDN_STAT_ICALL)
|
|
if ((retval = isdn_tty_find_icall(di, c->arg, &c->parm.setup))) return(retval);
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
if (divert_if)
|
|
if ((retval = divert_if->stat_callback(c)))
|
|
return(retval); /* processed */
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
if ((!retval) && (dev->drv[di]->flags & DRV_FLAG_REJBUS)) {
|
|
/* No tty responding */
|
|
cmd.driver = di;
|
|
cmd.arg = c->arg;
|
|
cmd.command = ISDN_CMD_HANGUP;
|
|
isdn_command(&cmd);
|
|
retval = 2;
|
|
}
|
|
break;
|
|
case 1:
|
|
/* Schedule connection-setup */
|
|
isdn_net_dial();
|
|
cmd.driver = di;
|
|
cmd.arg = c->arg;
|
|
cmd.command = ISDN_CMD_ACCEPTD;
|
|
for ( p = dev->netdev; p; p = p->next )
|
|
if ( p->local->isdn_channel == cmd.arg )
|
|
{
|
|
strcpy( cmd.parm.setup.eazmsn, p->local->msn );
|
|
isdn_command(&cmd);
|
|
retval = 1;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 2: /* For calling back, first reject incoming call ... */
|
|
case 3: /* Interface found, but down, reject call actively */
|
|
retval = 2;
|
|
printk(KERN_INFO "isdn: Rejecting Call\n");
|
|
cmd.driver = di;
|
|
cmd.arg = c->arg;
|
|
cmd.command = ISDN_CMD_HANGUP;
|
|
isdn_command(&cmd);
|
|
if (r == 3)
|
|
break;
|
|
/* Fall through */
|
|
case 4:
|
|
/* ... then start callback. */
|
|
isdn_net_dial();
|
|
break;
|
|
case 5:
|
|
/* Number would eventually match, if longer */
|
|
retval = 3;
|
|
break;
|
|
}
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "ICALL: ret=%d\n", retval);
|
|
#endif
|
|
return retval;
|
|
break;
|
|
case ISDN_STAT_CINF:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "CINF: %ld %s\n", c->arg, c->parm.num);
|
|
#endif
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
if (strcmp(c->parm.num, "0"))
|
|
isdn_net_stat_callback(i, c);
|
|
isdn_tty_stat_callback(i, c);
|
|
break;
|
|
case ISDN_STAT_CAUSE:
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "CAUSE: %ld %s\n", c->arg, c->parm.num);
|
|
#endif
|
|
printk(KERN_INFO "isdn: %s,ch%ld cause: %s\n",
|
|
dev->drvid[di], c->arg, c->parm.num);
|
|
isdn_tty_stat_callback(i, c);
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
if (divert_if)
|
|
divert_if->stat_callback(c);
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
break;
|
|
case ISDN_STAT_DISPLAY:
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "DISPLAY: %ld %s\n", c->arg, c->parm.display);
|
|
#endif
|
|
isdn_tty_stat_callback(i, c);
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
if (divert_if)
|
|
divert_if->stat_callback(c);
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
break;
|
|
case ISDN_STAT_DCONN:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "DCONN: %ld\n", c->arg);
|
|
#endif
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
/* Find any net-device, waiting for D-channel setup */
|
|
if (isdn_net_stat_callback(i, c))
|
|
break;
|
|
isdn_v110_stat_callback(i, c);
|
|
/* Find any ttyI, waiting for D-channel setup */
|
|
if (isdn_tty_stat_callback(i, c)) {
|
|
cmd.driver = di;
|
|
cmd.arg = c->arg;
|
|
cmd.command = ISDN_CMD_ACCEPTB;
|
|
isdn_command(&cmd);
|
|
break;
|
|
}
|
|
break;
|
|
case ISDN_STAT_DHUP:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "DHUP: %ld\n", c->arg);
|
|
#endif
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
dev->drv[di]->online &= ~(1 << (c->arg));
|
|
isdn_info_update();
|
|
/* Signal hangup to network-devices */
|
|
if (isdn_net_stat_callback(i, c))
|
|
break;
|
|
isdn_v110_stat_callback(i, c);
|
|
if (isdn_tty_stat_callback(i, c))
|
|
break;
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
if (divert_if)
|
|
divert_if->stat_callback(c);
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
break;
|
|
break;
|
|
case ISDN_STAT_BCONN:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "BCONN: %ld\n", c->arg);
|
|
#endif
|
|
/* Signal B-channel-connect to network-devices */
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
dev->drv[di]->online |= (1 << (c->arg));
|
|
isdn_info_update();
|
|
if (isdn_net_stat_callback(i, c))
|
|
break;
|
|
isdn_v110_stat_callback(i, c);
|
|
if (isdn_tty_stat_callback(i, c))
|
|
break;
|
|
break;
|
|
case ISDN_STAT_BHUP:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "BHUP: %ld\n", c->arg);
|
|
#endif
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
dev->drv[di]->online &= ~(1 << (c->arg));
|
|
isdn_info_update();
|
|
#ifdef CONFIG_ISDN_X25
|
|
/* Signal hangup to network-devices */
|
|
if (isdn_net_stat_callback(i, c))
|
|
break;
|
|
#endif
|
|
isdn_v110_stat_callback(i, c);
|
|
if (isdn_tty_stat_callback(i, c))
|
|
break;
|
|
break;
|
|
case ISDN_STAT_NODCH:
|
|
if (i < 0)
|
|
return -1;
|
|
#ifdef ISDN_DEBUG_STATCALLB
|
|
printk(KERN_DEBUG "NODCH: %ld\n", c->arg);
|
|
#endif
|
|
if (dev->global_flags & ISDN_GLOBAL_STOPPED)
|
|
return 0;
|
|
if (isdn_net_stat_callback(i, c))
|
|
break;
|
|
if (isdn_tty_stat_callback(i, c))
|
|
break;
|
|
break;
|
|
case ISDN_STAT_ADDCH:
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
if (isdn_add_channels(dev->drv[di], di, c->arg, 1)) {
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
return -1;
|
|
}
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
isdn_info_update();
|
|
break;
|
|
case ISDN_STAT_DISCH:
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if ((dev->drvmap[i] == di) &&
|
|
(dev->chanmap[i] == c->arg)) {
|
|
if (c->parm.num[0])
|
|
dev->usage[i] &= ~ISDN_USAGE_DISABLED;
|
|
else
|
|
if (USG_NONE(dev->usage[i])) {
|
|
dev->usage[i] |= ISDN_USAGE_DISABLED;
|
|
}
|
|
else
|
|
retval = -1;
|
|
break;
|
|
}
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
isdn_info_update();
|
|
break;
|
|
case ISDN_STAT_UNLOAD:
|
|
while (dev->drv[di]->locks > 0) {
|
|
isdn_unlock_driver(dev->drv[di]);
|
|
}
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
isdn_tty_stat_callback(i, c);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if (dev->drvmap[i] == di) {
|
|
dev->drvmap[i] = -1;
|
|
dev->chanmap[i] = -1;
|
|
dev->usage[i] &= ~ISDN_USAGE_DISABLED;
|
|
}
|
|
dev->drivers--;
|
|
dev->channels -= dev->drv[di]->channels;
|
|
kfree(dev->drv[di]->rcverr);
|
|
kfree(dev->drv[di]->rcvcount);
|
|
for (i = 0; i < dev->drv[di]->channels; i++)
|
|
skb_queue_purge(&dev->drv[di]->rpqueue[i]);
|
|
kfree(dev->drv[di]->rpqueue);
|
|
kfree(dev->drv[di]->rcv_waitq);
|
|
kfree(dev->drv[di]);
|
|
dev->drv[di] = NULL;
|
|
dev->drvid[di][0] = '\0';
|
|
isdn_info_update();
|
|
set_global_features();
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
return 0;
|
|
case ISDN_STAT_L1ERR:
|
|
break;
|
|
case CAPI_PUT_MESSAGE:
|
|
return(isdn_capi_rec_hl_msg(&c->parm.cmsg));
|
|
#ifdef CONFIG_ISDN_TTY_FAX
|
|
case ISDN_STAT_FAXIND:
|
|
isdn_tty_stat_callback(i, c);
|
|
break;
|
|
#endif
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
case ISDN_STAT_AUDIO:
|
|
isdn_tty_stat_callback(i, c);
|
|
break;
|
|
#endif
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
case ISDN_STAT_PROT:
|
|
case ISDN_STAT_REDIR:
|
|
if (divert_if)
|
|
return(divert_if->stat_callback(c));
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
default:
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Get integer from char-pointer, set pointer to end of number
|
|
*/
|
|
int
|
|
isdn_getnum(char **p)
|
|
{
|
|
int v = -1;
|
|
|
|
while (*p[0] >= '0' && *p[0] <= '9')
|
|
v = ((v < 0) ? 0 : (v * 10)) + (int) ((*p[0]++) - '0');
|
|
return v;
|
|
}
|
|
|
|
#define DLE 0x10
|
|
|
|
/*
|
|
* isdn_readbchan() tries to get data from the read-queue.
|
|
* It MUST be called with interrupts off.
|
|
*
|
|
* Be aware that this is not an atomic operation when sleep != 0, even though
|
|
* interrupts are turned off! Well, like that we are currently only called
|
|
* on behalf of a read system call on raw device files (which are documented
|
|
* to be dangerous and for for debugging purpose only). The inode semaphore
|
|
* takes care that this is not called for the same minor device number while
|
|
* we are sleeping, but access is not serialized against simultaneous read()
|
|
* from the corresponding ttyI device. Can other ugly events, like changes
|
|
* of the mapping (di,ch)<->minor, happen during the sleep? --he
|
|
*/
|
|
int
|
|
isdn_readbchan(int di, int channel, u_char * buf, u_char * fp, int len, wait_queue_head_t *sleep)
|
|
{
|
|
int count;
|
|
int count_pull;
|
|
int count_put;
|
|
int dflag;
|
|
struct sk_buff *skb;
|
|
u_char *cp;
|
|
|
|
if (!dev->drv[di])
|
|
return 0;
|
|
if (skb_queue_empty(&dev->drv[di]->rpqueue[channel])) {
|
|
if (sleep)
|
|
interruptible_sleep_on(sleep);
|
|
else
|
|
return 0;
|
|
}
|
|
if (len > dev->drv[di]->rcvcount[channel])
|
|
len = dev->drv[di]->rcvcount[channel];
|
|
cp = buf;
|
|
count = 0;
|
|
while (len) {
|
|
if (!(skb = skb_peek(&dev->drv[di]->rpqueue[channel])))
|
|
break;
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
if (ISDN_AUDIO_SKB_LOCK(skb))
|
|
break;
|
|
ISDN_AUDIO_SKB_LOCK(skb) = 1;
|
|
if ((ISDN_AUDIO_SKB_DLECOUNT(skb)) || (dev->drv[di]->DLEflag & (1 << channel))) {
|
|
char *p = skb->data;
|
|
unsigned long DLEmask = (1 << channel);
|
|
|
|
dflag = 0;
|
|
count_pull = count_put = 0;
|
|
while ((count_pull < skb->len) && (len > 0)) {
|
|
len--;
|
|
if (dev->drv[di]->DLEflag & DLEmask) {
|
|
*cp++ = DLE;
|
|
dev->drv[di]->DLEflag &= ~DLEmask;
|
|
} else {
|
|
*cp++ = *p;
|
|
if (*p == DLE) {
|
|
dev->drv[di]->DLEflag |= DLEmask;
|
|
(ISDN_AUDIO_SKB_DLECOUNT(skb))--;
|
|
}
|
|
p++;
|
|
count_pull++;
|
|
}
|
|
count_put++;
|
|
}
|
|
if (count_pull >= skb->len)
|
|
dflag = 1;
|
|
} else {
|
|
#endif
|
|
/* No DLE's in buff, so simply copy it */
|
|
dflag = 1;
|
|
if ((count_pull = skb->len) > len) {
|
|
count_pull = len;
|
|
dflag = 0;
|
|
}
|
|
count_put = count_pull;
|
|
memcpy(cp, skb->data, count_put);
|
|
cp += count_put;
|
|
len -= count_put;
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
}
|
|
#endif
|
|
count += count_put;
|
|
if (fp) {
|
|
memset(fp, 0, count_put);
|
|
fp += count_put;
|
|
}
|
|
if (dflag) {
|
|
/* We got all the data in this buff.
|
|
* Now we can dequeue it.
|
|
*/
|
|
if (fp)
|
|
*(fp - 1) = 0xff;
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
ISDN_AUDIO_SKB_LOCK(skb) = 0;
|
|
#endif
|
|
skb = skb_dequeue(&dev->drv[di]->rpqueue[channel]);
|
|
dev_kfree_skb(skb);
|
|
} else {
|
|
/* Not yet emptied this buff, so it
|
|
* must stay in the queue, for further calls
|
|
* but we pull off the data we got until now.
|
|
*/
|
|
skb_pull(skb, count_pull);
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
ISDN_AUDIO_SKB_LOCK(skb) = 0;
|
|
#endif
|
|
}
|
|
dev->drv[di]->rcvcount[channel] -= count_put;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/*
|
|
* isdn_readbchan_tty() tries to get data from the read-queue.
|
|
* It MUST be called with interrupts off.
|
|
*
|
|
* Be aware that this is not an atomic operation when sleep != 0, even though
|
|
* interrupts are turned off! Well, like that we are currently only called
|
|
* on behalf of a read system call on raw device files (which are documented
|
|
* to be dangerous and for for debugging purpose only). The inode semaphore
|
|
* takes care that this is not called for the same minor device number while
|
|
* we are sleeping, but access is not serialized against simultaneous read()
|
|
* from the corresponding ttyI device. Can other ugly events, like changes
|
|
* of the mapping (di,ch)<->minor, happen during the sleep? --he
|
|
*/
|
|
int
|
|
isdn_readbchan_tty(int di, int channel, struct tty_struct *tty, int cisco_hack)
|
|
{
|
|
int count;
|
|
int count_pull;
|
|
int count_put;
|
|
int dflag;
|
|
struct sk_buff *skb;
|
|
char last = 0;
|
|
int len;
|
|
|
|
if (!dev->drv[di])
|
|
return 0;
|
|
if (skb_queue_empty(&dev->drv[di]->rpqueue[channel]))
|
|
return 0;
|
|
|
|
len = tty_buffer_request_room(tty, dev->drv[di]->rcvcount[channel]);
|
|
if(len == 0)
|
|
return len;
|
|
|
|
count = 0;
|
|
while (len) {
|
|
if (!(skb = skb_peek(&dev->drv[di]->rpqueue[channel])))
|
|
break;
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
if (ISDN_AUDIO_SKB_LOCK(skb))
|
|
break;
|
|
ISDN_AUDIO_SKB_LOCK(skb) = 1;
|
|
if ((ISDN_AUDIO_SKB_DLECOUNT(skb)) || (dev->drv[di]->DLEflag & (1 << channel))) {
|
|
char *p = skb->data;
|
|
unsigned long DLEmask = (1 << channel);
|
|
|
|
dflag = 0;
|
|
count_pull = count_put = 0;
|
|
while ((count_pull < skb->len) && (len > 0)) {
|
|
len--;
|
|
if (dev->drv[di]->DLEflag & DLEmask) {
|
|
last = DLE;
|
|
dev->drv[di]->DLEflag &= ~DLEmask;
|
|
} else {
|
|
last = *p;
|
|
if (last == DLE) {
|
|
dev->drv[di]->DLEflag |= DLEmask;
|
|
(ISDN_AUDIO_SKB_DLECOUNT(skb))--;
|
|
}
|
|
p++;
|
|
count_pull++;
|
|
}
|
|
count_put++;
|
|
}
|
|
if (count_pull >= skb->len)
|
|
dflag = 1;
|
|
} else {
|
|
#endif
|
|
/* No DLE's in buff, so simply copy it */
|
|
dflag = 1;
|
|
if ((count_pull = skb->len) > len) {
|
|
count_pull = len;
|
|
dflag = 0;
|
|
}
|
|
count_put = count_pull;
|
|
if(count_put > 1)
|
|
tty_insert_flip_string(tty, skb->data, count_put - 1);
|
|
last = skb->data[count_put] - 1;
|
|
len -= count_put;
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
}
|
|
#endif
|
|
count += count_put;
|
|
if (dflag) {
|
|
/* We got all the data in this buff.
|
|
* Now we can dequeue it.
|
|
*/
|
|
if(cisco_hack)
|
|
tty_insert_flip_char(tty, last, 0xFF);
|
|
else
|
|
tty_insert_flip_char(tty, last, TTY_NORMAL);
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
ISDN_AUDIO_SKB_LOCK(skb) = 0;
|
|
#endif
|
|
skb = skb_dequeue(&dev->drv[di]->rpqueue[channel]);
|
|
dev_kfree_skb(skb);
|
|
} else {
|
|
tty_insert_flip_char(tty, last, TTY_NORMAL);
|
|
/* Not yet emptied this buff, so it
|
|
* must stay in the queue, for further calls
|
|
* but we pull off the data we got until now.
|
|
*/
|
|
skb_pull(skb, count_pull);
|
|
#ifdef CONFIG_ISDN_AUDIO
|
|
ISDN_AUDIO_SKB_LOCK(skb) = 0;
|
|
#endif
|
|
}
|
|
dev->drv[di]->rcvcount[channel] -= count_put;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
|
|
static __inline int
|
|
isdn_minor2drv(int minor)
|
|
{
|
|
return (dev->drvmap[minor]);
|
|
}
|
|
|
|
static __inline int
|
|
isdn_minor2chan(int minor)
|
|
{
|
|
return (dev->chanmap[minor]);
|
|
}
|
|
|
|
static char *
|
|
isdn_statstr(void)
|
|
{
|
|
static char istatbuf[2048];
|
|
char *p;
|
|
int i;
|
|
|
|
sprintf(istatbuf, "idmap:\t");
|
|
p = istatbuf + strlen(istatbuf);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
sprintf(p, "%s ", (dev->drvmap[i] < 0) ? "-" : dev->drvid[dev->drvmap[i]]);
|
|
p = istatbuf + strlen(istatbuf);
|
|
}
|
|
sprintf(p, "\nchmap:\t");
|
|
p = istatbuf + strlen(istatbuf);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
sprintf(p, "%d ", dev->chanmap[i]);
|
|
p = istatbuf + strlen(istatbuf);
|
|
}
|
|
sprintf(p, "\ndrmap:\t");
|
|
p = istatbuf + strlen(istatbuf);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
sprintf(p, "%d ", dev->drvmap[i]);
|
|
p = istatbuf + strlen(istatbuf);
|
|
}
|
|
sprintf(p, "\nusage:\t");
|
|
p = istatbuf + strlen(istatbuf);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
sprintf(p, "%d ", dev->usage[i]);
|
|
p = istatbuf + strlen(istatbuf);
|
|
}
|
|
sprintf(p, "\nflags:\t");
|
|
p = istatbuf + strlen(istatbuf);
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++) {
|
|
if (dev->drv[i]) {
|
|
sprintf(p, "%ld ", dev->drv[i]->online);
|
|
p = istatbuf + strlen(istatbuf);
|
|
} else {
|
|
sprintf(p, "? ");
|
|
p = istatbuf + strlen(istatbuf);
|
|
}
|
|
}
|
|
sprintf(p, "\nphone:\t");
|
|
p = istatbuf + strlen(istatbuf);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
sprintf(p, "%s ", dev->num[i]);
|
|
p = istatbuf + strlen(istatbuf);
|
|
}
|
|
sprintf(p, "\n");
|
|
return istatbuf;
|
|
}
|
|
|
|
/* Module interface-code */
|
|
|
|
void
|
|
isdn_info_update(void)
|
|
{
|
|
infostruct *p = dev->infochain;
|
|
|
|
while (p) {
|
|
*(p->private) = 1;
|
|
p = (infostruct *) p->next;
|
|
}
|
|
wake_up_interruptible(&(dev->info_waitq));
|
|
}
|
|
|
|
static ssize_t
|
|
isdn_read(struct file *file, char __user *buf, size_t count, loff_t * off)
|
|
{
|
|
uint minor = MINOR(file->f_dentry->d_inode->i_rdev);
|
|
int len = 0;
|
|
int drvidx;
|
|
int chidx;
|
|
int retval;
|
|
char *p;
|
|
|
|
lock_kernel();
|
|
if (minor == ISDN_MINOR_STATUS) {
|
|
if (!file->private_data) {
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
retval = -EAGAIN;
|
|
goto out;
|
|
}
|
|
interruptible_sleep_on(&(dev->info_waitq));
|
|
}
|
|
p = isdn_statstr();
|
|
file->private_data = NULL;
|
|
if ((len = strlen(p)) <= count) {
|
|
if (copy_to_user(buf, p, len)) {
|
|
retval = -EFAULT;
|
|
goto out;
|
|
}
|
|
*off += len;
|
|
retval = len;
|
|
goto out;
|
|
}
|
|
retval = 0;
|
|
goto out;
|
|
}
|
|
if (!dev->drivers) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
if (minor <= ISDN_MINOR_BMAX) {
|
|
printk(KERN_WARNING "isdn_read minor %d obsolete!\n", minor);
|
|
drvidx = isdn_minor2drv(minor);
|
|
if (drvidx < 0) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
if (!(dev->drv[drvidx]->flags & DRV_FLAG_RUNNING)) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
chidx = isdn_minor2chan(minor);
|
|
if (!(p = kmalloc(count, GFP_KERNEL))) {
|
|
retval = -ENOMEM;
|
|
goto out;
|
|
}
|
|
len = isdn_readbchan(drvidx, chidx, p, NULL, count,
|
|
&dev->drv[drvidx]->rcv_waitq[chidx]);
|
|
*off += len;
|
|
if (copy_to_user(buf,p,len))
|
|
len = -EFAULT;
|
|
kfree(p);
|
|
retval = len;
|
|
goto out;
|
|
}
|
|
if (minor <= ISDN_MINOR_CTRLMAX) {
|
|
drvidx = isdn_minor2drv(minor - ISDN_MINOR_CTRL);
|
|
if (drvidx < 0) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
if (!dev->drv[drvidx]->stavail) {
|
|
if (file->f_flags & O_NONBLOCK) {
|
|
retval = -EAGAIN;
|
|
goto out;
|
|
}
|
|
interruptible_sleep_on(&(dev->drv[drvidx]->st_waitq));
|
|
}
|
|
if (dev->drv[drvidx]->interface->readstat) {
|
|
if (count > dev->drv[drvidx]->stavail)
|
|
count = dev->drv[drvidx]->stavail;
|
|
len = dev->drv[drvidx]->interface->
|
|
readstat(buf, count, drvidx,
|
|
isdn_minor2chan(minor));
|
|
} else {
|
|
len = 0;
|
|
}
|
|
if (len)
|
|
dev->drv[drvidx]->stavail -= len;
|
|
else
|
|
dev->drv[drvidx]->stavail = 0;
|
|
*off += len;
|
|
retval = len;
|
|
goto out;
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (minor <= ISDN_MINOR_PPPMAX) {
|
|
retval = isdn_ppp_read(minor - ISDN_MINOR_PPP, file, buf, count);
|
|
goto out;
|
|
}
|
|
#endif
|
|
retval = -ENODEV;
|
|
out:
|
|
unlock_kernel();
|
|
return retval;
|
|
}
|
|
|
|
static ssize_t
|
|
isdn_write(struct file *file, const char __user *buf, size_t count, loff_t * off)
|
|
{
|
|
uint minor = MINOR(file->f_dentry->d_inode->i_rdev);
|
|
int drvidx;
|
|
int chidx;
|
|
int retval;
|
|
|
|
if (minor == ISDN_MINOR_STATUS)
|
|
return -EPERM;
|
|
if (!dev->drivers)
|
|
return -ENODEV;
|
|
|
|
lock_kernel();
|
|
if (minor <= ISDN_MINOR_BMAX) {
|
|
printk(KERN_WARNING "isdn_write minor %d obsolete!\n", minor);
|
|
drvidx = isdn_minor2drv(minor);
|
|
if (drvidx < 0) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
if (!(dev->drv[drvidx]->flags & DRV_FLAG_RUNNING)) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
chidx = isdn_minor2chan(minor);
|
|
while (isdn_writebuf_stub(drvidx, chidx, buf, count) != count)
|
|
interruptible_sleep_on(&dev->drv[drvidx]->snd_waitq[chidx]);
|
|
retval = count;
|
|
goto out;
|
|
}
|
|
if (minor <= ISDN_MINOR_CTRLMAX) {
|
|
drvidx = isdn_minor2drv(minor - ISDN_MINOR_CTRL);
|
|
if (drvidx < 0) {
|
|
retval = -ENODEV;
|
|
goto out;
|
|
}
|
|
/*
|
|
* We want to use the isdnctrl device to load the firmware
|
|
*
|
|
if (!(dev->drv[drvidx]->flags & DRV_FLAG_RUNNING))
|
|
return -ENODEV;
|
|
*/
|
|
if (dev->drv[drvidx]->interface->writecmd)
|
|
retval = dev->drv[drvidx]->interface->
|
|
writecmd(buf, count, drvidx, isdn_minor2chan(minor));
|
|
else
|
|
retval = count;
|
|
goto out;
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (minor <= ISDN_MINOR_PPPMAX) {
|
|
retval = isdn_ppp_write(minor - ISDN_MINOR_PPP, file, buf, count);
|
|
goto out;
|
|
}
|
|
#endif
|
|
retval = -ENODEV;
|
|
out:
|
|
unlock_kernel();
|
|
return retval;
|
|
}
|
|
|
|
static unsigned int
|
|
isdn_poll(struct file *file, poll_table * wait)
|
|
{
|
|
unsigned int mask = 0;
|
|
unsigned int minor = MINOR(file->f_dentry->d_inode->i_rdev);
|
|
int drvidx = isdn_minor2drv(minor - ISDN_MINOR_CTRL);
|
|
|
|
lock_kernel();
|
|
if (minor == ISDN_MINOR_STATUS) {
|
|
poll_wait(file, &(dev->info_waitq), wait);
|
|
/* mask = POLLOUT | POLLWRNORM; */
|
|
if (file->private_data) {
|
|
mask |= POLLIN | POLLRDNORM;
|
|
}
|
|
goto out;
|
|
}
|
|
if (minor >= ISDN_MINOR_CTRL && minor <= ISDN_MINOR_CTRLMAX) {
|
|
if (drvidx < 0) {
|
|
/* driver deregistered while file open */
|
|
mask = POLLHUP;
|
|
goto out;
|
|
}
|
|
poll_wait(file, &(dev->drv[drvidx]->st_waitq), wait);
|
|
mask = POLLOUT | POLLWRNORM;
|
|
if (dev->drv[drvidx]->stavail) {
|
|
mask |= POLLIN | POLLRDNORM;
|
|
}
|
|
goto out;
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (minor <= ISDN_MINOR_PPPMAX) {
|
|
mask = isdn_ppp_poll(file, wait);
|
|
goto out;
|
|
}
|
|
#endif
|
|
mask = POLLERR;
|
|
out:
|
|
unlock_kernel();
|
|
return mask;
|
|
}
|
|
|
|
|
|
static int
|
|
isdn_ioctl(struct inode *inode, struct file *file, uint cmd, ulong arg)
|
|
{
|
|
uint minor = MINOR(inode->i_rdev);
|
|
isdn_ctrl c;
|
|
int drvidx;
|
|
int chidx;
|
|
int ret;
|
|
int i;
|
|
char __user *p;
|
|
char *s;
|
|
union iocpar {
|
|
char name[10];
|
|
char bname[22];
|
|
isdn_ioctl_struct iocts;
|
|
isdn_net_ioctl_phone phone;
|
|
isdn_net_ioctl_cfg cfg;
|
|
} iocpar;
|
|
void __user *argp = (void __user *)arg;
|
|
|
|
#define name iocpar.name
|
|
#define bname iocpar.bname
|
|
#define iocts iocpar.iocts
|
|
#define phone iocpar.phone
|
|
#define cfg iocpar.cfg
|
|
|
|
if (minor == ISDN_MINOR_STATUS) {
|
|
switch (cmd) {
|
|
case IIOCGETDVR:
|
|
return (TTY_DV +
|
|
(NET_DV << 8) +
|
|
(INF_DV << 16));
|
|
case IIOCGETCPS:
|
|
if (arg) {
|
|
ulong __user *p = argp;
|
|
int i;
|
|
if (!access_ok(VERIFY_WRITE, p,
|
|
sizeof(ulong) * ISDN_MAX_CHANNELS * 2))
|
|
return -EFAULT;
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
put_user(dev->ibytes[i], p++);
|
|
put_user(dev->obytes[i], p++);
|
|
}
|
|
return 0;
|
|
} else
|
|
return -EINVAL;
|
|
break;
|
|
#ifdef CONFIG_NETDEVICES
|
|
case IIOCNETGPN:
|
|
/* Get peer phone number of a connected
|
|
* isdn network interface */
|
|
if (arg) {
|
|
if (copy_from_user(&phone, argp, sizeof(phone)))
|
|
return -EFAULT;
|
|
return isdn_net_getpeer(&phone, argp);
|
|
} else
|
|
return -EINVAL;
|
|
#endif
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
if (!dev->drivers)
|
|
return -ENODEV;
|
|
if (minor <= ISDN_MINOR_BMAX) {
|
|
drvidx = isdn_minor2drv(minor);
|
|
if (drvidx < 0)
|
|
return -ENODEV;
|
|
chidx = isdn_minor2chan(minor);
|
|
if (!(dev->drv[drvidx]->flags & DRV_FLAG_RUNNING))
|
|
return -ENODEV;
|
|
return 0;
|
|
}
|
|
if (minor <= ISDN_MINOR_CTRLMAX) {
|
|
/*
|
|
* isdn net devices manage lots of configuration variables as linked lists.
|
|
* Those lists must only be manipulated from user space. Some of the ioctl's
|
|
* service routines access user space and are not atomic. Therefor, ioctl's
|
|
* manipulating the lists and ioctl's sleeping while accessing the lists
|
|
* are serialized by means of a semaphore.
|
|
*/
|
|
switch (cmd) {
|
|
case IIOCNETDWRSET:
|
|
printk(KERN_INFO "INFO: ISDN_DW_ABC_EXTENSION not enabled\n");
|
|
return(-EINVAL);
|
|
case IIOCNETLCR:
|
|
printk(KERN_INFO "INFO: ISDN_ABC_LCR_SUPPORT not enabled\n");
|
|
return -ENODEV;
|
|
#ifdef CONFIG_NETDEVICES
|
|
case IIOCNETAIF:
|
|
/* Add a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(name, argp, sizeof(name)))
|
|
return -EFAULT;
|
|
s = name;
|
|
} else {
|
|
s = NULL;
|
|
}
|
|
ret = down_interruptible(&dev->sem);
|
|
if( ret ) return ret;
|
|
if ((s = isdn_net_new(s, NULL))) {
|
|
if (copy_to_user(argp, s, strlen(s) + 1)){
|
|
ret = -EFAULT;
|
|
} else {
|
|
ret = 0;
|
|
}
|
|
} else
|
|
ret = -ENODEV;
|
|
up(&dev->sem);
|
|
return ret;
|
|
case IIOCNETASL:
|
|
/* Add a slave to a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(bname, argp, sizeof(bname) - 1))
|
|
return -EFAULT;
|
|
} else
|
|
return -EINVAL;
|
|
ret = down_interruptible(&dev->sem);
|
|
if( ret ) return ret;
|
|
if ((s = isdn_net_newslave(bname))) {
|
|
if (copy_to_user(argp, s, strlen(s) + 1)){
|
|
ret = -EFAULT;
|
|
} else {
|
|
ret = 0;
|
|
}
|
|
} else
|
|
ret = -ENODEV;
|
|
up(&dev->sem);
|
|
return ret;
|
|
case IIOCNETDIF:
|
|
/* Delete a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(name, argp, sizeof(name)))
|
|
return -EFAULT;
|
|
ret = down_interruptible(&dev->sem);
|
|
if( ret ) return ret;
|
|
ret = isdn_net_rm(name);
|
|
up(&dev->sem);
|
|
return ret;
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCNETSCF:
|
|
/* Set configurable parameters of a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(&cfg, argp, sizeof(cfg)))
|
|
return -EFAULT;
|
|
return isdn_net_setcfg(&cfg);
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCNETGCF:
|
|
/* Get configurable parameters of a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(&cfg, argp, sizeof(cfg)))
|
|
return -EFAULT;
|
|
if (!(ret = isdn_net_getcfg(&cfg))) {
|
|
if (copy_to_user(argp, &cfg, sizeof(cfg)))
|
|
return -EFAULT;
|
|
}
|
|
return ret;
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCNETANM:
|
|
/* Add a phone-number to a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(&phone, argp, sizeof(phone)))
|
|
return -EFAULT;
|
|
ret = down_interruptible(&dev->sem);
|
|
if( ret ) return ret;
|
|
ret = isdn_net_addphone(&phone);
|
|
up(&dev->sem);
|
|
return ret;
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCNETGNM:
|
|
/* Get list of phone-numbers of a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(&phone, argp, sizeof(phone)))
|
|
return -EFAULT;
|
|
ret = down_interruptible(&dev->sem);
|
|
if( ret ) return ret;
|
|
ret = isdn_net_getphones(&phone, argp);
|
|
up(&dev->sem);
|
|
return ret;
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCNETDNM:
|
|
/* Delete a phone-number of a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(&phone, argp, sizeof(phone)))
|
|
return -EFAULT;
|
|
ret = down_interruptible(&dev->sem);
|
|
if( ret ) return ret;
|
|
ret = isdn_net_delphone(&phone);
|
|
up(&dev->sem);
|
|
return ret;
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCNETDIL:
|
|
/* Force dialing of a network-interface */
|
|
if (arg) {
|
|
if (copy_from_user(name, argp, sizeof(name)))
|
|
return -EFAULT;
|
|
return isdn_net_force_dial(name);
|
|
} else
|
|
return -EINVAL;
|
|
#ifdef CONFIG_ISDN_PPP
|
|
case IIOCNETALN:
|
|
if (!arg)
|
|
return -EINVAL;
|
|
if (copy_from_user(name, argp, sizeof(name)))
|
|
return -EFAULT;
|
|
return isdn_ppp_dial_slave(name);
|
|
case IIOCNETDLN:
|
|
if (!arg)
|
|
return -EINVAL;
|
|
if (copy_from_user(name, argp, sizeof(name)))
|
|
return -EFAULT;
|
|
return isdn_ppp_hangup_slave(name);
|
|
#endif
|
|
case IIOCNETHUP:
|
|
/* Force hangup of a network-interface */
|
|
if (!arg)
|
|
return -EINVAL;
|
|
if (copy_from_user(name, argp, sizeof(name)))
|
|
return -EFAULT;
|
|
return isdn_net_force_hangup(name);
|
|
break;
|
|
#endif /* CONFIG_NETDEVICES */
|
|
case IIOCSETVER:
|
|
dev->net_verbose = arg;
|
|
printk(KERN_INFO "isdn: Verbose-Level is %d\n", dev->net_verbose);
|
|
return 0;
|
|
case IIOCSETGST:
|
|
if (arg)
|
|
dev->global_flags |= ISDN_GLOBAL_STOPPED;
|
|
else
|
|
dev->global_flags &= ~ISDN_GLOBAL_STOPPED;
|
|
printk(KERN_INFO "isdn: Global Mode %s\n",
|
|
(dev->global_flags & ISDN_GLOBAL_STOPPED) ? "stopped" : "running");
|
|
return 0;
|
|
case IIOCSETBRJ:
|
|
drvidx = -1;
|
|
if (arg) {
|
|
int i;
|
|
char *p;
|
|
if (copy_from_user(&iocts, argp,
|
|
sizeof(isdn_ioctl_struct)))
|
|
return -EFAULT;
|
|
if (strlen(iocts.drvid)) {
|
|
if ((p = strchr(iocts.drvid, ',')))
|
|
*p = 0;
|
|
drvidx = -1;
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++)
|
|
if (!(strcmp(dev->drvid[i], iocts.drvid))) {
|
|
drvidx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (drvidx == -1)
|
|
return -ENODEV;
|
|
if (iocts.arg)
|
|
dev->drv[drvidx]->flags |= DRV_FLAG_REJBUS;
|
|
else
|
|
dev->drv[drvidx]->flags &= ~DRV_FLAG_REJBUS;
|
|
return 0;
|
|
case IIOCSIGPRF:
|
|
dev->profd = current;
|
|
return 0;
|
|
break;
|
|
case IIOCGETPRF:
|
|
/* Get all Modem-Profiles */
|
|
if (arg) {
|
|
char __user *p = argp;
|
|
int i;
|
|
|
|
if (!access_ok(VERIFY_WRITE, argp,
|
|
(ISDN_MODEM_NUMREG + ISDN_MSNLEN + ISDN_LMSNLEN)
|
|
* ISDN_MAX_CHANNELS))
|
|
return -EFAULT;
|
|
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
if (copy_to_user(p, dev->mdm.info[i].emu.profile,
|
|
ISDN_MODEM_NUMREG))
|
|
return -EFAULT;
|
|
p += ISDN_MODEM_NUMREG;
|
|
if (copy_to_user(p, dev->mdm.info[i].emu.pmsn, ISDN_MSNLEN))
|
|
return -EFAULT;
|
|
p += ISDN_MSNLEN;
|
|
if (copy_to_user(p, dev->mdm.info[i].emu.plmsn, ISDN_LMSNLEN))
|
|
return -EFAULT;
|
|
p += ISDN_LMSNLEN;
|
|
}
|
|
return (ISDN_MODEM_NUMREG + ISDN_MSNLEN + ISDN_LMSNLEN) * ISDN_MAX_CHANNELS;
|
|
} else
|
|
return -EINVAL;
|
|
break;
|
|
case IIOCSETPRF:
|
|
/* Set all Modem-Profiles */
|
|
if (arg) {
|
|
char __user *p = argp;
|
|
int i;
|
|
|
|
if (!access_ok(VERIFY_READ, argp,
|
|
(ISDN_MODEM_NUMREG + ISDN_MSNLEN + ISDN_LMSNLEN)
|
|
* ISDN_MAX_CHANNELS))
|
|
return -EFAULT;
|
|
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
if (copy_from_user(dev->mdm.info[i].emu.profile, p,
|
|
ISDN_MODEM_NUMREG))
|
|
return -EFAULT;
|
|
p += ISDN_MODEM_NUMREG;
|
|
if (copy_from_user(dev->mdm.info[i].emu.plmsn, p, ISDN_LMSNLEN))
|
|
return -EFAULT;
|
|
p += ISDN_LMSNLEN;
|
|
if (copy_from_user(dev->mdm.info[i].emu.pmsn, p, ISDN_MSNLEN))
|
|
return -EFAULT;
|
|
p += ISDN_MSNLEN;
|
|
}
|
|
return 0;
|
|
} else
|
|
return -EINVAL;
|
|
break;
|
|
case IIOCSETMAP:
|
|
case IIOCGETMAP:
|
|
/* Set/Get MSN->EAZ-Mapping for a driver */
|
|
if (arg) {
|
|
|
|
if (copy_from_user(&iocts, argp,
|
|
sizeof(isdn_ioctl_struct)))
|
|
return -EFAULT;
|
|
if (strlen(iocts.drvid)) {
|
|
drvidx = -1;
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++)
|
|
if (!(strcmp(dev->drvid[i], iocts.drvid))) {
|
|
drvidx = i;
|
|
break;
|
|
}
|
|
} else
|
|
drvidx = 0;
|
|
if (drvidx == -1)
|
|
return -ENODEV;
|
|
if (cmd == IIOCSETMAP) {
|
|
int loop = 1;
|
|
|
|
p = (char __user *) iocts.arg;
|
|
i = 0;
|
|
while (loop) {
|
|
int j = 0;
|
|
|
|
while (1) {
|
|
if (!access_ok(VERIFY_READ, p, 1))
|
|
return -EFAULT;
|
|
get_user(bname[j], p++);
|
|
switch (bname[j]) {
|
|
case '\0':
|
|
loop = 0;
|
|
/* Fall through */
|
|
case ',':
|
|
bname[j] = '\0';
|
|
strcpy(dev->drv[drvidx]->msn2eaz[i], bname);
|
|
j = ISDN_MSNLEN;
|
|
break;
|
|
default:
|
|
j++;
|
|
}
|
|
if (j >= ISDN_MSNLEN)
|
|
break;
|
|
}
|
|
if (++i > 9)
|
|
break;
|
|
}
|
|
} else {
|
|
p = (char __user *) iocts.arg;
|
|
for (i = 0; i < 10; i++) {
|
|
sprintf(bname, "%s%s",
|
|
strlen(dev->drv[drvidx]->msn2eaz[i]) ?
|
|
dev->drv[drvidx]->msn2eaz[i] : "_",
|
|
(i < 9) ? "," : "\0");
|
|
if (copy_to_user(p, bname, strlen(bname) + 1))
|
|
return -EFAULT;
|
|
p += strlen(bname);
|
|
}
|
|
}
|
|
return 0;
|
|
} else
|
|
return -EINVAL;
|
|
case IIOCDBGVAR:
|
|
if (arg) {
|
|
if (copy_to_user(argp, &dev, sizeof(ulong)))
|
|
return -EFAULT;
|
|
return 0;
|
|
} else
|
|
return -EINVAL;
|
|
break;
|
|
default:
|
|
if ((cmd & IIOCDRVCTL) == IIOCDRVCTL)
|
|
cmd = ((cmd >> _IOC_NRSHIFT) & _IOC_NRMASK) & ISDN_DRVIOCTL_MASK;
|
|
else
|
|
return -EINVAL;
|
|
if (arg) {
|
|
int i;
|
|
char *p;
|
|
if (copy_from_user(&iocts, argp, sizeof(isdn_ioctl_struct)))
|
|
return -EFAULT;
|
|
if (strlen(iocts.drvid)) {
|
|
if ((p = strchr(iocts.drvid, ',')))
|
|
*p = 0;
|
|
drvidx = -1;
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++)
|
|
if (!(strcmp(dev->drvid[i], iocts.drvid))) {
|
|
drvidx = i;
|
|
break;
|
|
}
|
|
} else
|
|
drvidx = 0;
|
|
if (drvidx == -1)
|
|
return -ENODEV;
|
|
if (!access_ok(VERIFY_WRITE, argp,
|
|
sizeof(isdn_ioctl_struct)))
|
|
return -EFAULT;
|
|
c.driver = drvidx;
|
|
c.command = ISDN_CMD_IOCTL;
|
|
c.arg = cmd;
|
|
memcpy(c.parm.num, &iocts.arg, sizeof(ulong));
|
|
ret = isdn_command(&c);
|
|
memcpy(&iocts.arg, c.parm.num, sizeof(ulong));
|
|
if (copy_to_user(argp, &iocts, sizeof(isdn_ioctl_struct)))
|
|
return -EFAULT;
|
|
return ret;
|
|
} else
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (minor <= ISDN_MINOR_PPPMAX)
|
|
return (isdn_ppp_ioctl(minor - ISDN_MINOR_PPP, file, cmd, arg));
|
|
#endif
|
|
return -ENODEV;
|
|
|
|
#undef name
|
|
#undef bname
|
|
#undef iocts
|
|
#undef phone
|
|
#undef cfg
|
|
}
|
|
|
|
/*
|
|
* Open the device code.
|
|
*/
|
|
static int
|
|
isdn_open(struct inode *ino, struct file *filep)
|
|
{
|
|
uint minor = MINOR(ino->i_rdev);
|
|
int drvidx;
|
|
int chidx;
|
|
int retval = -ENODEV;
|
|
|
|
|
|
if (minor == ISDN_MINOR_STATUS) {
|
|
infostruct *p;
|
|
|
|
if ((p = kmalloc(sizeof(infostruct), GFP_KERNEL))) {
|
|
p->next = (char *) dev->infochain;
|
|
p->private = (char *) &(filep->private_data);
|
|
dev->infochain = p;
|
|
/* At opening we allow a single update */
|
|
filep->private_data = (char *) 1;
|
|
retval = 0;
|
|
goto out;
|
|
} else {
|
|
retval = -ENOMEM;
|
|
goto out;
|
|
}
|
|
}
|
|
if (!dev->channels)
|
|
goto out;
|
|
if (minor <= ISDN_MINOR_BMAX) {
|
|
printk(KERN_WARNING "isdn_open minor %d obsolete!\n", minor);
|
|
drvidx = isdn_minor2drv(minor);
|
|
if (drvidx < 0)
|
|
goto out;
|
|
chidx = isdn_minor2chan(minor);
|
|
if (!(dev->drv[drvidx]->flags & DRV_FLAG_RUNNING))
|
|
goto out;
|
|
if (!(dev->drv[drvidx]->online & (1 << chidx)))
|
|
goto out;
|
|
isdn_lock_drivers();
|
|
retval = 0;
|
|
goto out;
|
|
}
|
|
if (minor <= ISDN_MINOR_CTRLMAX) {
|
|
drvidx = isdn_minor2drv(minor - ISDN_MINOR_CTRL);
|
|
if (drvidx < 0)
|
|
goto out;
|
|
isdn_lock_drivers();
|
|
retval = 0;
|
|
goto out;
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (minor <= ISDN_MINOR_PPPMAX) {
|
|
retval = isdn_ppp_open(minor - ISDN_MINOR_PPP, filep);
|
|
if (retval == 0)
|
|
isdn_lock_drivers();
|
|
goto out;
|
|
}
|
|
#endif
|
|
out:
|
|
nonseekable_open(ino, filep);
|
|
return retval;
|
|
}
|
|
|
|
static int
|
|
isdn_close(struct inode *ino, struct file *filep)
|
|
{
|
|
uint minor = MINOR(ino->i_rdev);
|
|
|
|
lock_kernel();
|
|
if (minor == ISDN_MINOR_STATUS) {
|
|
infostruct *p = dev->infochain;
|
|
infostruct *q = NULL;
|
|
|
|
while (p) {
|
|
if (p->private == (char *) &(filep->private_data)) {
|
|
if (q)
|
|
q->next = p->next;
|
|
else
|
|
dev->infochain = (infostruct *) (p->next);
|
|
kfree(p);
|
|
goto out;
|
|
}
|
|
q = p;
|
|
p = (infostruct *) (p->next);
|
|
}
|
|
printk(KERN_WARNING "isdn: No private data while closing isdnctrl\n");
|
|
goto out;
|
|
}
|
|
isdn_unlock_drivers();
|
|
if (minor <= ISDN_MINOR_BMAX)
|
|
goto out;
|
|
if (minor <= ISDN_MINOR_CTRLMAX) {
|
|
if (dev->profd == current)
|
|
dev->profd = NULL;
|
|
goto out;
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (minor <= ISDN_MINOR_PPPMAX)
|
|
isdn_ppp_release(minor - ISDN_MINOR_PPP, filep);
|
|
#endif
|
|
|
|
out:
|
|
unlock_kernel();
|
|
return 0;
|
|
}
|
|
|
|
static struct file_operations isdn_fops =
|
|
{
|
|
.owner = THIS_MODULE,
|
|
.llseek = no_llseek,
|
|
.read = isdn_read,
|
|
.write = isdn_write,
|
|
.poll = isdn_poll,
|
|
.ioctl = isdn_ioctl,
|
|
.open = isdn_open,
|
|
.release = isdn_close,
|
|
};
|
|
|
|
char *
|
|
isdn_map_eaz2msn(char *msn, int di)
|
|
{
|
|
isdn_driver_t *this = dev->drv[di];
|
|
int i;
|
|
|
|
if (strlen(msn) == 1) {
|
|
i = msn[0] - '0';
|
|
if ((i >= 0) && (i <= 9))
|
|
if (strlen(this->msn2eaz[i]))
|
|
return (this->msn2eaz[i]);
|
|
}
|
|
return (msn);
|
|
}
|
|
|
|
/*
|
|
* Find an unused ISDN-channel, whose feature-flags match the
|
|
* given L2- and L3-protocols.
|
|
*/
|
|
#define L2V (~(ISDN_FEATURE_L2_V11096|ISDN_FEATURE_L2_V11019|ISDN_FEATURE_L2_V11038))
|
|
|
|
/*
|
|
* This function must be called with holding the dev->lock.
|
|
*/
|
|
int
|
|
isdn_get_free_channel(int usage, int l2_proto, int l3_proto, int pre_dev
|
|
,int pre_chan, char *msn)
|
|
{
|
|
int i;
|
|
ulong features;
|
|
ulong vfeatures;
|
|
|
|
features = ((1 << l2_proto) | (0x10000 << l3_proto));
|
|
vfeatures = (((1 << l2_proto) | (0x10000 << l3_proto)) &
|
|
~(ISDN_FEATURE_L2_V11096|ISDN_FEATURE_L2_V11019|ISDN_FEATURE_L2_V11038));
|
|
/* If Layer-2 protocol is V.110, accept drivers with
|
|
* transparent feature even if these don't support V.110
|
|
* because we can emulate this in linklevel.
|
|
*/
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if (USG_NONE(dev->usage[i]) &&
|
|
(dev->drvmap[i] != -1)) {
|
|
int d = dev->drvmap[i];
|
|
if ((dev->usage[i] & ISDN_USAGE_EXCLUSIVE) &&
|
|
((pre_dev != d) || (pre_chan != dev->chanmap[i])))
|
|
continue;
|
|
if (!strcmp(isdn_map_eaz2msn(msn, d), "-"))
|
|
continue;
|
|
if (dev->usage[i] & ISDN_USAGE_DISABLED)
|
|
continue; /* usage not allowed */
|
|
if (dev->drv[d]->flags & DRV_FLAG_RUNNING) {
|
|
if (((dev->drv[d]->interface->features & features) == features) ||
|
|
(((dev->drv[d]->interface->features & vfeatures) == vfeatures) &&
|
|
(dev->drv[d]->interface->features & ISDN_FEATURE_L2_TRANS))) {
|
|
if ((pre_dev < 0) || (pre_chan < 0)) {
|
|
dev->usage[i] &= ISDN_USAGE_EXCLUSIVE;
|
|
dev->usage[i] |= usage;
|
|
isdn_info_update();
|
|
return i;
|
|
} else {
|
|
if ((pre_dev == d) && (pre_chan == dev->chanmap[i])) {
|
|
dev->usage[i] &= ISDN_USAGE_EXCLUSIVE;
|
|
dev->usage[i] |= usage;
|
|
isdn_info_update();
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Set state of ISDN-channel to 'unused'
|
|
*/
|
|
void
|
|
isdn_free_channel(int di, int ch, int usage)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if (((!usage) || ((dev->usage[i] & ISDN_USAGE_MASK) == usage)) &&
|
|
(dev->drvmap[i] == di) &&
|
|
(dev->chanmap[i] == ch)) {
|
|
dev->usage[i] &= (ISDN_USAGE_NONE | ISDN_USAGE_EXCLUSIVE);
|
|
strcpy(dev->num[i], "???");
|
|
dev->ibytes[i] = 0;
|
|
dev->obytes[i] = 0;
|
|
// 20.10.99 JIM, try to reinitialize v110 !
|
|
dev->v110emu[i] = 0;
|
|
atomic_set(&(dev->v110use[i]), 0);
|
|
isdn_v110_close(dev->v110[i]);
|
|
dev->v110[i] = NULL;
|
|
// 20.10.99 JIM, try to reinitialize v110 !
|
|
isdn_info_update();
|
|
skb_queue_purge(&dev->drv[di]->rpqueue[ch]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Cancel Exclusive-Flag for ISDN-channel
|
|
*/
|
|
void
|
|
isdn_unexclusive_channel(int di, int ch)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++)
|
|
if ((dev->drvmap[i] == di) &&
|
|
(dev->chanmap[i] == ch)) {
|
|
dev->usage[i] &= ~ISDN_USAGE_EXCLUSIVE;
|
|
isdn_info_update();
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* writebuf replacement for SKB_ABLE drivers
|
|
*/
|
|
static int
|
|
isdn_writebuf_stub(int drvidx, int chan, const u_char __user * buf, int len)
|
|
{
|
|
int ret;
|
|
int hl = dev->drv[drvidx]->interface->hl_hdrlen;
|
|
struct sk_buff *skb = alloc_skb(hl + len, GFP_ATOMIC);
|
|
|
|
if (!skb)
|
|
return 0;
|
|
skb_reserve(skb, hl);
|
|
copy_from_user(skb_put(skb, len), buf, len);
|
|
ret = dev->drv[drvidx]->interface->writebuf_skb(drvidx, chan, 1, skb);
|
|
if (ret <= 0)
|
|
dev_kfree_skb(skb);
|
|
if (ret > 0)
|
|
dev->obytes[isdn_dc2minor(drvidx, chan)] += ret;
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Return: length of data on success, -ERRcode on failure.
|
|
*/
|
|
int
|
|
isdn_writebuf_skb_stub(int drvidx, int chan, int ack, struct sk_buff *skb)
|
|
{
|
|
int ret;
|
|
struct sk_buff *nskb = NULL;
|
|
int v110_ret = skb->len;
|
|
int idx = isdn_dc2minor(drvidx, chan);
|
|
|
|
if (dev->v110[idx]) {
|
|
atomic_inc(&dev->v110use[idx]);
|
|
nskb = isdn_v110_encode(dev->v110[idx], skb);
|
|
atomic_dec(&dev->v110use[idx]);
|
|
if (!nskb)
|
|
return 0;
|
|
v110_ret = *((int *)nskb->data);
|
|
skb_pull(nskb, sizeof(int));
|
|
if (!nskb->len) {
|
|
dev_kfree_skb(nskb);
|
|
return v110_ret;
|
|
}
|
|
/* V.110 must always be acknowledged */
|
|
ack = 1;
|
|
ret = dev->drv[drvidx]->interface->writebuf_skb(drvidx, chan, ack, nskb);
|
|
} else {
|
|
int hl = dev->drv[drvidx]->interface->hl_hdrlen;
|
|
|
|
if( skb_headroom(skb) < hl ){
|
|
/*
|
|
* This should only occur when new HL driver with
|
|
* increased hl_hdrlen was loaded after netdevice
|
|
* was created and connected to the new driver.
|
|
*
|
|
* The V.110 branch (re-allocates on its own) does
|
|
* not need this
|
|
*/
|
|
struct sk_buff * skb_tmp;
|
|
|
|
skb_tmp = skb_realloc_headroom(skb, hl);
|
|
printk(KERN_DEBUG "isdn_writebuf_skb_stub: reallocating headroom%s\n", skb_tmp ? "" : " failed");
|
|
if (!skb_tmp) return -ENOMEM; /* 0 better? */
|
|
ret = dev->drv[drvidx]->interface->writebuf_skb(drvidx, chan, ack, skb_tmp);
|
|
if( ret > 0 ){
|
|
dev_kfree_skb(skb);
|
|
} else {
|
|
dev_kfree_skb(skb_tmp);
|
|
}
|
|
} else {
|
|
ret = dev->drv[drvidx]->interface->writebuf_skb(drvidx, chan, ack, skb);
|
|
}
|
|
}
|
|
if (ret > 0) {
|
|
dev->obytes[idx] += ret;
|
|
if (dev->v110[idx]) {
|
|
atomic_inc(&dev->v110use[idx]);
|
|
dev->v110[idx]->skbuser++;
|
|
atomic_dec(&dev->v110use[idx]);
|
|
/* For V.110 return unencoded data length */
|
|
ret = v110_ret;
|
|
/* if the complete frame was send we free the skb;
|
|
if not upper function will requeue the skb */
|
|
if (ret == skb->len)
|
|
dev_kfree_skb(skb);
|
|
}
|
|
} else
|
|
if (dev->v110[idx])
|
|
dev_kfree_skb(nskb);
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
isdn_add_channels(isdn_driver_t *d, int drvidx, int n, int adding)
|
|
{
|
|
int j, k, m;
|
|
|
|
init_waitqueue_head(&d->st_waitq);
|
|
if (d->flags & DRV_FLAG_RUNNING)
|
|
return -1;
|
|
if (n < 1) return 0;
|
|
|
|
m = (adding) ? d->channels + n : n;
|
|
|
|
if (dev->channels + n > ISDN_MAX_CHANNELS) {
|
|
printk(KERN_WARNING "register_isdn: Max. %d channels supported\n",
|
|
ISDN_MAX_CHANNELS);
|
|
return -1;
|
|
}
|
|
|
|
if ((adding) && (d->rcverr))
|
|
kfree(d->rcverr);
|
|
if (!(d->rcverr = kmalloc(sizeof(int) * m, GFP_ATOMIC))) {
|
|
printk(KERN_WARNING "register_isdn: Could not alloc rcverr\n");
|
|
return -1;
|
|
}
|
|
memset((char *) d->rcverr, 0, sizeof(int) * m);
|
|
|
|
if ((adding) && (d->rcvcount))
|
|
kfree(d->rcvcount);
|
|
if (!(d->rcvcount = kmalloc(sizeof(int) * m, GFP_ATOMIC))) {
|
|
printk(KERN_WARNING "register_isdn: Could not alloc rcvcount\n");
|
|
if (!adding)
|
|
kfree(d->rcverr);
|
|
return -1;
|
|
}
|
|
memset((char *) d->rcvcount, 0, sizeof(int) * m);
|
|
|
|
if ((adding) && (d->rpqueue)) {
|
|
for (j = 0; j < d->channels; j++)
|
|
skb_queue_purge(&d->rpqueue[j]);
|
|
kfree(d->rpqueue);
|
|
}
|
|
if (!(d->rpqueue = kmalloc(sizeof(struct sk_buff_head) * m, GFP_ATOMIC))) {
|
|
printk(KERN_WARNING "register_isdn: Could not alloc rpqueue\n");
|
|
if (!adding) {
|
|
kfree(d->rcvcount);
|
|
kfree(d->rcverr);
|
|
}
|
|
return -1;
|
|
}
|
|
for (j = 0; j < m; j++) {
|
|
skb_queue_head_init(&d->rpqueue[j]);
|
|
}
|
|
|
|
if ((adding) && (d->rcv_waitq))
|
|
kfree(d->rcv_waitq);
|
|
d->rcv_waitq = kmalloc(sizeof(wait_queue_head_t) * 2 * m, GFP_ATOMIC);
|
|
if (!d->rcv_waitq) {
|
|
printk(KERN_WARNING "register_isdn: Could not alloc rcv_waitq\n");
|
|
if (!adding) {
|
|
kfree(d->rpqueue);
|
|
kfree(d->rcvcount);
|
|
kfree(d->rcverr);
|
|
}
|
|
return -1;
|
|
}
|
|
d->snd_waitq = d->rcv_waitq + m;
|
|
for (j = 0; j < m; j++) {
|
|
init_waitqueue_head(&d->rcv_waitq[j]);
|
|
init_waitqueue_head(&d->snd_waitq[j]);
|
|
}
|
|
|
|
dev->channels += n;
|
|
for (j = d->channels; j < m; j++)
|
|
for (k = 0; k < ISDN_MAX_CHANNELS; k++)
|
|
if (dev->chanmap[k] < 0) {
|
|
dev->chanmap[k] = j;
|
|
dev->drvmap[k] = drvidx;
|
|
break;
|
|
}
|
|
d->channels = m;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Low-level-driver registration
|
|
*/
|
|
|
|
static void
|
|
set_global_features(void)
|
|
{
|
|
int drvidx;
|
|
|
|
dev->global_features = 0;
|
|
for (drvidx = 0; drvidx < ISDN_MAX_DRIVERS; drvidx++) {
|
|
if (!dev->drv[drvidx])
|
|
continue;
|
|
if (dev->drv[drvidx]->interface)
|
|
dev->global_features |= dev->drv[drvidx]->interface->features;
|
|
}
|
|
}
|
|
|
|
#ifdef CONFIG_ISDN_DIVERSION
|
|
|
|
static char *map_drvname(int di)
|
|
{
|
|
if ((di < 0) || (di >= ISDN_MAX_DRIVERS))
|
|
return(NULL);
|
|
return(dev->drvid[di]); /* driver name */
|
|
} /* map_drvname */
|
|
|
|
static int map_namedrv(char *id)
|
|
{ int i;
|
|
|
|
for (i = 0; i < ISDN_MAX_DRIVERS; i++)
|
|
{ if (!strcmp(dev->drvid[i],id))
|
|
return(i);
|
|
}
|
|
return(-1);
|
|
} /* map_namedrv */
|
|
|
|
int DIVERT_REG_NAME(isdn_divert_if *i_div)
|
|
{
|
|
if (i_div->if_magic != DIVERT_IF_MAGIC)
|
|
return(DIVERT_VER_ERR);
|
|
switch (i_div->cmd)
|
|
{
|
|
case DIVERT_CMD_REL:
|
|
if (divert_if != i_div)
|
|
return(DIVERT_REL_ERR);
|
|
divert_if = NULL; /* free interface */
|
|
return(DIVERT_NO_ERR);
|
|
|
|
case DIVERT_CMD_REG:
|
|
if (divert_if)
|
|
return(DIVERT_REG_ERR);
|
|
i_div->ll_cmd = isdn_command; /* set command function */
|
|
i_div->drv_to_name = map_drvname;
|
|
i_div->name_to_drv = map_namedrv;
|
|
divert_if = i_div; /* remember interface */
|
|
return(DIVERT_NO_ERR);
|
|
|
|
default:
|
|
return(DIVERT_CMD_ERR);
|
|
}
|
|
} /* DIVERT_REG_NAME */
|
|
|
|
EXPORT_SYMBOL(DIVERT_REG_NAME);
|
|
|
|
#endif /* CONFIG_ISDN_DIVERSION */
|
|
|
|
|
|
EXPORT_SYMBOL(register_isdn);
|
|
#ifdef CONFIG_ISDN_PPP
|
|
EXPORT_SYMBOL(isdn_ppp_register_compressor);
|
|
EXPORT_SYMBOL(isdn_ppp_unregister_compressor);
|
|
#endif
|
|
|
|
int
|
|
register_isdn(isdn_if * i)
|
|
{
|
|
isdn_driver_t *d;
|
|
int j;
|
|
ulong flags;
|
|
int drvidx;
|
|
|
|
if (dev->drivers >= ISDN_MAX_DRIVERS) {
|
|
printk(KERN_WARNING "register_isdn: Max. %d drivers supported\n",
|
|
ISDN_MAX_DRIVERS);
|
|
return 0;
|
|
}
|
|
if (!i->writebuf_skb) {
|
|
printk(KERN_WARNING "register_isdn: No write routine given.\n");
|
|
return 0;
|
|
}
|
|
if (!(d = kmalloc(sizeof(isdn_driver_t), GFP_KERNEL))) {
|
|
printk(KERN_WARNING "register_isdn: Could not alloc driver-struct\n");
|
|
return 0;
|
|
}
|
|
memset((char *) d, 0, sizeof(isdn_driver_t));
|
|
|
|
d->maxbufsize = i->maxbufsize;
|
|
d->pktcount = 0;
|
|
d->stavail = 0;
|
|
d->flags = DRV_FLAG_LOADED;
|
|
d->online = 0;
|
|
d->interface = i;
|
|
d->channels = 0;
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
for (drvidx = 0; drvidx < ISDN_MAX_DRIVERS; drvidx++)
|
|
if (!dev->drv[drvidx])
|
|
break;
|
|
if (isdn_add_channels(d, drvidx, i->channels, 0)) {
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
kfree(d);
|
|
return 0;
|
|
}
|
|
i->channels = drvidx;
|
|
i->rcvcallb_skb = isdn_receive_skb_callback;
|
|
i->statcallb = isdn_status_callback;
|
|
if (!strlen(i->id))
|
|
sprintf(i->id, "line%d", drvidx);
|
|
for (j = 0; j < drvidx; j++)
|
|
if (!strcmp(i->id, dev->drvid[j]))
|
|
sprintf(i->id, "line%d", drvidx);
|
|
dev->drv[drvidx] = d;
|
|
strcpy(dev->drvid[drvidx], i->id);
|
|
isdn_info_update();
|
|
dev->drivers++;
|
|
set_global_features();
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
*****************************************************************************
|
|
* And now the modules code.
|
|
*****************************************************************************
|
|
*/
|
|
|
|
static char *
|
|
isdn_getrev(const char *revision)
|
|
{
|
|
char *rev;
|
|
char *p;
|
|
|
|
if ((p = strchr(revision, ':'))) {
|
|
rev = p + 2;
|
|
p = strchr(rev, '$');
|
|
*--p = 0;
|
|
} else
|
|
rev = "???";
|
|
return rev;
|
|
}
|
|
|
|
/*
|
|
* Allocate and initialize all data, register modem-devices
|
|
*/
|
|
static int __init isdn_init(void)
|
|
{
|
|
int i;
|
|
char tmprev[50];
|
|
|
|
if (!(dev = (isdn_dev *) vmalloc(sizeof(isdn_dev)))) {
|
|
printk(KERN_WARNING "isdn: Could not allocate device-struct.\n");
|
|
return -EIO;
|
|
}
|
|
memset((char *) dev, 0, sizeof(isdn_dev));
|
|
init_timer(&dev->timer);
|
|
dev->timer.function = isdn_timer_funct;
|
|
spin_lock_init(&dev->lock);
|
|
spin_lock_init(&dev->timerlock);
|
|
#ifdef MODULE
|
|
dev->owner = THIS_MODULE;
|
|
#endif
|
|
init_MUTEX(&dev->sem);
|
|
init_waitqueue_head(&dev->info_waitq);
|
|
for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
|
|
dev->drvmap[i] = -1;
|
|
dev->chanmap[i] = -1;
|
|
dev->m_idx[i] = -1;
|
|
strcpy(dev->num[i], "???");
|
|
init_waitqueue_head(&dev->mdm.info[i].open_wait);
|
|
init_waitqueue_head(&dev->mdm.info[i].close_wait);
|
|
}
|
|
if (register_chrdev(ISDN_MAJOR, "isdn", &isdn_fops)) {
|
|
printk(KERN_WARNING "isdn: Could not register control devices\n");
|
|
vfree(dev);
|
|
return -EIO;
|
|
}
|
|
if ((isdn_tty_modem_init()) < 0) {
|
|
printk(KERN_WARNING "isdn: Could not register tty devices\n");
|
|
vfree(dev);
|
|
unregister_chrdev(ISDN_MAJOR, "isdn");
|
|
return -EIO;
|
|
}
|
|
#ifdef CONFIG_ISDN_PPP
|
|
if (isdn_ppp_init() < 0) {
|
|
printk(KERN_WARNING "isdn: Could not create PPP-device-structs\n");
|
|
isdn_tty_exit();
|
|
unregister_chrdev(ISDN_MAJOR, "isdn");
|
|
vfree(dev);
|
|
return -EIO;
|
|
}
|
|
#endif /* CONFIG_ISDN_PPP */
|
|
|
|
strcpy(tmprev, isdn_revision);
|
|
printk(KERN_NOTICE "ISDN subsystem Rev: %s/", isdn_getrev(tmprev));
|
|
strcpy(tmprev, isdn_tty_revision);
|
|
printk("%s/", isdn_getrev(tmprev));
|
|
strcpy(tmprev, isdn_net_revision);
|
|
printk("%s/", isdn_getrev(tmprev));
|
|
strcpy(tmprev, isdn_ppp_revision);
|
|
printk("%s/", isdn_getrev(tmprev));
|
|
strcpy(tmprev, isdn_audio_revision);
|
|
printk("%s/", isdn_getrev(tmprev));
|
|
strcpy(tmprev, isdn_v110_revision);
|
|
printk("%s", isdn_getrev(tmprev));
|
|
|
|
#ifdef MODULE
|
|
printk(" loaded\n");
|
|
#else
|
|
printk("\n");
|
|
#endif
|
|
isdn_info_update();
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Unload module
|
|
*/
|
|
static void __exit isdn_exit(void)
|
|
{
|
|
#ifdef CONFIG_ISDN_PPP
|
|
isdn_ppp_cleanup();
|
|
#endif
|
|
if (isdn_net_rmall() < 0) {
|
|
printk(KERN_WARNING "isdn: net-device busy, remove cancelled\n");
|
|
return;
|
|
}
|
|
isdn_tty_exit();
|
|
unregister_chrdev(ISDN_MAJOR, "isdn");
|
|
del_timer(&dev->timer);
|
|
/* call vfree with interrupts enabled, else it will hang */
|
|
vfree(dev);
|
|
printk(KERN_NOTICE "ISDN-subsystem unloaded\n");
|
|
}
|
|
|
|
module_init(isdn_init);
|
|
module_exit(isdn_exit);
|