patch-pre2.0.6 linux/drivers/isdn/isdn_ppp.c
Next file: linux/drivers/isdn/isdn_ppp.h
Previous file: linux/drivers/isdn/isdn_net.c
Back to the patch index
Back to the overall index
- Lines: 621
- Date:
Sun May 19 15:29:29 1996
- Orig file:
pre2.0.5/linux/drivers/isdn/isdn_ppp.c
- Orig date:
Tue May 7 16:22:26 1996
diff -u --recursive --new-file pre2.0.5/linux/drivers/isdn/isdn_ppp.c linux/drivers/isdn/isdn_ppp.c
@@ -1,4 +1,4 @@
-/* $Id: isdn_ppp.c,v 1.5 1996/04/20 16:32:32 fritz Exp $
+/* $Id: isdn_ppp.c,v 1.9 1996/05/18 01:37:01 fritz Exp $
*
* Linux ISDN subsystem, functions for synchronous PPP (linklevel).
*
@@ -19,6 +19,19 @@
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Log: isdn_ppp.c,v $
+ * Revision 1.9 1996/05/18 01:37:01 fritz
+ * Added spelling corrections and some minor changes
+ * to stay in sync with kernel.
+ *
+ * Revision 1.8 1996/05/06 11:34:55 hipp
+ * fixed a few bugs
+ *
+ * Revision 1.7 1996/04/30 11:07:42 fritz
+ * Added Michael's ippp-bind patch.
+ *
+ * Revision 1.6 1996/04/30 09:33:09 fritz
+ * Removed compatibility-macros.
+ *
* Revision 1.5 1996/04/20 16:32:32 fritz
* Changed ippp_table to an array of pointers, allocating each part
* separately.
@@ -43,9 +56,7 @@
/* TODO: right tbusy handling when using MP */
-#ifndef STANDALONE
#include <linux/config.h>
-#endif
#define __NO_VERSION__
#include <linux/module.h>
#include <linux/isdn.h>
@@ -58,8 +69,8 @@
#endif
/* Prototypes */
-static int isdn_ppp_fill_rq(char *buf, int len, int minor);
-static int isdn_ppp_hangup(int);
+static int isdn_ppp_fill_rq(char *buf, int len,int proto, int minor);
+static int isdn_ppp_closewait(int);
static void isdn_ppp_push_higher(isdn_net_dev * net_dev, isdn_net_local * lp,
struct sk_buff *skb, int proto);
static int isdn_ppp_if_get_unit(char **namebuf);
@@ -72,47 +83,53 @@
int BEbyte, int *sqno, int min_sqno);
#endif
-char *isdn_ppp_revision = "$Revision: 1.5 $";
+char *isdn_ppp_revision = "$Revision: 1.9 $";
struct ippp_struct *ippp_table[ISDN_MAX_CHANNELS];
extern int isdn_net_force_dial_lp(isdn_net_local *);
-int isdn_ppp_free(isdn_net_local * lp)
+/*
+ * unbind isdn_net_local <=> ippp-device
+ * note: it can happen, that we hangup/free the master before the slaves
+ */
+int isdn_ppp_free(isdn_net_local *lp)
{
+ isdn_net_local *master_lp=lp;
+
if (lp->ppp_minor < 0)
return 0;
#ifdef CONFIG_ISDN_MPP
if(lp->master)
- {
- isdn_net_dev *p = dev->netdev;
- lp->last->next = lp->next;
- lp->next->last = lp->last;
- if(lp->netdev->queue == lp)
- lp->netdev->queue = lp->next;
- lp->next = lp->last = lp;
- while(p) {
- if(lp == &p->local) {
- lp->netdev = p;
- break;
- }
- p=p->next;
+ master_lp = (isdn_net_local *) lp->master->priv;
+
+ lp->last->next = lp->next;
+ lp->next->last = lp->last;
+ if(master_lp->netdev->queue == lp) {
+ master_lp->netdev->queue = lp->next;
+ if(lp->next == lp) { /* last link in queue? */
+ master_lp->netdev->ib.bundled = 0;
+ isdn_ppp_free_mpqueue(master_lp->netdev);
+ isdn_ppp_free_sqqueue(master_lp->netdev);
}
- } else {
- lp->netdev->ib.bundled = 0;
- /* last link: free mpqueue, free sqqueue ? */
}
-
+ lp->next = lp->last = lp; /* (re)set own pointers */
#endif
- isdn_ppp_hangup(lp->ppp_minor);
-#if 0
- printk(KERN_DEBUG "isdn_ppp_free %d %lx %lx\n", lp->ppp_minor, (long) lp,(long) ippp_table[lp->ppp_minor]->lp);
-#endif
- ippp_table[lp->ppp_minor]->lp = NULL;
+ isdn_ppp_closewait(lp->ppp_minor); /* force wakeup on ippp device */
+
+ if(ippp_table[lp->ppp_minor]->debug & 0x1)
+ printk(KERN_DEBUG "isdn_ppp_free %d %lx %lx\n", lp->ppp_minor, (long) lp,(long) ippp_table[lp->ppp_minor]->lp);
+
+ ippp_table[lp->ppp_minor]->lp = NULL; /* link is down .. set lp to NULL */
+ lp->ppp_minor = -1; /* is this OK ?? */
+
return 0;
}
+/*
+ * bind isdn_net_local <=> ippp-device
+ */
int isdn_ppp_bind(isdn_net_local * lp)
{
int i;
@@ -126,16 +143,31 @@
save_flags(flags);
cli();
- /*
- * search a free device
- */
- for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
- if (ippp_table[i]->state == IPPP_OPEN) { /* OPEN, but not connected! */
-#if 0
- printk(KERN_DEBUG "find_minor, %d lp: %08lx\n", i, (long) lp);
-#endif
- break;
+ if(lp->pppbind < 0) /* device bounded to ippp device ? */
+ {
+ isdn_net_dev *net_dev = dev->netdev;
+ char exclusive[ISDN_MAX_CHANNELS]; /* exclusive flags */
+ memset(exclusive,0,ISDN_MAX_CHANNELS);
+ while (net_dev) { /* step through net devices to find exclusive minors */
+ isdn_net_local *lp = &net_dev->local;
+ if(lp->pppbind >= 0)
+ exclusive[lp->pppbind] = 1;
+ net_dev = net_dev->next;
}
+ /*
+ * search a free device
+ */
+ for (i = 0; i < ISDN_MAX_CHANNELS; i++) {
+ if (ippp_table[i]->state == IPPP_OPEN && !exclusive[i]) { /* OPEN, but not connected! */
+ break;
+ }
+ }
+ }
+ else {
+ if (ippp_table[lp->pppbind]->state == IPPP_OPEN) /* OPEN, but not connected! */
+ i = lp->pppbind;
+ else
+ i = ISDN_MAX_CHANNELS; /* trigger error */
}
if (i >= ISDN_MAX_CHANNELS) {
@@ -163,7 +195,12 @@
return lp->ppp_minor;
}
-static int isdn_ppp_hangup(int minor)
+/*
+ * there was a hangup on the netdevice
+ * force wakeup of the ippp device
+ * go into 'device waits for release' state
+ */
+static int isdn_ppp_closewait(int minor)
{
if (minor < 0 || minor >= ISDN_MAX_CHANNELS)
return 0;
@@ -181,9 +218,8 @@
int isdn_ppp_open(int minor, struct file *file)
{
-#if 0
- printk(KERN_DEBUG "ippp, open, minor: %d state: %04x\n", minor,ippp_table[minor]->state);
-#endif
+ if(ippp_table[minor]->debug & 0x1)
+ printk(KERN_DEBUG "ippp, open, minor: %d state: %04x\n", minor,ippp_table[minor]->state);
if (ippp_table[minor]->state)
return -EBUSY;
@@ -219,6 +255,9 @@
return 0;
}
+/*
+ * release ippp device
+ */
void isdn_ppp_release(int minor, struct file *file)
{
int i;
@@ -226,29 +265,24 @@
if (minor < 0 || minor >= ISDN_MAX_CHANNELS)
return;
-#if 0
- printk(KERN_DEBUG "ippp: release, minor: %d %lx\n", minor, (long) ippp_table[minor]->lp);
-#endif
+ if(ippp_table[minor]->debug & 0x1)
+ printk(KERN_DEBUG "ippp: release, minor: %d %lx\n", minor, (long) ippp_table[minor]->lp);
if (ippp_table[minor]->lp) { /* a lp address says: this link is still up */
isdn_net_dev *p = dev->netdev;
- while(p) { /* find interface for our lp; */
- if(&p->local == ippp_table[minor]->lp)
- break;
- p = p->next;
- }
- if(!p) {
- printk(KERN_ERR "isdn_ppp_release: Can't find device for net_local\n");
- p = ippp_table[minor]->lp->netdev;
- }
+ p = ippp_table[minor]->lp->netdev;
ippp_table[minor]->lp->ppp_minor = -1;
- isdn_net_hangup(&p->dev); /* lp->ppp_minor==-1 => no calling of isdn_ppp_hangup() */
+ isdn_net_hangup(&p->dev); /* lp->ppp_minor==-1 => no calling of isdn_ppp_closewait() */
ippp_table[minor]->lp = NULL;
}
for (i = 0; i < NUM_RCV_BUFFS; i++) {
- if (ippp_table[minor]->rq[i].buf)
+ if (ippp_table[minor]->rq[i].buf) {
kfree(ippp_table[minor]->rq[i].buf);
+ ippp_table[minor]->rq[i].buf = NULL;
+ }
}
+ ippp_table[minor]->first = ippp_table[minor]->rq + NUM_RCV_BUFFS - 1; /* receive queue */
+ ippp_table[minor]->last = ippp_table[minor]->rq;
#ifdef CONFIG_ISDN_PPP_VJ
slhc_free(ippp_table[minor]->slcomp);
@@ -258,6 +292,9 @@
ippp_table[minor]->state = 0;
}
+/*
+ * get_arg .. ioctl helper
+ */
static int get_arg(void *b, unsigned long *val)
{
int r;
@@ -267,6 +304,9 @@
return 0;
}
+/*
+ * set arg .. ioctl helper
+ */
static int set_arg(void *b, unsigned long val)
{
int r;
@@ -276,15 +316,17 @@
return 0;
}
+/*
+ * ippp device ioctl
+ */
int isdn_ppp_ioctl(int minor, struct file *file, unsigned int cmd, unsigned long arg)
{
unsigned long val;
int r;
-#if 0
- printk(KERN_DEBUG "isdn_ppp_ioctl: minor: %d cmd: %x",minor,cmd);
- printk(KERN_DEBUG " state: %x\n",ippp_table[minor]->state);
-#endif
+ if(ippp_table[minor]->debug & 0x1)
+ printk(KERN_DEBUG "isdn_ppp_ioctl: minor: %d cmd: %x state: %x\n",
+ minor,cmd,ippp_table[minor]->state);
if (!(ippp_table[minor]->state & IPPP_OPEN))
return -EINVAL;
@@ -328,8 +370,9 @@
return r;
}
if (val & SC_ENABLE_IP && !(ippp_table[minor]->pppcfg & SC_ENABLE_IP)) {
- ippp_table[minor]->lp->netdev->dev.tbusy = 0;
- mark_bh(NET_BH); /* OK .. we are ready to send the first buffer */
+ isdn_net_local *lp = ippp_table[minor]->lp;
+ lp->netdev->dev.tbusy = 0;
+ mark_bh(NET_BH); /* OK .. we are ready to send buffers */
}
ippp_table[minor]->pppcfg = val;
break;
@@ -354,8 +397,13 @@
ippp_table[minor]->maxcid = val;
break;
case PPPIOCGDEBUG:
+ if ((r = set_arg((void *) arg, ippp_table[minor]->debug)))
+ return r;
break;
case PPPIOCSDEBUG:
+ if ((r = get_arg((void *) arg, &val)))
+ return r;
+ ippp_table[minor]->debug = val;
break;
default:
break;
@@ -368,9 +416,8 @@
struct ippp_buf_queue *bf, *bl;
unsigned long flags;
-#if 0
- printk(KERN_DEBUG "isdn_ppp_select: minor: %d, type: %d \n",minor,type);
-#endif
+ if(ippp_table[minor]->debug & 0x2)
+ printk(KERN_DEBUG "isdn_ppp_select: minor: %d, type: %d \n",minor,type);
if (!(ippp_table[minor]->state & IPPP_OPEN))
return -EINVAL;
@@ -381,6 +428,9 @@
cli();
bl = ippp_table[minor]->last;
bf = ippp_table[minor]->first;
+ /*
+ * if IPPP_NOBLOCK is set we return even if we have nothing to read
+ */
if (bf->next == bl && !(ippp_table[minor]->state & IPPP_NOBLOCK)) {
select_wait(&ippp_table[minor]->wq, st);
restore_flags(flags);
@@ -403,7 +453,7 @@
* fill up isdn_ppp_read() queue ..
*/
-static int isdn_ppp_fill_rq(char *buf, int len, int minor)
+static int isdn_ppp_fill_rq(char *buf, int len,int proto, int minor)
{
struct ippp_buf_queue *bf, *bl;
unsigned long flags;
@@ -428,15 +478,19 @@
kfree(bf->buf);
ippp_table[minor]->first = bf;
}
- bl->buf = (char *) kmalloc(len, GFP_ATOMIC);
+ bl->buf = (char *) kmalloc(len+4, GFP_ATOMIC);
if (!bl->buf) {
printk(KERN_WARNING "ippp: Can't alloc buf\n");
restore_flags(flags);
return 0;
}
- bl->len = len;
+ bl->len = len+4;
- memcpy(bl->buf, buf, len);
+ bl->buf[0] = PPP_ALLSTATIONS;
+ bl->buf[1] = PPP_UI;
+ bl->buf[2] = proto >> 8;
+ bl->buf[3] = proto & 0xff;
+ memcpy(bl->buf+4, buf, len);
ippp_table[minor]->last = bl->next;
restore_flags(flags);
@@ -502,6 +556,7 @@
if (!lp)
printk(KERN_DEBUG "isdn_ppp_write: lp == NULL\n");
else {
+ lp->huptimer = 0;
if (lp->isdn_device < 0 || lp->isdn_channel < 0)
return 0;
@@ -526,11 +581,11 @@
if (!(ippp_table[i] = (struct ippp_struct *)
kmalloc(sizeof(struct ippp_struct), GFP_KERNEL))) {
printk(KERN_WARNING "isdn_ppp_init: Could not alloc ippp_table\n");
- for (j = 0; j < i; j++)
- kfree(ippp_table[i]);
+ for (j = 0; j < i; j++)
+ kfree(ippp_table[i]);
return -1;
}
- memset((char *) ippp_table[i], 0, sizeof(struct ippp_struct));
+ memset((char *) ippp_table[i], 0, sizeof(struct ippp_struct));
ippp_table[i]->state = 0;
ippp_table[i]->first = ippp_table[i]->rq + NUM_RCV_BUFFS - 1;
ippp_table[i]->last = ippp_table[i]->rq;
@@ -559,14 +614,15 @@
void isdn_ppp_receive(isdn_net_dev * net_dev, isdn_net_local * lp, struct sk_buff *skb)
{
-#if 0
- printk(KERN_DEBUG "recv, skb %d\n",skb->len);
-#endif
+ if(ippp_table[lp->ppp_minor]->debug & 0x4)
+ printk(KERN_DEBUG "recv skb, len: %ld\n",skb->len);
if(skb->data[0] == 0xff && skb->data[1] == 0x03)
skb_pull(skb,2);
- else if (ippp_table[lp->ppp_minor]->pppcfg & SC_REJ_COMP_AC)
+ else if (ippp_table[lp->ppp_minor]->pppcfg & SC_REJ_COMP_AC) {
+ dev_kfree_skb(skb,FREE_WRITE);
return; /* discard it silently */
+ }
#ifdef CONFIG_ISDN_MPP
if (!(ippp_table[lp->ppp_minor]->mpppcfg & SC_REJ_MP_PROT)) {
@@ -583,11 +639,10 @@
isdn_net_local *lpq;
int sqno, min_sqno, tseq;
u_char BEbyte = skb->data[0];
-#if 0
- printk(KERN_DEBUG "recv: %d/%04x/%d -> %02x %02x %02x %02x %02x %02x\n", lp->ppp_minor, proto ,
- (int) skb->len, (int) skb->data[0], (int) skb->data[1], (int) skb->data[2],
- (int) skb->data[3], (int) skb->data[4], (int) skb->data[5]);
-#endif
+ if(ippp_table[lp->ppp_minor]->debug & 0x8)
+ printk(KERN_DEBUG "recv: %d/%04x/%d -> %02x %02x %02x %02x %02x %02x\n", lp->ppp_minor, proto ,
+ (int) skb->len, (int) skb->data[0], (int) skb->data[1], (int) skb->data[2],
+ (int) skb->data[3], (int) skb->data[4], (int) skb->data[5]);
if (!(ippp_table[lp->ppp_minor]->mpppcfg & SC_IN_SHORT_SEQ)) {
sqno = ((int) skb->data[1] << 16) + ((int) skb->data[2] << 8) + (int) skb->data[3];
skb_pull(skb,4);
@@ -657,8 +712,9 @@
q = (struct sqqueue *) kmalloc(sizeof(struct sqqueue), GFP_ATOMIC);
if (!q) {
- printk(KERN_WARNING "ippp: err, no memory !!\n");
net_dev->ib.modify = 0;
+ printk(KERN_WARNING "ippp/MPPP: Bad! Can't alloc sq node!\n");
+ dev_kfree_skb(skb,FREE_WRITE);
return; /* discard */
}
q->skb = skb;
@@ -726,9 +782,8 @@
}
}
-#if 0
- printk(KERN_DEBUG "push, skb %d %04x\n",skb->len,proto);
-#endif
+ if(ippp_table[lp->ppp_minor]->debug & 0x10)
+ printk(KERN_DEBUG "push, skb %ld %04x\n",skb->len,proto);
switch (proto) {
case PPP_IPX: /* untested */
@@ -755,6 +810,7 @@
if (!skb) {
printk(KERN_WARNING "%s: Memory squeeze, dropping packet.\n", dev->name);
net_dev->local.stats.rx_dropped++;
+ dev_kfree_skb(skb_old,FREE_WRITE);
return;
}
skb->dev = dev;
@@ -770,16 +826,12 @@
#else
printk(KERN_INFO "isdn: Ooopsa .. VJ-Compression support not compiled into isdn driver.\n");
lp->stats.rx_dropped++;
+ dev_kfree_skb(skb,FREE_WRITE);
return;
#endif
break;
default:
- skb_push(skb,4);
- skb->data[0] = 0xff;
- skb->data[1] = 0x03;
- skb->data[2] = (proto>>8);
- skb->data[3] = proto & 0xff;
- isdn_ppp_fill_rq(skb->data, skb->len, lp->ppp_minor); /* push data to pppd device */
+ isdn_ppp_fill_rq(skb->data, skb->len,proto, lp->ppp_minor); /* push data to pppd device */
dev_kfree_skb(skb,FREE_WRITE);
return;
}
@@ -793,19 +845,32 @@
}
/*
- * send ppp frame .. we expect a PIDCOMPable proto --
+ * send ppp frame .. we expect a PIDCOMPressable proto --
* (here: currently always PPP_IP,PPP_VJC_COMP,PPP_VJC_UNCOMP)
*/
int isdn_ppp_xmit(struct sk_buff *skb, struct device *dev)
{
- isdn_net_dev *nd = ((isdn_net_local *) dev->priv)->netdev;
- isdn_net_local *lp = nd->queue;
+ struct device *mdev = ((isdn_net_local *) (dev->priv) )->master; /* get master (for redundancy) */
+ isdn_net_local *lp,*mlp;
+ isdn_net_dev *nd;
int proto = PPP_IP; /* 0x21 */
- struct ippp_struct *ipt = ippp_table[lp->ppp_minor];
-#if defined(CONFIG_ISDN_PPP_VJ) || defined(CONFIG_ISDN_MPP)
- struct ippp_struct *ipts = ippp_table[lp->netdev->local.ppp_minor];
-#endif
+ struct ippp_struct *ipt,*ipts;
+ if(mdev)
+ mlp = (isdn_net_local *) (mdev->priv);
+ else
+ mlp = (isdn_net_local *) (dev->priv);
+ nd = mlp->netdev; /* get master lp */
+ lp = nd->queue; /* get lp on top of queue */
+ ipt = ippp_table[lp->ppp_minor];
+ ipts = ippp_table[mlp->ppp_minor];
+
+ if (!(ipt->pppcfg & SC_ENABLE_IP)) { /* PPP connected ? */
+ printk(KERN_INFO "isdn, xmit: Packet blocked: %d %d\n", lp->isdn_device, lp->isdn_channel);
+ return 1;
+ }
+ lp->huptimer = 0;
+
/* If packet is to be resent, it has already been processed and
* therefore its first bytes are already initialized. In this case
* send it immediately ...
@@ -817,17 +882,16 @@
/* future: step to next 'lp' when this lp is 'tbusy' */
-#if 0
- printk(KERN_DEBUG "xmit, skb %d\n",skb->len);
-#endif
+ if(ippp_table[lp->ppp_minor]->debug & 0x4)
+ printk(KERN_DEBUG "xmit skb, len %ld\n",skb->len);
#ifdef CONFIG_ISDN_PPP_VJ
- if (ipt->pppcfg & SC_COMP_TCP) {
+ if (ipt->pppcfg & SC_COMP_TCP) { /* ipt or ipts ? -> check this again! */
u_char *buf = skb->data;
int pktlen;
int len = 4;
#ifdef CONFIG_ISDN_MPP
- if (ipt->mpppcfg & SC_MP_PROT) /* sigh */
+ if (ipt->mpppcfg & SC_MP_PROT) /* sigh */ /* ipt or ipts ?? */
if (ipt->mpppcfg & SC_OUT_SHORT_SEQ)
len += 3;
else
@@ -851,9 +915,8 @@
}
#endif
-#if 0
- printk(KERN_DEBUG "xmit, skb %d %04x\n",skb->len,proto);
-#endif
+ if(ippp_table[lp->ppp_minor]->debug & 0x24)
+ printk(KERN_DEBUG "xmit2 skb, len %ld, proto %04x\n",skb->len,proto);
#ifdef CONFIG_ISDN_MPP
if (ipt->mpppcfg & SC_MP_PROT) {
@@ -883,15 +946,25 @@
skb->data[2] = proto >> 8;
skb->data[3] = proto & 0xff;
- lp->huptimer = 0;
- if (!(ipt->pppcfg & SC_ENABLE_IP)) { /* PPP connected ? */
- printk(KERN_INFO "isdn, xmit: Packet blocked: %d %d\n", lp->isdn_device, lp->isdn_channel);
- return 1;
- }
/* tx-stats are now updated via BSENT-callback */
return (isdn_net_send_skb(dev , lp , skb));
}
+void isdn_ppp_free_sqqueue(isdn_net_dev * p)
+{
+ struct sqqueue *q = p->ib.sq;
+
+ p->ib.sq = NULL;
+ while(q) {
+ struct sqqueue *qn = q->next;
+ if(q->skb)
+ dev_kfree_skb(q->skb,FREE_WRITE);
+ kfree(q);
+ q = qn;
+ }
+
+}
+
void isdn_ppp_free_mpqueue(isdn_net_dev * p)
{
struct mpqueue *ql, *q = p->mp_last;
@@ -932,8 +1005,6 @@
nlp->next = lp;
p->queue = nlp;
- nlp->netdev = lp->netdev;
-
ippp_table[nlp->ppp_minor]->unit = ippp_table[lp->ppp_minor]->unit;
/* maybe also SC_CCP stuff */
ippp_table[nlp->ppp_minor]->pppcfg |= ippp_table[lp->ppp_minor]->pppcfg &
@@ -1239,4 +1310,34 @@
#endif
}
+int isdn_ppp_hangup_slave(char *name)
+{
+#ifdef CONFIG_ISDN_MPP
+ isdn_net_dev *ndev;
+ isdn_net_local *lp;
+ struct device *sdev;
+
+ if(!(ndev = isdn_net_findif(name)))
+ return 1;
+ lp = &ndev->local;
+ if(!(lp->flags & ISDN_NET_CONNECTED))
+ return 5;
+
+ sdev = lp->slave;
+ while(sdev)
+ {
+ isdn_net_local *mlp = (isdn_net_local *) sdev->priv;
+ if((mlp->flags & ISDN_NET_CONNECTED))
+ break;
+ sdev = mlp->slave;
+ }
+ if(!sdev)
+ return 2;
+
+ isdn_net_hangup(sdev);
+ return 0;
+#else
+ return -1;
+#endif
+}
FUNET's LINUX-ADM group, linux-adm@nic.funet.fi
TCL-scripts by Sam Shen, slshen@lbl.gov
with Sam's (original) version of this