Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 1 Nov 2012 16:54:26 +0000 (UTC)
From:      Hans Petter Selasky <hselasky@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r242438 - head/sys/dev/sound/usb
Message-ID:  <201211011654.qA1GsQiF035570@svn.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: hselasky
Date: Thu Nov  1 16:54:26 2012
New Revision: 242438
URL: http://svn.freebsd.org/changeset/base/242438

Log:
  Export all mixer nodes into dev.pcm.X.mixer.Y sysctl nodes, hence the
  PCM API doesn't support showing all the knobs. Make sure all the USB audio
  mixer nodes are freed at detach. Before this patch USB audio would leak
  some memory at detach. Print out buffer sizes in number of samples at attach.
  Fix setting of volume controls when the number of channels is greater than two.
  
  MFC after:	1 week

Modified:
  head/sys/dev/sound/usb/uaudio.c

Modified: head/sys/dev/sound/usb/uaudio.c
==============================================================================
--- head/sys/dev/sound/usb/uaudio.c	Thu Nov  1 16:52:00 2012	(r242437)
+++ head/sys/dev/sound/usb/uaudio.c	Thu Nov  1 16:54:26 2012	(r242438)
@@ -71,6 +71,7 @@ __FBSDID("$FreeBSD$");
 #include <dev/usb/usb.h>
 #include <dev/usb/usbdi.h>
 #include <dev/usb/usbdi_util.h>
+#include <dev/usb/usb_request.h>
 
 #define	USB_DEBUG_VAR uaudio_debug
 #include <dev/usb/usb_debug.h>
