685 lines
13 KiB
C
685 lines
13 KiB
C
#include "mISDNlib.h"
|
|
#include <stdio.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <pthread.h>
|
|
|
|
|
|
|
|
/* API library to use with /dev/mISDN */
|
|
|
|
typedef struct _mISDNdev {
|
|
struct _mISDNdev *prev;
|
|
struct _mISDNdev *next;
|
|
pthread_mutex_t rmutex;
|
|
pthread_mutex_t wmutex;
|
|
int Flags;
|
|
int fid;
|
|
int isize;
|
|
unsigned char *inbuf;
|
|
unsigned char *irp;
|
|
unsigned char *iend;
|
|
} mISDNdev_t;
|
|
|
|
#define FLG_mISDN_WRRD_ATOMIC 1
|
|
|
|
mISDNdev_t *devlist = NULL;
|
|
|
|
static pthread_mutex_t devlist_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
#define mISDN_DEVICE "/dev/mISDN"
|
|
|
|
#if 0
|
|
void
|
|
xxxxxxxxxx(void) {
|
|
if (devlist)
|
|
fprintf(stderr, "xxxxxxxxxx dev %p prev %p next %p\n", devlist, devlist->prev, devlist->next);
|
|
else
|
|
fprintf(stderr, "xxxxxxxxxx devlist %p\n", devlist);
|
|
}
|
|
#endif
|
|
|
|
int
|
|
mISDN_open(void)
|
|
{
|
|
int fid;
|
|
mISDNdev_t *dev;
|
|
|
|
|
|
if (0>(fid = open(mISDN_DEVICE, O_RDWR | O_NONBLOCK)))
|
|
return(fid);
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev = devlist;
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
if (dev) {
|
|
fprintf(stderr, "%s: device %d (%p) has allready fid(%d)\n",
|
|
__FUNCTION__, dev->fid, dev, fid);
|
|
close(fid);
|
|
errno = EBUSY;
|
|
return(-1);
|
|
}
|
|
dev = malloc(sizeof(mISDNdev_t));
|
|
if (!dev) {
|
|
close(fid);
|
|
errno = ENOMEM;
|
|
return(-1);
|
|
}
|
|
memset(dev, 0, sizeof(mISDNdev_t));
|
|
dev->fid = fid;
|
|
dev->isize = mISDN_INBUFFER_SIZE;
|
|
dev->inbuf = malloc(dev->isize);
|
|
if (!dev->inbuf) {
|
|
free(dev);
|
|
close(fid);
|
|
errno = ENOMEM;
|
|
return(-1);
|
|
}
|
|
dev->irp = dev->inbuf;
|
|
dev->iend = dev->inbuf;
|
|
pthread_mutex_init(&dev->rmutex, NULL);
|
|
pthread_mutex_init(&dev->wmutex, NULL);
|
|
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev->prev = devlist;
|
|
while(dev->prev && dev->prev->next)
|
|
dev->prev = dev->prev->next;
|
|
if (devlist)
|
|
dev->prev->next = dev;
|
|
else
|
|
devlist = dev;
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
return(fid);
|
|
}
|
|
|
|
int
|
|
mISDN_close(int fid)
|
|
{
|
|
mISDNdev_t *dev = devlist;
|
|
int ret;
|
|
|
|
pthread_mutex_lock(&devlist_lock);
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
|
|
if (!dev) {
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
errno = ENODEV;
|
|
return(-1);
|
|
}
|
|
if (dev->prev)
|
|
dev->prev->next = dev->next;
|
|
if (dev->next)
|
|
dev->next->prev = dev->prev;
|
|
if (devlist==dev)
|
|
devlist=dev->next;
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
if (dev->inbuf)
|
|
free(dev->inbuf);
|
|
dev->inbuf = NULL;
|
|
pthread_mutex_unlock(&dev->rmutex);
|
|
ret = pthread_mutex_destroy(&dev->rmutex);
|
|
if (ret)
|
|
fprintf(stderr, "%s: rmutex destroy returns %d\n",
|
|
__FUNCTION__, ret);
|
|
pthread_mutex_lock(&dev->wmutex);
|
|
pthread_mutex_unlock(&dev->wmutex);
|
|
ret = pthread_mutex_destroy(&dev->wmutex);
|
|
if (ret)
|
|
fprintf(stderr, "%s: wmutex destroy returns %d\n",
|
|
__FUNCTION__, ret);
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
free(dev);
|
|
return(close(fid));
|
|
}
|
|
|
|
static int
|
|
mISDN_remove_iframe(mISDNdev_t *dev, iframe_t *frm)
|
|
{
|
|
u_char *ep;
|
|
int len;
|
|
|
|
if (frm->len > 0)
|
|
len = mISDN_HEADER_LEN + frm->len;
|
|
else
|
|
len = mISDN_HEADER_LEN;
|
|
ep = (u_char *)frm;
|
|
ep += len;
|
|
if (ep >= dev->iend)
|
|
dev->iend = (u_char *)frm;
|
|
else {
|
|
memcpy(frm, ep, dev->iend - ep);
|
|
dev->iend -= len;
|
|
}
|
|
|
|
return(dev->iend - dev->irp);
|
|
}
|
|
|
|
int
|
|
mISDN_read(int fid, void *buf, size_t count, int utimeout) {
|
|
mISDNdev_t *dev;
|
|
int ret = 0, len, sel;
|
|
fd_set in;
|
|
iframe_t *ifr;
|
|
struct timeval tout;
|
|
#ifdef MUTEX_TIMELOCK
|
|
struct timespec ts;
|
|
#endif
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev = devlist;
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
if (!dev) {
|
|
errno = ENODEV;
|
|
return(-1);
|
|
}
|
|
if (utimeout != -1) {
|
|
tout.tv_sec = utimeout/1000000;
|
|
tout.tv_usec = utimeout%1000000;
|
|
#ifdef MUTEX_TIMELOCK
|
|
if (utimeout == 0) {
|
|
ret = pthread_mutex_trylock(&dev->rmutex);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_trylock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
} else {
|
|
#ifdef CLOCK_REALTIME
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
#else
|
|
{
|
|
struct timeval tv;
|
|
struct timezone tz;
|
|
|
|
gettimeofday(&tv,&tz);
|
|
TIMEVAL_TO_TIMESPEC(&tv,&ts);
|
|
}
|
|
#endif
|
|
ts.tv_sec += tout.tv_sec;
|
|
ts.tv_nsec += 1000*tout.tv_usec;
|
|
if (ts.tv_nsec > 1000000000L) {
|
|
ts.tv_sec++;
|
|
ts.tv_nsec -= 1000000000L;
|
|
}
|
|
ret = pthread_mutex_timedlock(&dev->rmutex, &ts);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_timedlock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
}
|
|
#else
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
#endif
|
|
} else
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
|
|
if (dev->Flags & FLG_mISDN_WRRD_ATOMIC) {
|
|
// fprintf(stderr, "%s: WRRD_ATOMIC try again\n", __FUNCTION__);
|
|
errno = EAGAIN;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
len = dev->iend - dev->irp;
|
|
if (!len) {
|
|
dev->irp = dev->iend = dev->inbuf;
|
|
pthread_mutex_unlock(&dev->rmutex);
|
|
FD_ZERO(&in);
|
|
FD_SET(fid, &in);
|
|
if (utimeout != -1) {
|
|
sel = select(fid + 1, &in, NULL, NULL, &tout);
|
|
} else
|
|
sel = select(fid + 1, &in, NULL, NULL, NULL);
|
|
if (sel<0) {
|
|
// fprintf(stderr, "%s: select err(%d)\n", __FUNCTION__, errno);
|
|
return(sel);
|
|
} else if (sel==0) {
|
|
// fprintf(stderr, "%s: select timed out\n", __FUNCTION__);
|
|
return(0);
|
|
}
|
|
if (FD_ISSET(fid, &in)) {
|
|
#ifdef MUTEX_TIMELOCK
|
|
if (!utimeout) {
|
|
ret = pthread_mutex_trylock(&dev->rmutex);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_trylock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
} else
|
|
#endif
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
len = dev->isize - (dev->iend - dev->irp);
|
|
if (len<=0) {
|
|
errno = ENOSPC;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
if (dev->Flags & FLG_mISDN_WRRD_ATOMIC) {
|
|
// fprintf(stderr, "%s: WRRD_ATOMIC try again\n", __FUNCTION__);
|
|
errno = EAGAIN;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
len = read(fid, dev->iend, len);
|
|
// fprintf(stderr, "%s: read %d\n", __FUNCTION__, len);
|
|
if (len <= 0) {
|
|
ret = len;
|
|
goto out;
|
|
}
|
|
dev->iend += len;
|
|
len = dev->iend - dev->irp;
|
|
} else {
|
|
return(0);
|
|
}
|
|
}
|
|
if (len < mISDN_HEADER_LEN) {
|
|
dev->iend = dev->irp;
|
|
fprintf(stderr, "%s: frame too short:%d\n",
|
|
__FUNCTION__, len);
|
|
errno = EINVAL;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
ifr = (iframe_t *)dev->irp;
|
|
if (ifr->len > 0) {
|
|
if ((ifr->len + mISDN_HEADER_LEN) > len) {
|
|
dev->iend = dev->irp;
|
|
errno = EINVAL;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
len = ifr->len + mISDN_HEADER_LEN;
|
|
} else
|
|
len = mISDN_HEADER_LEN;
|
|
if (len>count) {
|
|
errno = ENOSPC;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
memcpy(buf, dev->irp, len);
|
|
dev->irp += len;
|
|
ret = len;
|
|
out:
|
|
pthread_mutex_unlock(&dev->rmutex);
|
|
return(ret);
|
|
}
|
|
|
|
static iframe_t *
|
|
mISDN_find_iframe(mISDNdev_t *dev, u_int addr, u_int prim) {
|
|
iframe_t *frm;
|
|
u_char *rp;
|
|
|
|
rp = dev->irp;
|
|
while(rp<dev->iend) {
|
|
if ((dev->iend - rp) < mISDN_HEADER_LEN) {
|
|
return(NULL);
|
|
}
|
|
frm = (iframe_t *)rp;
|
|
if ((frm->addr == addr) && (frm->prim == prim))
|
|
return(frm);
|
|
if (frm->len > 0)
|
|
rp += mISDN_HEADER_LEN + frm->len;
|
|
else
|
|
rp += mISDN_HEADER_LEN;
|
|
}
|
|
return(NULL);
|
|
}
|
|
|
|
|
|
int
|
|
mISDN_read_frame(int fid, void *buf, size_t count, u_int addr, u_int msgtype,
|
|
int utimeout)
|
|
{
|
|
mISDNdev_t *dev;
|
|
int len, sel, first, ret = 0;
|
|
fd_set in;
|
|
iframe_t *ifr;
|
|
struct timeval tout;
|
|
#ifdef MUTEX_TIMELOCK
|
|
struct timespec ts;
|
|
#endif
|
|
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev = devlist;
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
if (!dev) {
|
|
errno = ENODEV;
|
|
return(-1);
|
|
}
|
|
if (utimeout != -1) {
|
|
tout.tv_sec = utimeout/1000000;
|
|
tout.tv_usec = utimeout%1000000;
|
|
#ifdef MUTEX_TIMELOCK
|
|
if (utimeout == 0) {
|
|
ret = pthread_mutex_trylock(&dev->rmutex);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_trylock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
} else {
|
|
#ifdef CLOCK_REALTIME
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
#else
|
|
{
|
|
struct timeval tv;
|
|
struct timezone tz;
|
|
|
|
gettimeofday(&tv,&tz);
|
|
TIMEVAL_TO_TIMESPEC(&tv,&ts);
|
|
}
|
|
#endif
|
|
ts.tv_sec += tout.tv_sec;
|
|
ts.tv_nsec += 1000*tout.tv_usec;
|
|
if (ts.tv_nsec > 1000000000L) {
|
|
ts.tv_sec++;
|
|
ts.tv_nsec -= 1000000000L;
|
|
}
|
|
ret = pthread_mutex_timedlock(&dev->rmutex, &ts);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_timedlock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
}
|
|
#else
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
#endif
|
|
} else
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
|
|
first = 1;
|
|
while((utimeout == -1) || tout.tv_sec || tout.tv_usec || first) {
|
|
if (!first || !(dev->iend - dev->irp)) {
|
|
FD_ZERO(&in);
|
|
FD_SET(fid, &in);
|
|
if (utimeout != -1)
|
|
sel = select(fid + 1, &in, NULL, NULL, &tout);
|
|
else
|
|
sel = select(fid + 1, &in, NULL, NULL, NULL);
|
|
if (sel<0) {
|
|
// fprintf(stderr, "%s: select err(%d)\n", __FUNCTION__, errno);
|
|
ret = sel;
|
|
goto out;
|
|
} else if (sel==0) {
|
|
// fprintf(stderr, "%s: select timed out\n", __FUNCTION__);
|
|
ret = 0;
|
|
goto out;
|
|
}
|
|
if (FD_ISSET(fid, &in)) {
|
|
len = dev->isize - (dev->iend - dev->irp);
|
|
if (len<=0) {
|
|
errno = ENOSPC;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
len = read(fid, dev->iend, len);
|
|
// fprintf(stderr, "%s: read %d\n", __FUNCTION__, len);
|
|
if (len <= 0) {
|
|
ret = len;
|
|
goto out;
|
|
}
|
|
dev->iend += len;
|
|
} else
|
|
continue;
|
|
}
|
|
if (dev->iend - dev->irp) {
|
|
ifr = mISDN_find_iframe(dev, addr, msgtype);
|
|
if (ifr) {
|
|
if (ifr->len > 0) {
|
|
#if 0
|
|
if ((ifr->len + mISDN_HEADER_LEN) > len) {
|
|
dev->irp = dev->iend;
|
|
errno = EINVAL;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
#endif
|
|
len = ifr->len + mISDN_HEADER_LEN;
|
|
} else
|
|
len = mISDN_HEADER_LEN;
|
|
if (len > count) {
|
|
errno = ENOSPC;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
memcpy(buf, ifr, len);
|
|
mISDN_remove_iframe(dev, ifr);
|
|
ret = len;
|
|
goto out;
|
|
}
|
|
}
|
|
first = 0;
|
|
}
|
|
out:
|
|
pthread_mutex_unlock(&dev->rmutex);
|
|
return(ret);
|
|
}
|
|
|
|
int
|
|
mISDN_write(int fid, void *buf, size_t count, int utimeout) {
|
|
mISDNdev_t *dev;
|
|
int len, sel;
|
|
fd_set out;
|
|
struct timeval tout;
|
|
#ifdef MUTEX_TIMELOCK
|
|
struct timespec ts;
|
|
int ret;
|
|
#endif
|
|
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev = devlist;
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
if (!dev) {
|
|
errno = ENODEV;
|
|
return(-1);
|
|
}
|
|
FD_ZERO(&out);
|
|
FD_SET(fid, &out);
|
|
if (utimeout != -1) {
|
|
tout.tv_sec = utimeout/1000000;
|
|
tout.tv_usec = utimeout%1000000;
|
|
sel = select(fid + 1, NULL, &out, NULL, &tout);
|
|
} else
|
|
sel = select(fid + 1, NULL, &out, NULL, NULL);
|
|
if (sel<=0)
|
|
return(sel);
|
|
if (!FD_ISSET(fid, &out))
|
|
return(0);
|
|
if (utimeout != -1) {
|
|
#ifdef MUTEX_TIMELOCK
|
|
if (utimeout == 0) {
|
|
ret = pthread_mutex_trylock(&dev->wmutex);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_trylock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
} else {
|
|
#ifdef CLOCK_REALTIME
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
#else
|
|
{
|
|
struct timeval tv;
|
|
struct timezone tz;
|
|
|
|
gettimeofday(&tv,&tz);
|
|
TIMEVAL_TO_TIMESPEC(&tv,&ts);
|
|
}
|
|
#endif
|
|
ts.tv_sec += tout.tv_sec;
|
|
ts.tv_nsec += 1000*tout.tv_usec;
|
|
if (ts.tv_nsec > 1000000000L) {
|
|
ts.tv_sec++;
|
|
ts.tv_nsec -= 1000000000L;
|
|
}
|
|
ret = pthread_mutex_timedlock(&dev->wmutex, &ts);
|
|
if (ret) {
|
|
fprintf(stderr, "%s: mutex_timedlock (%d)\n",
|
|
__FUNCTION__, ret);
|
|
errno = ret;
|
|
return(-1);
|
|
}
|
|
}
|
|
#else
|
|
pthread_mutex_lock(&dev->wmutex);
|
|
#endif
|
|
} else
|
|
pthread_mutex_lock(&dev->wmutex);
|
|
len = write(fid, buf, count);
|
|
pthread_mutex_unlock(&dev->wmutex);
|
|
return(len);
|
|
}
|
|
|
|
int
|
|
mISDN_write_frame(int fid, void *fbuf, u_int addr, u_int msgtype,
|
|
int dinfo, int dlen, void *dbuf, int utimeout)
|
|
{
|
|
iframe_t *ifr = fbuf;
|
|
int len = mISDN_HEADER_LEN;
|
|
int ret;
|
|
|
|
if (!fbuf) {
|
|
errno = EINVAL;
|
|
return(-1);
|
|
}
|
|
if ((dlen > 0) && !dbuf) {
|
|
errno = EINVAL;
|
|
return(-1);
|
|
}
|
|
ifr->addr = addr;
|
|
ifr->prim = msgtype;
|
|
ifr->dinfo= dinfo;
|
|
ifr->len = dlen;
|
|
if (dlen>0) {
|
|
len += dlen;
|
|
memcpy(&ifr->data.i, dbuf, dlen);
|
|
}
|
|
ret = mISDN_write(fid, ifr, len, utimeout);
|
|
if (ret == len)
|
|
ret = 0;
|
|
else if (ret>=0) {
|
|
errno = ENOSPC;
|
|
ret = -1;
|
|
}
|
|
return(ret);
|
|
}
|
|
|
|
int
|
|
mISDN_select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
|
|
struct timeval *timeout)
|
|
{
|
|
mISDNdev_t *dev = devlist;
|
|
|
|
if (readfds) {
|
|
pthread_mutex_lock(&devlist_lock);
|
|
while(dev) {
|
|
if (FD_ISSET(dev->fid, readfds)) {
|
|
if (dev->iend - dev->irp) {
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
FD_ZERO(readfds);
|
|
FD_SET(dev->fid, readfds);
|
|
if (writefds)
|
|
FD_ZERO(writefds);
|
|
if (exceptfds)
|
|
FD_ZERO(exceptfds);
|
|
return(1);
|
|
}
|
|
}
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
}
|
|
|
|
return(select(n, readfds, writefds, exceptfds, timeout));
|
|
}
|
|
|
|
int
|
|
set_wrrd_atomic(int fid)
|
|
{
|
|
mISDNdev_t *dev;
|
|
int ret;
|
|
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev = devlist;
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
if (!dev) {
|
|
return(-1);
|
|
}
|
|
pthread_mutex_lock(&dev->rmutex);
|
|
if (dev->Flags & FLG_mISDN_WRRD_ATOMIC)
|
|
ret = 1;
|
|
else {
|
|
ret = 0;
|
|
dev->Flags |= FLG_mISDN_WRRD_ATOMIC;
|
|
}
|
|
pthread_mutex_unlock(&dev->rmutex);
|
|
return(ret);
|
|
}
|
|
|
|
int
|
|
clear_wrrd_atomic(int fid)
|
|
{
|
|
mISDNdev_t *dev;
|
|
int ret;
|
|
|
|
pthread_mutex_lock(&devlist_lock);
|
|
dev = devlist;
|
|
while(dev) {
|
|
if (dev->fid==fid)
|
|
break;
|
|
dev = dev->next;
|
|
}
|
|
pthread_mutex_unlock(&devlist_lock);
|
|
if (!dev) {
|
|
return(-1);
|
|
}
|
|
if (dev->Flags & FLG_mISDN_WRRD_ATOMIC) {
|
|
dev->Flags &= ~FLG_mISDN_WRRD_ATOMIC;
|
|
ret = 0;
|
|
} else {
|
|
ret = 1;
|
|
}
|
|
return(ret);
|
|
}
|