@@ -138,14 +139,16 @@ union uaudio_sed {
 };
 
 struct uaudio_mixer_node {
+	const char *name;
+
 	int32_t	minval;
 	int32_t	maxval;
-#define	MIX_MAX_CHAN 8
+#define	MIX_MAX_CHAN 16
 	int32_t	wValue[MIX_MAX_CHAN];	/* using nchan */
 	uint32_t mul;
 	uint32_t ctl;
 
-	uint16_t wData[MIX_MAX_CHAN];	/* using nchan */
+	int wData[MIX_MAX_CHAN];	/* using nchan */
 	uint16_t wIndex;
 
 	uint8_t	update[(MIX_MAX_CHAN + 7) / 8];
@@ -165,6 +168,8 @@ struct uaudio_mixer_node {
 	uint8_t	slctrtype[MAX_SELECTOR_INPUT_PIN];
 	uint8_t	class;
 
+	uint8_t desc[64];
+
 	struct uaudio_mixer_node *next;
 };
 
@@ -277,7 +282,9 @@ struct uaudio_softc {
 	struct uaudio_chan sc_play_chan;
 	struct umidi_chan sc_midi_chan;
 	struct uaudio_search_result sc_mixer_clocks;
+	struct sysctl_ctx_list sc_sysctl_ctx;
 
+	struct mtx *sc_mixer_lock;
 	struct usb_device *sc_udev;
 	struct usb_xfer *sc_mixer_xfer[1];
 	struct uaudio_mixer_node *sc_mixer_root;
@@ -399,6 +406,13 @@ static usb_callback_t uaudio_mixer_write
 static usb_callback_t umidi_bulk_read_callback;
 static usb_callback_t umidi_bulk_write_callback;
 
+/* ==== USB mixer ==== */
+
+static int uaudio_mixer_sysctl_handler(SYSCTL_HANDLER_ARGS);
+static void uaudio_mixer_ctl_free(struct uaudio_softc *);
+static void uaudio_mixer_register_sysctl(struct uaudio_softc *, device_t);
+static void uaudio_mixer_reload_all(struct uaudio_softc *);
+
 /* ==== USB audio v1.0 ==== */
 
 static void	uaudio_mixer_add_mixer(struct uaudio_softc *,
@@ -700,6 +714,8 @@ uaudio_attach(device_t dev)
 	if (usb_test_quirk(uaa, UQ_AU_VENDOR_CLASS))
 		sc->sc_uq_au_vendor_class = 1;
 
+	sysctl_ctx_init(&sc->sc_sysctl_ctx);
+
 	umidi_init(dev);
 
 	device_set_usb_desc(dev);
@@ -720,19 +736,27 @@ uaudio_attach(device_t dev)
 	    sc->sc_mixer_count);
 
 	if (sc->sc_play_chan.valid) {
-		device_printf(dev, "Play: %d Hz, %d ch, %s format.\n",
+		device_printf(dev, "Play: %d Hz, %d ch, %s format, "
+		    "2x%d samples buffer.\n",
 		    sc->sc_play_chan.sample_rate,
 		    sc->sc_play_chan.channels,
-		    sc->sc_play_chan.p_fmt->description);
+		    sc->sc_play_chan.p_fmt->description,
+		    (sc->sc_play_chan.bytes_per_frame[0] *
+		    sc->sc_play_chan.intr_frames) /
+		    sc->sc_play_chan.sample_size);
 	} else {
 		device_printf(dev, "No playback.\n");
 	}
 
 	if (sc->sc_rec_chan.valid) {
-		device_printf(dev, "Record: %d Hz, %d ch, %s format.\n",
+		device_printf(dev, "Record: %d Hz, %d ch, %s format, "
+		    "2x%d samples buffer.\n",
 		    sc->sc_rec_chan.sample_rate,
-		    sc->sc_play_chan.channels,
-		    sc->sc_rec_chan.p_fmt->description);
+		    sc->sc_rec_chan.channels,
+		    sc->sc_rec_chan.p_fmt->description,
+		    (sc->sc_rec_chan.bytes_per_frame[0] *
+		    sc->sc_rec_chan.intr_frames) /
+		    sc->sc_rec_chan.sample_size);
 	} else {
 		device_printf(dev, "No recording.\n");
 	}
@@ -773,6 +797,10 @@ uaudio_attach(device_t dev)
 		DPRINTF("child attach failed\n");
 		goto detach;
 	}
+
+	/* reload all mixer settings */
+	uaudio_mixer_reload_all(sc);
+
 	return (0);			/* success */
 
 detach:
@@ -808,9 +836,8 @@ uaudio_attach_sub(device_t dev, kobj_cla
 		 */
 		uaudio_pcm_setflags(dev, SD_F_SOFTPCMVOL);
 	}
-	if (mixer_init(dev, mixer_class, sc)) {
+	if (mixer_init(dev, mixer_class, sc))
 		goto detach;
-	}
 	sc->sc_mixer_init = 1;
 
 	snprintf(status, sizeof(status), "at ? %s", PCM_KLDSTRING(snd_uaudio));
@@ -832,6 +859,8 @@ uaudio_attach_sub(device_t dev, kobj_cla
 	}
 	pcm_setstatus(dev, status);
 
+	uaudio_mixer_register_sysctl(sc, dev);
+
 	return (0);			/* success */
 
 detach:
@@ -867,6 +896,10 @@ uaudio_detach(device_t dev)
 {
 	struct uaudio_softc *sc = device_get_softc(dev);
 
+	/* free all sysctls */
+
+	sysctl_ctx_free(&sc->sc_sysctl_ctx);
+
 	/*
 	 * Stop USB transfers early so that any audio applications
 	 * will time out and close opened /dev/dspX.Y device(s), if
@@ -885,6 +918,10 @@ uaudio_detach(device_t dev)
 
 	umidi_detach(dev);
 
+	/* free mixer data */
+
+	uaudio_mixer_ctl_free(sc);
+
 	return (0);
 }
 
@@ -2036,6 +2073,149 @@ uaudio_chan_stop(struct uaudio_chan *ch)
  * AC - Audio Controller - routines
  *========================================================================*/
 
+static int
+uaudio_mixer_sysctl_handler(SYSCTL_HANDLER_ARGS)
+{
+	struct uaudio_softc *sc;
+	struct uaudio_mixer_node *pmc;
+	int hint;
+	int error;
+	int temp = 0;
+	int chan = 0;
+
+	sc = (struct uaudio_softc *)oidp->oid_arg1;
+	hint = oidp->oid_arg2;
+
+	if (sc->sc_mixer_lock == NULL)
+		return (ENXIO);
+
+	/* lookup mixer node */
+
+	mtx_lock(sc->sc_mixer_lock);
+	for (pmc = sc->sc_mixer_root; pmc != NULL; pmc = pmc->next) {
+		for (chan = 0; chan != (int)pmc->nchan; chan++) {
+			if (pmc->wValue[chan] != -1 &&
+			    pmc->wValue[chan] == hint) {
+				temp = pmc->wData[chan];
+				goto found;
+			}
+		}
+	}
+found:
+	mtx_unlock(sc->sc_mixer_lock);
+
+	error = sysctl_handle_int(oidp, &temp, 0, req);
+	if (error != 0 || req->newptr == NULL)
+		return (error);
+
+	/* update mixer value */
+
+	mtx_lock(sc->sc_mixer_lock);
+	if (pmc != NULL &&
+	    temp >= pmc->minval &&
+	    temp <= pmc->maxval) {
+
+		pmc->wData[chan] = temp;
+		pmc->update[(chan / 8)] |= (1 << (chan % 8));
+
+		/* start the transfer, if not already started */
+		usbd_transfer_start(sc->sc_mixer_xfer[0]);
+	}
+	mtx_unlock(sc->sc_mixer_lock);
+
+	return (0);
+}
+
+static void
+uaudio_mixer_ctl_free(struct uaudio_softc *sc)
+{
+	struct uaudio_mixer_node *p_mc;
+
+	while ((p_mc = sc->sc_mixer_root) != NULL) {
+		sc->sc_mixer_root = p_mc->next;
+		free(p_mc, M_USBDEV);
+	}
+}
+
+static void
+uaudio_mixer_register_sysctl(struct uaudio_softc *sc, device_t dev)
+{
+	struct uaudio_mixer_node *pmc;
+	struct sysctl_oid *mixer_tree;
+	struct sysctl_oid *control_tree;
+	char buf[32];
+	int chan;
+	int n;
+
+	mixer_tree = SYSCTL_ADD_NODE(&sc->sc_sysctl_ctx,
+	    SYSCTL_CHILDREN(device_get_sysctl_tree(dev)), OID_AUTO, "mixer",
+	    CTLFLAG_RD, NULL, "");
+
+	if (mixer_tree == NULL)
+		return;
+
+	for (n = 0, pmc = sc->sc_mixer_root; pmc != NULL;
+	    pmc = pmc->next, n++) {
+
+		for (chan = 0; chan < pmc->nchan; chan++) {
+
+			if (pmc->nchan > 1) {
+				snprintf(buf, sizeof(buf), "%s_%d_%d",
+				    pmc->name, n, chan);
+			} else {
+				snprintf(buf, sizeof(buf), "%s_%d",
+				    pmc->name, n);
+			}
+
+			control_tree = SYSCTL_ADD_NODE(&sc->sc_sysctl_ctx,
+			    SYSCTL_CHILDREN(mixer_tree), OID_AUTO, buf,
+			    CTLFLAG_RD, NULL, "");
+
+			if (control_tree == NULL)
+				continue;
+
+			SYSCTL_ADD_PROC(&sc->sc_sysctl_ctx,
+			    SYSCTL_CHILDREN(control_tree),
+			    OID_AUTO, "val", CTLTYPE_INT | CTLFLAG_RW, sc,
+			    pmc->wValue[chan],
+			    uaudio_mixer_sysctl_handler, "I", "Current value");
+
+			SYSCTL_ADD_INT(&sc->sc_sysctl_ctx,
+			    SYSCTL_CHILDREN(control_tree),
+			    OID_AUTO, "min", CTLFLAG_RD, 0, pmc->minval,
+			    "Minimum value");
+
+			SYSCTL_ADD_INT(&sc->sc_sysctl_ctx,
+			    SYSCTL_CHILDREN(control_tree),
+			    OID_AUTO, "max", CTLFLAG_RD, 0, pmc->maxval,
+			    "Maximum value");
+
+			SYSCTL_ADD_STRING(&sc->sc_sysctl_ctx,
+			    SYSCTL_CHILDREN(control_tree),
+			    OID_AUTO, "desc", CTLFLAG_RD, pmc->desc, 0,
+			    "Description");
+		}
+	}
+}
+
+static void
+uaudio_mixer_reload_all(struct uaudio_softc *sc)
+{
+	struct uaudio_mixer_node *pmc;
+	int chan;
+
+	if (sc->sc_mixer_lock == NULL)
+		return;
+
+	mtx_lock(sc->sc_mixer_lock);
+	for (pmc = sc->sc_mixer_root; pmc != NULL; pmc = pmc->next) {
+		for (chan = 0; chan < pmc->nchan; chan++)
+			pmc->update[chan / 8] |= (1 << (chan % 8));
+	}
+	usbd_transfer_start(sc->sc_mixer_xfer[0]);
+	mtx_unlock(sc->sc_mixer_lock);
+}
+
 static void
 uaudio_mixer_add_ctl_sub(struct uaudio_softc *sc, struct uaudio_mixer_node *mc)
 {
@@ -2287,9 +2467,9 @@ uaudio_mixer_add_selector(struct uaudio_
 	DPRINTFN(3, "bUnitId=%d bNrInPins=%d\n",
 	    d->bUnitId, d->bNrInPins);
 
-	if (d->bNrInPins == 0) {
+	if (d->bNrInPins == 0)
 		return;
-	}
+
 	memset(&mix, 0, sizeof(mix));
 
 	mix.wIndex = MAKE_WORD(d->bUnitId, sc->sc_mixer_iface_no);
@@ -2297,10 +2477,17 @@ uaudio_mixer_add_selector(struct uaudio_
 	uaudio_mixer_determine_class(&iot[id], &mix);
 	mix.nchan = 1;
 	mix.type = MIX_SELECTOR;
-
 	mix.ctl = SOUND_MIXER_NRDEVICES;
 	mix.minval = 1;
 	mix.maxval = d->bNrInPins;
+	mix.name = "selector";
+
+	i = d->baSourceId[d->bNrInPins];
+	if (i == 0 ||
+	    usbd_req_get_string_any(sc->sc_udev, NULL,
+	    mix.desc, sizeof(mix.desc), i) != 0) {
+		mix.desc[0] = 0;
+	}
 
 	if (mix.maxval > MAX_SELECTOR_INPUT_PIN) {
 		mix.maxval = MAX_SELECTOR_INPUT_PIN;
@@ -2341,10 +2528,17 @@ uaudio20_mixer_add_selector(struct uaudi
 	uaudio20_mixer_determine_class(&iot[id], &mix);
 	mix.nchan = 1;
 	mix.type = MIX_SELECTOR;
-
 	mix.ctl = SOUND_MIXER_NRDEVICES;
 	mix.minval = 1;
 	mix.maxval = d->bNrInPins;
+	mix.name = "selector";
+
+	i = d->baSourceId[d->bNrInPins];
+	if (i == 0 ||
+	    usbd_req_get_string_any(sc->sc_udev, NULL,
+	    mix.desc, sizeof(mix.desc), i) != 0) {
+		mix.desc[0] = 0;
+	}
 
 	if (mix.maxval > MAX_SELECTOR_INPUT_PIN)
 		mix.maxval = MAX_SELECTOR_INPUT_PIN;
@@ -2400,18 +2594,18 @@ uaudio_mixer_add_feature(struct uaudio_s
 	uint8_t ctl;
 	uint8_t i;
 
-	if (d->bControlSize == 0) {
+	if (d->bControlSize == 0)
 		return;
-	}
+
 	memset(&mix, 0, sizeof(mix));
 
 	nchan = (d->bLength - 7) / d->bControlSize;
 	mmask = uaudio_mixer_feature_get_bmaControls(d, 0);
 	cmask = 0;
 
-	if (nchan == 0) {
+	if (nchan == 0)
 		return;
-	}
+
 	/* figure out what we can control */
 
 	for (chan = 1; chan < nchan; chan++) {
@@ -2426,6 +2620,13 @@ uaudio_mixer_add_feature(struct uaudio_s
 	}
 	mix.wIndex = MAKE_WORD(d->bUnitId, sc->sc_mixer_iface_no);
 
+	i = d->bmaControls[d->bControlSize];
+	if (i == 0 ||
+	    usbd_req_get_string_any(sc->sc_udev, NULL,
+	    mix.desc, sizeof(mix.desc), i) != 0) {
+		mix.desc[0] = 0;
+	}
+
 	for (ctl = 1; ctl <= LOUDNESS_CONTROL; ctl++) {
 
 		fumask = FU_MASK(ctl);
@@ -2454,50 +2655,58 @@ uaudio_mixer_add_feature(struct uaudio_s
 		case MUTE_CONTROL:
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_NRDEVICES;
+			mix.name = "mute";
 			break;
 
 		case VOLUME_CONTROL:
 			mix.type = MIX_SIGNED_16;
 			mix.ctl = mixernumber;
+			mix.name = "vol";
 			break;
 
 		case BASS_CONTROL:
 			mix.type = MIX_SIGNED_8;
 			mix.ctl = SOUND_MIXER_BASS;
+			mix.name = "bass";
 			break;
 
 		case MID_CONTROL:
 			mix.type = MIX_SIGNED_8;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "mid";
 			break;
 
 		case TREBLE_CONTROL:
 			mix.type = MIX_SIGNED_8;
 			mix.ctl = SOUND_MIXER_TREBLE;
+			mix.name = "treble";
 			break;
 
 		case GRAPHIC_EQUALIZER_CONTROL:
 			continue;	/* XXX don't add anything */
-			break;
 
 		case AGC_CONTROL:
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "agc";
 			break;
 
 		case DELAY_CONTROL:
 			mix.type = MIX_UNSIGNED_16;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "delay";
 			break;
 
 		case BASS_BOOST_CONTROL:
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "boost";
 			break;
 
 		case LOUDNESS_CONTROL:
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_LOUD;	/* Is this correct ? */
+			mix.name = "loudness";
 			break;
 
 		default:
@@ -2547,6 +2756,13 @@ uaudio20_mixer_add_feature(struct uaudio
 
 	mix.wIndex = MAKE_WORD(d->bUnitId, sc->sc_mixer_iface_no);
 
+	i = d->bmaControls[nchan][0];
+	if (i == 0 ||
+	    usbd_req_get_string_any(sc->sc_udev, NULL,
+	    mix.desc, sizeof(mix.desc), i) != 0) {
+		mix.desc[0] = 0;
+	}
+
 	for (ctl = 3; ctl != 0; ctl <<= 2) {
 
 		mixernumber = uaudio20_mixer_feature_name(&iot[id], &mix);
@@ -2555,56 +2771,67 @@ uaudio20_mixer_add_feature(struct uaudio
 		case (3 << 0):
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_NRDEVICES;
+			mix.name = "mute";
 			what = MUTE_CONTROL;
 			break;
 		case (3 << 2): 
 			mix.type = MIX_SIGNED_16;
 			mix.ctl = mixernumber;
+			mix.name = "vol";
 			what = VOLUME_CONTROL;
 			break;
 		case (3 << 4):
 			mix.type = MIX_SIGNED_8;
 			mix.ctl = SOUND_MIXER_BASS;
+			mix.name = "bass";
 			what = BASS_CONTROL;
 			break;
 		case (3 << 6):
 			mix.type = MIX_SIGNED_8;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "mid";
 			what = MID_CONTROL;
 			break;
 		case (3 << 8):
 			mix.type = MIX_SIGNED_8;
 			mix.ctl = SOUND_MIXER_TREBLE;
+			mix.name = "treble";
 			what = TREBLE_CONTROL;
 			break;
 		case (3 << 12):
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "agc";
 			what = AGC_CONTROL;
 			break;
 		case (3 << 14):
 			mix.type = MIX_UNSIGNED_16;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "delay";
 			what = DELAY_CONTROL;
 			break;
 		case (3 << 16):
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_NRDEVICES;	/* XXXXX */
+			mix.name = "boost";
 			what = BASS_BOOST_CONTROL;
 			break;
 		case (3 << 18):
 			mix.type = MIX_ON_OFF;
 			mix.ctl = SOUND_MIXER_LOUD;	/* Is this correct ? */
+			mix.name = "loudness";
 			what = LOUDNESS_CONTROL;
 			break;
 		case (3 << 20):
 			mix.type = MIX_SIGNED_16;
 			mix.ctl = mixernumber;
+			mix.name = "igain";
 			what = INPUT_GAIN_CONTROL;
 			break;
 		case (3 << 22):
 			mix.type = MIX_SIGNED_16;
 			mix.ctl = mixernumber;
+			mix.name = "igainpad";
 			what = INPUT_GAIN_PAD_CONTROL;
 			break;
 		default:
@@ -2807,11 +3034,16 @@ uaudio_mixer_verify_desc(const void *arg
 		if (u.desc->bLength < len) {
 			goto error;
 		}
-		len += u.su->bNrInPins;
+		len += u.su->bNrInPins + 1;
 		break;
 
 	case UDESCSUB_AC_FEATURE:
-		len += (sizeof(*u.fu) + 1);
+		len += sizeof(*u.fu) + 1;
+
+		if (u.desc->bLength < len)
+			goto error;
+
+		len += u.fu->bControlSize;
 		break;
 
 	case UDESCSUB_AC_PROCESSING:
@@ -2939,14 +3171,11 @@ uaudio20_mixer_verify_desc(const void *a
 		if (u.desc->bLength < len)
 			goto error;
 
-		len += u.su->bNrInPins;
+		len += u.su->bNrInPins + 1;
 		break;
 
 	case UDESCSUB_AC_FEATURE:
 		len += sizeof(*u.fu) + 1;
-
-		if (u.desc->bLength < len)
-			goto error;
 		break;
 
 	case UDESCSUB_AC_EFFECT:
@@ -4303,9 +4532,11 @@ uaudio_mixer_init_sub(struct uaudio_soft
 {
 	DPRINTF("\n");
 
+	sc->sc_mixer_lock = mixer_get_lock(m);
+
 	if (usbd_transfer_setup(sc->sc_udev, &sc->sc_mixer_iface_index,
 	    sc->sc_mixer_xfer, uaudio_mixer_config, 1, sc,
-	    mixer_get_lock(m))) {
+	    sc->sc_mixer_lock)) {
 		DPRINTFN(0, "could not allocate USB "
 		    "transfer for audio mixer!\n");
 		return (ENOMEM);
@@ -4326,6 +4557,8 @@ uaudio_mixer_uninit_sub(struct uaudio_so
 
 	usbd_transfer_unsetup(sc->sc_mixer_xfer, 1);
 
+	sc->sc_mixer_lock = NULL;
+
 	return (0);
 }
 
@@ -4334,17 +4567,16 @@ uaudio_mixer_set(struct uaudio_softc *sc
     unsigned left, unsigned right)
 {
 	struct uaudio_mixer_node *mc;
+	int chan;
 
-	for (mc = sc->sc_mixer_root; mc;
-	    mc = mc->next) {
+	for (mc = sc->sc_mixer_root; mc != NULL; mc = mc->next) {
 
 		if (mc->ctl == type) {
-			if (mc->nchan == 2) {
-				/* set Right */
-				uaudio_mixer_ctl_set(sc, mc, 1, (int)(right * 255) / 100);
+			for (chan = 0; chan < mc->nchan; chan++) {
+				uaudio_mixer_ctl_set(sc, mc, chan,
+				    (int)((chan == 0 ? left : right) *
+				    255) / 100);
 			}
-			/* set Left or Mono */
-			uaudio_mixer_ctl_set(sc, mc, 0, (int)(left * 255) / 100);
 		}
 	}
 }



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201211011654.qA1GsQiF035570>