diff options
author | Ka Ho Ng <khng@FreeBSD.org> | 2021-03-17 10:51:58 +0000 |
---|---|---|
committer | Ka Ho Ng <khng@FreeBSD.org> | 2021-03-17 11:05:43 +0000 |
commit | c96151d33509655efb7fb26768cb56a041c176f1 (patch) | |
tree | 2ce59a89aeb8410db838b3ba63d90523bed2df9d | |
parent | 096a84721670d388e432a1f7399251e4b20714f1 (diff) | |
download | src-c96151d33509655efb7fb26768cb56a041c176f1.tar.gz src-c96151d33509655efb7fb26768cb56a041c176f1.zip |
Implement sndstat nvlist-based enumeration ioctls.
These ioctl commands aim to provide easier ways for user space
applications to enumerate existing audio devices and the node they can
potentially use.
The exchange of device lists between user space and kernel is done on
nv(9). Some ioctl commands are added to /dev/sndstat node:
- SNDSTAT_REFRESH_DEVS
- SNDSTAT_GET_DEVS
- SNDSTAT_ADD_USER_DEVS
- SNDSTAT_FLUSH_USER_DEVS
Bump __FreeBSD_version to reflect the addition of the ioctls.
Sponsored by: The FreeBSD Foundation
Reviewed by: hselasky
Approved by: philip (mentor)
Differential Revision: https://reviews.freebsd.org/D26884
-rw-r--r-- | share/man/man4/Makefile | 1 | ||||
-rw-r--r-- | share/man/man4/sndstat.4 | 259 | ||||
-rw-r--r-- | sys/dev/sound/pcm/sndstat.c | 738 | ||||
-rw-r--r-- | sys/dev/sound/pcm/sound.h | 1 | ||||
-rw-r--r-- | sys/sys/param.h | 2 | ||||
-rw-r--r-- | sys/sys/sndstat.h | 95 |
6 files changed, 1086 insertions, 10 deletions
diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile index 686e86cf6897..54fd89fe7590 100644 --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -526,6 +526,7 @@ MAN= aac.4 \ snd_via8233.4 \ snd_via82c686.4 \ snd_vibes.4 \ + sndstat.4 \ snp.4 \ spigen.4 \ ${_spkr.4} \ diff --git a/share/man/man4/sndstat.4 b/share/man/man4/sndstat.4 new file mode 100644 index 000000000000..ad5f4a76ea7e --- /dev/null +++ b/share/man/man4/sndstat.4 @@ -0,0 +1,259 @@ +.\" +.\" SPDX-License-Identifier: BSD-2-Clause-FreeBSD +.\" +.\" This software was developed by Ka Ho Ng +.\" under sponsorship from the FreeBSD Foundation. +.\" +.\" Copyright (c) 2020 The FreeBSD Foundation +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" $FreeBSD$ +.\" +.\" Note: The date here should be updated whenever a non-trivial +.\" change is made to the manual page. +.Dd December 7, 2020 +.Dt SNDSTAT 4 +.Os +.Sh NAME +.Nm sndstat +.Nd "nvlist-based PCM audio device enumeration interface" +.Sh SYNOPSIS +To compile the driver into the kernel, +place the following lines in the +kernel configuration file: +.Bd -ragged -offset indent +.Cd "device sound" +.Ed +.Sh DESCRIPTION +The ioctl interface provided by +.Pa /dev/sndstat +device allows callers to enumeration PCM audio devices available for use. +.Sh IOCTLS +For all ioctls requiring data exchange between the subsystem and callers, +the following structures are used to describe a serialized nvlist: +.Bd -literal -offset indent +struct sndstat_nvlbuf_arg { + size_t nbytes; + void *buf; +}; +.Ed +.Pp +Here is an example of an nvlist, with explanations of the common fields: +.Bd -literal -offset indent +dsps (NVLIST ARRAY): 1 + from_user (BOOL): FALSE + nameunit (STRING): [pcm0] + devnode (STRING): [dsp0] + desc (STRING): [Generic (0x8086) (Analog Line-out)] + pchan (NUMBER): 1 (1) (0x1) + rchan (NUMBER): 0 (0) (0x0) + pminrate (NUMBER): 48000 (48000) (0xbb80) + pmaxrate (NUMBER): 48000 (48000) (0xbb80) + pfmts (NUMBER): 2097168 (2097168) (0x200010) + provider_info (NVLIST): + unit (NUMBER): 0 (0) (0x0) + bitperfect (BOOL): FALSE + pvchan (NUMBER): 1 (1) (0x1) + rvchan (NUMBER): 0 (0) (0x0) + provider (STRING): [sound(4)] + , +.Ed +.Bl -tag -width ".Dv provider_info" +.It Dv from_user +Whether the PCM audio device node is created by in-kernel audio subsystem or +userspace providers. +.It Dv nameunit +The device identification in the form of subsystem plus a unit number. +.It Dv devnode +The PCM audio device node relative path in devfs. +.It Dv desc +The descripton of the PCM audio device. +.It Dv pchan +The number of playback channels supported by hardware. +This can be 0 if this PCM audio device does not support playback at all. +.It Dv rchan +The number of recording channels supported by hardware. +This can be 0 if this PCM audio device does not support recording at all. +.It Dv pminrate +The minimum supported playback direction sampling rate. +Only exists if pchan is greater than 0. +.It Dv pmaxrate +The maximum supported playback direction sampling rate. +Only exists if pchan is greater than 0. +.It Dv pfmts +The supported playback direction sample format. +Only exists if pchan is greater than 0. +.It Dv rminrate +The minimum supported recording direction sampling rate. +Only exists if rchan is greater than 0. +.It Dv rmaxrate +The maximum supported recording direction sampling rate. +Only exists if rchan is greater than 0. +.It Dv rfmts +The supported playback recording sample format. +Only exists if rchan is greater than 0. +.It Dv provider_info +Provider-specific fields. +This field may not exist if the PCM audio device is not provided by in-kernel +interface. +This field will not exist if the provider field is an empty string. +.It Dv provider +A string specifying the provider of the PCm audio device. +.El +.Pp +The following ioctls are providede for use: +.Bl -tag -width ".Dv SNDSTAT_FLUSH_USER_DEVS" +.It Dv SNDSTAT_REFRESH_DEVS +Drop any previously fetched PCM audio devices list snapshots. +This ioctl takes no arguments. +.It Dv SNDSTAT_GET_DEVS +Generate and/or return PCM audio devices list snapshots to callers. +This ioctl takes a pointer to +.Fa struct sndstat_nvlbuf_arg +as the first and the only argument. +Callers need to provide a sufficiently large buffer to hold a serialized +nvlist. +If there is no existing PCM audio device list snapshot available in the +internal structure of the opened sndstat. +.Fa fd , +a new PCM audio device list snapshot will be automatically generated. +Callers have to set +.Fa nbytes +to either 0 or the size of buffer provided. +In case +.Fa nbytes +is 0, the buffer size required to hold a serialized nvlist +stream of current snapshot will be returned in +.Fa nbytes , +and +.Fa buf +will be ignored. +Otherwise, if the buffer is not sufficiently large, +the ioctl returns success, and +.Fa nbytes +will be set to 0. +If the buffer provided is sufficiently large, +.Fa nbytes +will be set to the size of the serialized nvlist written to the provided buffer. +Once a PCM audio device list snapshot is returned to user-space successfully, +the snapshot stored in the subsystem's internal structure of the given +.Fa fd +will be freed. +.It Dv SNDSTAT_ADD_USER_DEVS +Add a list of PCM audio devices provided by callers to +.Pa /dev/sndstat +device. +This ioctl takes a pointer to +.Fa struct sndstat_nvlbuf_arg +as the first and the only argument. +Callers have to provide a buffer holding a serialized nvlist. +.Fa nbytes +should be set to the length in bytes of the serialized nvlist. +.Fa buf +should be pointed to a buffer storing the serialized nvlist. +Userspace-backed PCM audio device nodes should be listed inside the serialized +nvlist. +.It Dv SNDSTAT_FLUSH_USER_DEVS +Flush any PCM audio devices previously added by callers. +This ioctl takes no arguments. +.El +.Sh FILES +.Bl -tag -width ".Pa /dev/sndstat" -compact +.It Pa /dev/sndstat +.El +.Sh EXAMPLES +The following code enumerates all available PCM audio devices: +.Bd -literal -offset indent +#include <sys/types.h> +#include <err.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/nv.h> +#include <sys/sndstat.h> +#include <sysexits.h> +#include <unistd.h> + +int +main() +{ + int fd; + struct sndstat_nvlbuf_arg arg; + const nvlist_t * const *di; + size_t i, nitems; + nvlist_t *nvl; + + /* Open sndstat node in read-only first */ + fd = open("/dev/sndstat", O_RDONLY); + + if (ioctl(fd, SNDSTAT_REFRESH_DEVS, NULL)) + err(1, "ioctl(fd, SNDSTAT_REFRESH_DEVS, NULL)"); + + /* Get the size of snapshot, when nbytes = 0 */ + arg.nbytes = 0; + arg.buf = NULL; + if (ioctl(fd, SNDSTAT_GET_DEVS, &arg)) + err(1, "ioctl(fd, SNDSTAT_GET_DEVS, &arg)"); + + /* Get snapshot data */ + arg.buf = malloc(arg.nbytes); + if (arg.buf == NULL) + err(EX_OSERR, "malloc"); + if (ioctl(fd, SNDSTAT_GET_DEVS, &arg)) + err(1, "ioctl(fd, SNDSTAT_GET_DEVS, &arg)"); + + /* Deserialize the nvlist stream */ + nvl = nvlist_unpack(arg.buf, arg.nbytes, 0); + free(arg.buf); + + /* Get DSPs array */ + di = nvlist_get_nvlist_array(nvl, SNDSTAT_LABEL_DSPS, &nitems); + for (i = 0; i < nitems; i++) { + const char *nameunit, *devnode, *desc; + + /* + * Examine each device nvlist item + */ + + nameunit = nvlist_get_string(di[i], SNDSTAT_LABEL_NAMEUNIT); + devnode = nvlist_get_string(di[i], SNDSTAT_LABEL_DEVNODE); + desc = nvlist_get_string(di[i], SNDSTAT_LABEL_DESC); + printf("Name unit: `%s`, Device node: `%s`, Description: `%s`\n", + nameunit, devnode, desc); + } + + nvlist_destroy(nvl); + return (0); +} +.Ed +.Sh SEE ALSO +.Xr sound 4 , +.Xr nv 9 +.Sh HISTORY +The nvlist-based ioctls support for +.Nm +device first appeared in +.Fx 13.0 . +.Sh AUTHORS +This manual page was written by +.An Ka Ho Ng Aq Mt khng@FreeBSD.org . diff --git a/sys/dev/sound/pcm/sndstat.c b/sys/dev/sound/pcm/sndstat.c index 31c4a1b14a79..e89af772fe72 100644 --- a/sys/dev/sound/pcm/sndstat.c +++ b/sys/dev/sound/pcm/sndstat.c @@ -3,8 +3,12 @@ * * Copyright (c) 2005-2009 Ariff Abdullah <ariff@FreeBSD.org> * Copyright (c) 2001 Cameron Grant <cg@FreeBSD.org> + * Copyright (c) 2020 The FreeBSD Foundation * All rights reserved. * + * Portions of this software were developed by Ka Ho Ng + * under sponsorship from the FreeBSD Foundation. + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: @@ -31,10 +35,20 @@ #include "opt_snd.h" #endif +#include <sys/param.h> +#include <sys/lock.h> +#include <sys/malloc.h> +#include <sys/nv.h> +#include <sys/dnv.h> +#include <sys/sx.h> +#ifdef COMPAT_FREEBSD32 +#include <sys/sysent.h> +#endif + #include <dev/sound/pcm/sound.h> #include <dev/sound/pcm/pcm.h> #include <dev/sound/version.h> -#include <sys/sx.h> + SND_DECLARE_FILE("$FreeBSD$"); @@ -47,12 +61,14 @@ static d_open_t sndstat_open; static void sndstat_close(void *); static d_read_t sndstat_read; static d_write_t sndstat_write; +static d_ioctl_t sndstat_ioctl; static struct cdevsw sndstat_cdevsw = { .d_version = D_VERSION, .d_open = sndstat_open, .d_read = sndstat_read, .d_write = sndstat_write, + .d_ioctl = sndstat_ioctl, .d_name = "sndstat", .d_flags = D_TRACKCLOSE, }; @@ -65,11 +81,33 @@ struct sndstat_entry { int type, unit; }; +struct sndstat_userdev { + TAILQ_ENTRY(sndstat_userdev) link; + char *provider; + char *nameunit; + char *devnode; + char *desc; + unsigned int pchan; + unsigned int rchan; + uint32_t pminrate; + uint32_t pmaxrate; + uint32_t rminrate; + uint32_t rmaxrate; + uint32_t pfmts; + uint32_t rfmts; + nvlist_t *provider_nvl; +}; + struct sndstat_file { TAILQ_ENTRY(sndstat_file) entry; struct sbuf sbuf; + struct sx lock; + void *devs_nvlbuf; /* (l) */ + size_t devs_nbytes; /* (l) */ + TAILQ_HEAD(, sndstat_userdev) userdev_list; /* (l) */ int out_offset; int in_offset; + int fflags; }; static struct sx sndstat_lock; @@ -84,6 +122,8 @@ static TAILQ_HEAD(, sndstat_file) sndstat_filelist = TAILQ_HEAD_INITIALIZER(snds int snd_verbose = 0; static int sndstat_prepare(struct sndstat_file *); +static struct sndstat_userdev * +sndstat_line2userdev(struct sndstat_file *, const char *, int); static int sysctl_hw_sndverbose(SYSCTL_HANDLER_ARGS) @@ -112,12 +152,16 @@ sndstat_open(struct cdev *i_dev, int flags, int mode, struct thread *td) pf = malloc(sizeof(*pf), M_DEVBUF, M_WAITOK | M_ZERO); - SNDSTAT_LOCK(); if (sbuf_new(&pf->sbuf, NULL, 4096, SBUF_AUTOEXTEND) == NULL) { - SNDSTAT_UNLOCK(); free(pf, M_DEVBUF); return (ENOMEM); } + + pf->fflags = flags; + TAILQ_INIT(&pf->userdev_list); + sx_init(&pf->lock, "sndstat_file"); + + SNDSTAT_LOCK(); TAILQ_INSERT_TAIL(&sndstat_filelist, pf, entry); SNDSTAT_UNLOCK(); @@ -126,6 +170,29 @@ sndstat_open(struct cdev *i_dev, int flags, int mode, struct thread *td) return (0); } +/* + * Should only be called either when: + * * Closing + * * pf->lock held + */ +static void +sndstat_remove_all_userdevs(struct sndstat_file *pf) +{ + struct sndstat_userdev *ud; + + KASSERT( + sx_xlocked(&pf->lock), ("%s: Called without pf->lock", __func__)); + while ((ud = TAILQ_FIRST(&pf->userdev_list)) != NULL) { + TAILQ_REMOVE(&pf->userdev_list, ud, link); + free(ud->provider, M_DEVBUF); + free(ud->desc, M_DEVBUF); + free(ud->devnode, M_DEVBUF); + free(ud->nameunit, M_DEVBUF); + nvlist_destroy(ud->provider_nvl); + free(ud, M_DEVBUF); + } +} + static void sndstat_close(void *sndstat_file) { @@ -136,6 +203,12 @@ sndstat_close(void *sndstat_file) TAILQ_REMOVE(&sndstat_filelist, pf, entry); SNDSTAT_UNLOCK(); + free(pf->devs_nvlbuf, M_NVLIST); + sx_xlock(&pf->lock); + sndstat_remove_all_userdevs(pf); + sx_xunlock(&pf->lock); + sx_destroy(&pf->lock); + free(pf, M_DEVBUF); } @@ -203,7 +276,10 @@ sndstat_write(struct cdev *i_dev, struct uio *buf, int flag) err = EINVAL; } else { /* only remember the last write - allows for updates */ - sbuf_clear(&pf->sbuf); + sx_xlock(&pf->lock); + sndstat_remove_all_userdevs(pf); + sx_xunlock(&pf->lock); + while (1) { len = sizeof(temp); if (len > buf->uio_resid) @@ -221,15 +297,647 @@ sndstat_write(struct cdev *i_dev, struct uio *buf, int flag) } } sbuf_finish(&pf->sbuf); - if (err == 0) + + if (err == 0) { + char *line, *str; + + str = sbuf_data(&pf->sbuf); + while ((line = strsep(&str, "\n")) != NULL) { + struct sndstat_userdev *ud; + + ud = sndstat_line2userdev(pf, line, strlen(line)); + if (ud == NULL) + continue; + + sx_xlock(&pf->lock); + TAILQ_INSERT_TAIL(&pf->userdev_list, ud, link); + sx_xunlock(&pf->lock); + } + pf->out_offset = sbuf_len(&pf->sbuf); - else + } else pf->out_offset = 0; + + sbuf_clear(&pf->sbuf); + } + SNDSTAT_UNLOCK(); + return (err); +} + +static void +sndstat_get_caps(struct snddev_info *d, bool play, uint32_t *min_rate, + uint32_t *max_rate, uint32_t *fmts) +{ + struct pcm_channel *c; + int dir; + + dir = play ? PCMDIR_PLAY : PCMDIR_REC; + *min_rate = 0; + *max_rate = 0; + *fmts = 0; + + if (play && d->pvchancount > 0) { + *min_rate = *max_rate = d->pvchanrate; + *fmts = d->pvchanformat; + return; + } else if (!play && d->rvchancount > 0) { + *min_rate = *max_rate = d->rvchanrate; + *fmts = d->rvchanformat; + return; + } + + CHN_FOREACH(c, d, channels.pcm) { + struct pcmchan_caps *caps; + + if (c->direction != dir || (c->flags & CHN_F_VIRTUAL) != 0) + continue; + + CHN_LOCK(c); + caps = chn_getcaps(c); + *min_rate = caps->minspeed; + *max_rate = caps->maxspeed; + *fmts = chn_getformats(c); + CHN_UNLOCK(c); + } +} + +static int +sndstat_build_sound4_nvlist(struct snddev_info *d, nvlist_t **dip) +{ + uint32_t maxrate, minrate, fmts; + nvlist_t *di = NULL, *sound4di = NULL; + int err; + + di = nvlist_create(0); + if (di == NULL) { + err = ENOMEM; + goto done; + } + sound4di = nvlist_create(0); + if (sound4di == NULL) { + err = ENOMEM; + goto done; + } + + nvlist_add_bool(di, SNDSTAT_LABEL_FROM_USER, false); + nvlist_add_stringf(di, SNDSTAT_LABEL_NAMEUNIT, "%s", + device_get_nameunit(d->dev)); + nvlist_add_stringf(di, SNDSTAT_LABEL_DEVNODE, "dsp%d", + device_get_unit(d->dev)); + nvlist_add_string( + di, SNDSTAT_LABEL_DESC, device_get_desc(d->dev)); + + PCM_ACQUIRE_QUICK(d); + nvlist_add_number(di, SNDSTAT_LABEL_PCHAN, d->playcount); + nvlist_add_number(di, SNDSTAT_LABEL_RCHAN, d->reccount); + if (d->playcount > 0) { + sndstat_get_caps(d, true, &minrate, &maxrate, &fmts); + nvlist_add_number(di, SNDSTAT_LABEL_PMINRATE, minrate); + nvlist_add_number(di, SNDSTAT_LABEL_PMAXRATE, maxrate); + nvlist_add_number(di, SNDSTAT_LABEL_PFMTS, fmts); + } + if (d->reccount > 0) { + sndstat_get_caps(d, false, &minrate, &maxrate, &fmts); + nvlist_add_number(di, SNDSTAT_LABEL_RMINRATE, minrate); + nvlist_add_number(di, SNDSTAT_LABEL_RMAXRATE, maxrate); + nvlist_add_number(di, SNDSTAT_LABEL_RFMTS, fmts); + } + + nvlist_add_number(sound4di, SNDSTAT_LABEL_SOUND4_UNIT, + device_get_unit(d->dev)); // XXX: I want signed integer here + nvlist_add_bool( + sound4di, SNDSTAT_LABEL_SOUND4_BITPERFECT, d->flags & SD_F_BITPERFECT); + nvlist_add_number(sound4di, SNDSTAT_LABEL_SOUND4_PVCHAN, d->pvchancount); + nvlist_add_number(sound4di, SNDSTAT_LABEL_SOUND4_RVCHAN, d->rvchancount); + nvlist_move_nvlist(di, SNDSTAT_LABEL_PROVIDER_INFO, sound4di); + sound4di = NULL; + PCM_RELEASE_QUICK(d); + nvlist_add_string(di, SNDSTAT_LABEL_PROVIDER, SNDSTAT_LABEL_SOUND4_PROVIDER); + + err = nvlist_error(di); + if (err) + goto done; + + *dip = di; + +done: + if (err) { + nvlist_destroy(sound4di); + nvlist_destroy(di); + } + return (err); +} + +static int +sndstat_build_userland_nvlist(struct sndstat_userdev *ud, nvlist_t **dip) +{ + nvlist_t *di; + int err; + + di = nvlist_create(0); + if (di == NULL) { + err = ENOMEM; + goto done; + } + + nvlist_add_bool(di, SNDSTAT_LABEL_FROM_USER, true); + nvlist_add_number(di, SNDSTAT_LABEL_PCHAN, ud->pchan); + nvlist_add_number(di, SNDSTAT_LABEL_RCHAN, ud->rchan); + nvlist_add_string(di, SNDSTAT_LABEL_NAMEUNIT, ud->nameunit); + nvlist_add_string( + di, SNDSTAT_LABEL_DEVNODE, ud->devnode); + nvlist_add_string(di, SNDSTAT_LABEL_DESC, ud->desc); + if (ud->pchan != 0) { + nvlist_add_number( + di, SNDSTAT_LABEL_PMINRATE, ud->pminrate); + nvlist_add_number( + di, SNDSTAT_LABEL_PMAXRATE, ud->pmaxrate); + nvlist_add_number( + di, SNDSTAT_LABEL_PFMTS, ud->pfmts); + } + if (ud->rchan != 0) { + nvlist_add_number( + di, SNDSTAT_LABEL_RMINRATE, ud->rminrate); + nvlist_add_number( + di, SNDSTAT_LABEL_RMAXRATE, ud->rmaxrate); + nvlist_add_number( + di, SNDSTAT_LABEL_RFMTS, ud->rfmts); + } + nvlist_add_string(di, SNDSTAT_LABEL_PROVIDER, + (ud->provider != NULL) ? ud->provider : ""); + if (ud->provider_nvl != NULL) + nvlist_add_nvlist( + di, SNDSTAT_LABEL_PROVIDER_INFO, ud->provider_nvl); + + err = nvlist_error(di); + if (err) + goto done; + + *dip = di; + +done: + if (err) + nvlist_destroy(di); + return (err); +} + +/* + * Should only be called with the following locks held: + * * sndstat_lock + */ +static int +sndstat_create_devs_nvlist(nvlist_t **nvlp) +{ + int err; + nvlist_t *nvl; + struct sndstat_entry *ent; + struct sndstat_file *pf; + + nvl = nvlist_create(0); + if (nvl == NULL) + return (ENOMEM); + + TAILQ_FOREACH(ent, &sndstat_devlist, link) { + struct snddev_info *d; + nvlist_t *di; + + if (ent->dev == NULL) + continue; + d = device_get_softc(ent->dev); + if (!PCM_REGISTERED(d)) + continue; + + err = sndstat_build_sound4_nvlist(d, &di); + if (err) + goto done; + + nvlist_append_nvlist_array(nvl, SNDSTAT_LABEL_DSPS, di); + nvlist_destroy(di); + err = nvlist_error(nvl); + if (err) + goto done; + } + + TAILQ_FOREACH(pf, &sndstat_filelist, entry) { + struct sndstat_userdev *ud; + + sx_xlock(&pf->lock); + + TAILQ_FOREACH(ud, &pf->userdev_list, link) { + nvlist_t *di; + + err = sndstat_build_userland_nvlist(ud, &di); + if (err != 0) { + sx_xunlock(&pf->lock); + goto done; + } + nvlist_append_nvlist_array(nvl, SNDSTAT_LABEL_DSPS, di); + nvlist_destroy(di); + + err = nvlist_error(nvl); + if (err != 0) { + sx_xunlock(&pf->lock); + goto done; + } + } + + sx_xunlock(&pf->lock); + } + + *nvlp = nvl; + +done: + if (err != 0) + nvlist_destroy(nvl); + return (err); +} + +static int +sndstat_refresh_devs(struct sndstat_file *pf) +{ + sx_xlock(&pf->lock); + free(pf->devs_nvlbuf, M_NVLIST); + pf->devs_nvlbuf = NULL; + pf->devs_nbytes = 0; + sx_unlock(&pf->lock); + + return (0); +} + +static int +sndstat_get_devs(struct sndstat_file *pf, caddr_t data) +{ + int err; + struct sndstat_nvlbuf_arg *arg = (struct sndstat_nvlbuf_arg *)data; + + SNDSTAT_LOCK(); + sx_xlock(&pf->lock); + + if (pf->devs_nvlbuf == NULL) { + nvlist_t *nvl; + void *nvlbuf; + size_t nbytes; + int err; + + sx_xunlock(&pf->lock); + + err = sndstat_create_devs_nvlist(&nvl); + if (err) { + SNDSTAT_UNLOCK(); + return (err); + } + + sx_xlock(&pf->lock); + + nvlbuf = nvlist_pack(nvl, &nbytes); + err = nvlist_error(nvl); + nvlist_destroy(nvl); + if (nvlbuf == NULL || err != 0) { + SNDSTAT_UNLOCK(); + sx_xunlock(&pf->lock); + if (err == 0) + return (ENOMEM); + return (err); + } + + free(pf->devs_nvlbuf, M_NVLIST); + pf->devs_nvlbuf = nvlbuf; + pf->devs_nbytes = nbytes; } + SNDSTAT_UNLOCK(); + + if (!arg->nbytes) { + arg->nbytes = pf->devs_nbytes; + err = 0; + goto done; + } + if (arg->nbytes < pf->devs_nbytes) { + arg->nbytes = 0; + err = 0; + goto done; + } + + err = copyout(pf->devs_nvlbuf, arg->buf, pf->devs_nbytes); + if (err) + goto done; + + arg->nbytes = pf->devs_nbytes; + + free(pf->devs_nvlbuf, M_NVLIST); + pf->devs_nvlbuf = NULL; + pf->devs_nbytes = 0; + +done: + sx_unlock(&pf->lock); return (err); } +static int +sndstat_unpack_user_nvlbuf(const void *unvlbuf, size_t nbytes, nvlist_t **nvl) +{ + void *nvlbuf; + int err; + + nvlbuf = malloc(nbytes, M_DEVBUF, M_WAITOK); + err = copyin(unvlbuf, nvlbuf, nbytes); + if (err != 0) { + free(nvlbuf, M_DEVBUF); + return (err); + } + *nvl = nvlist_unpack(nvlbuf, nbytes, 0); + free(nvlbuf, M_DEVBUF); + if (nvl == NULL) { + return (EINVAL); + } + + return (0); +} + +static bool +sndstat_dsp_nvlist_is_sane(const nvlist_t *nvlist) +{ + if (!(nvlist_exists_string(nvlist, SNDSTAT_LABEL_DEVNODE) && + nvlist_exists_string(nvlist, SNDSTAT_LABEL_DESC) && + nvlist_exists_number(nvlist, SNDSTAT_LABEL_PCHAN) && + nvlist_exists_number(nvlist, SNDSTAT_LABEL_RCHAN))) + return (false); + + if (nvlist_get_number(nvlist, SNDSTAT_LABEL_PCHAN) > 0) + if (!(nvlist_exists_number(nvlist, SNDSTAT_LABEL_PMINRATE) && + nvlist_exists_number(nvlist, SNDSTAT_LABEL_PMAXRATE) && + nvlist_exists_number(nvlist, SNDSTAT_LABEL_PFMTS))) + return (false); + + if (nvlist_get_number(nvlist, SNDSTAT_LABEL_RCHAN) > 0) + if (!(nvlist_exists_number(nvlist, SNDSTAT_LABEL_RMINRATE) && + nvlist_exists_number(nvlist, SNDSTAT_LABEL_RMAXRATE) && + nvlist_exists_number(nvlist, SNDSTAT_LABEL_RFMTS))) + return (false); + + return (true); + +} + +static int +sndstat_dsp_unpack_nvlist(const nvlist_t *nvlist, struct sndstat_userdev *ud) +{ + const char *nameunit, *devnode, *desc; + unsigned int pchan, rchan; + uint32_t pminrate = 0, pmaxrate = 0; + uint32_t rminrate = 0, rmaxrate = 0; + uint32_t pfmts = 0, rfmts = 0; + nvlist_t *provider_nvl = NULL; + const char *provider; + + devnode = nvlist_get_string(nvlist, SNDSTAT_LABEL_DEVNODE); + if (nvlist_exists_string(nvlist, SNDSTAT_LABEL_NAMEUNIT)) + nameunit = nvlist_get_string(nvlist, SNDSTAT_LABEL_NAMEUNIT); + else + nameunit = devnode; + desc = nvlist_get_string(nvlist, SNDSTAT_LABEL_DESC); + pchan = nvlist_get_number(nvlist, SNDSTAT_LABEL_PCHAN); + rchan = nvlist_get_number(nvlist, SNDSTAT_LABEL_RCHAN); + if (pchan != 0) { + pminrate = nvlist_get_number(nvlist, SNDSTAT_LABEL_PMINRATE); + pmaxrate = nvlist_get_number(nvlist, SNDSTAT_LABEL_PMAXRATE); + pfmts = nvlist_get_number(nvlist, SNDSTAT_LABEL_PFMTS); + } + if (rchan != 0) { + rminrate = nvlist_get_number(nvlist, SNDSTAT_LABEL_RMINRATE); + rmaxrate = nvlist_get_number(nvlist, SNDSTAT_LABEL_RMAXRATE); + rfmts = nvlist_get_number(nvlist, SNDSTAT_LABEL_RFMTS); + } + + provider = dnvlist_get_string(nvlist, SNDSTAT_LABEL_PROVIDER, ""); + if (provider[0] == '\0') + provider = NULL; + + if (provider != NULL && + nvlist_exists_nvlist(nvlist, SNDSTAT_LABEL_PROVIDER_INFO)) { + provider_nvl = nvlist_clone( + nvlist_get_nvlist(nvlist, SNDSTAT_LABEL_PROVIDER_INFO)); + if (provider_nvl == NULL) + return (ENOMEM); + } + + ud->provider = (provider != NULL) ? strdup(provider, M_DEVBUF) : NULL; + ud->devnode = strdup(devnode, M_DEVBUF); + ud->nameunit = strdup(nameunit, M_DEVBUF); + ud->desc = strdup(desc, M_DEVBUF); + ud->pchan = pchan; + ud->rchan = rchan; + ud->pminrate = pminrate; + ud->pmaxrate = pmaxrate; + ud->rminrate = rminrate; + ud->rmaxrate = rmaxrate; + ud->pfmts = pfmts; + ud->rfmts = rfmts; + ud->provider_nvl = provider_nvl; + return (0); +} + +static int +sndstat_add_user_devs(struct sndstat_file *pf, caddr_t data) +{ + int err; + nvlist_t *nvl = NULL; + const nvlist_t * const *dsps; + size_t i, ndsps; + struct sndstat_nvlbuf_arg *arg = (struct sndstat_nvlbuf_arg *)data; + + if ((pf->fflags & FWRITE) == 0) { + err = EPERM; + goto done; + } + + err = sndstat_unpack_user_nvlbuf(arg->buf, arg->nbytes, &nvl); + if (err != 0) + goto done; + + if (!nvlist_exists_nvlist_array(nvl, SNDSTAT_LABEL_DSPS)) { + err = EINVAL; + goto done; + } + dsps = nvlist_get_nvlist_array(nvl, SNDSTAT_LABEL_DSPS, &ndsps); + for (i = 0; i < ndsps; i++) { + if (!sndstat_dsp_nvlist_is_sane(dsps[i])) { + err = EINVAL; + goto done; + } + } + sx_xlock(&pf->lock); + for (i = 0; i < ndsps; i++) { + struct sndstat_userdev *ud = + malloc(sizeof(*ud), M_DEVBUF, M_WAITOK); + err = sndstat_dsp_unpack_nvlist(dsps[i], ud); + if (err) { + sx_unlock(&pf->lock); + goto done; + } + TAILQ_INSERT_TAIL(&pf->userdev_list, ud, link); + } + sx_unlock(&pf->lock); + +done: + nvlist_destroy(nvl); + return (err); +} + +static int +sndstat_flush_user_devs(struct sndstat_file *pf) +{ + if ((pf->fflags & FWRITE) == 0) + return (EPERM); + + sx_xlock(&pf->lock); + sndstat_remove_all_userdevs(pf); + sx_xunlock(&pf->lock); + + return (0); +} + +#ifdef COMPAT_FREEBSD32 +static int +compat_sndstat_get_devs32(struct sndstat_file *pf, caddr_t data) +{ + struct sndstat_nvlbuf_arg32 *arg32 = (struct sndstat_nvlbuf_arg32 *)data; + struct sndstat_nvlbuf_arg arg; + int err; + + arg.buf = (void *)(uintptr_t)arg32->buf; + arg.nbytes = arg32->nbytes; + + err = sndstat_get_devs(pf, (caddr_t)&arg); + if (err == 0) { + arg32->buf = (uint32_t)(uintptr_t)arg.buf; + arg32->nbytes = arg.nbytes; + } + + return (err); +} + +static int +compat_sndstat_add_user_devs32(struct sndstat_file *pf, caddr_t data) +{ + struct sndstat_nvlbuf_arg32 *arg32 = (struct sndstat_nvlbuf_arg32 *)data; + struct sndstat_nvlbuf_arg arg; + int err; + + arg.buf = (void *)(uintptr_t)arg32->buf; + arg.nbytes = arg32->nbytes; + + err = sndstat_add_user_devs(pf, (caddr_t)&arg); + if (err == 0) { + arg32->buf = (uint32_t)(uintptr_t)arg.buf; + arg32->nbytes = arg.nbytes; + } + + return (err); +} +#endif + +static int +sndstat_ioctl( + struct cdev *dev, u_long cmd, caddr_t data, int fflag, struct thread *td) +{ + int err; + struct sndstat_file *pf; + + err = devfs_get_cdevpriv((void **)&pf); + if (err != 0) + return (err); + + switch (cmd) { + case SNDSTAT_GET_DEVS: + err = sndstat_get_devs(pf, data); + break; +#ifdef COMPAT_FREEBSD32 + case SNDSTAT_GET_DEVS32: + if (!SV_CURPROC_FLAG(SV_ILP32)) { + err = ENODEV; + break; + } + err = compat_sndstat_get_devs32(pf, data); + break; +#endif + case SNDSTAT_ADD_USER_DEVS: + err = sndstat_add_user_devs(pf, data); + break; +#ifdef COMPAT_FREEBSD32 + case SNDSTAT_ADD_USER_DEVS32: + if (!SV_CURPROC_FLAG(SV_ILP32)) { + err = ENODEV; + break; + } + err = compat_sndstat_add_user_devs32(pf, data); + break; +#endif + case SNDSTAT_REFRESH_DEVS: + err = sndstat_refresh_devs(pf); + break; + case SNDSTAT_FLUSH_USER_DEVS: + err = sndstat_flush_user_devs(pf); + break; + default: + err = ENODEV; + } + + return (err); +} + +static struct sndstat_userdev * +sndstat_line2userdev(struct sndstat_file *pf, const char *line, int n) +{ + struct sndstat_userdev *ud; + const char *e, *m; + + ud = malloc(sizeof(*ud), M_DEVBUF, M_WAITOK|M_ZERO); + + ud->provider = NULL; + ud->provider_nvl = NULL; + e = strchr(line, ':'); + if (e == NULL) + goto fail; + ud->nameunit = strndup(line, e - line, M_DEVBUF); + ud->devnode = (char *)malloc(e - line + 1, M_DEVBUF, M_WAITOK | M_ZERO); + strlcat(ud->devnode, ud->nameunit, e - line + 1); + line = e + 1; + + e = strchr(line, '<'); + if (e == NULL) + goto fail; + line = e + 1; + e = strrchr(line, '>'); + if (e == NULL) + goto fail; + ud->desc = strndup(line, e - line, M_DEVBUF); + line = e + 1; + + e = strchr(line, '('); + if (e == NULL) + goto fail; + line = e + 1; + e = strrchr(line, ')'); + if (e == NULL) + goto fail; + m = strstr(line, "play"); + if (m != NULL && m < e) + ud->pchan = 1; + m = strstr(line, "rec"); + if (m != NULL && m < e) + ud->rchan = 1; + + return (ud); + +fail: + free(ud->nameunit, M_DEVBUF); + free(ud->devnode, M_DEVBUF); + free(ud->desc, M_DEVBUF); + free(ud, M_DEVBUF); + return (NULL); +} + /************************************************************************/ int @@ -379,14 +1087,26 @@ sndstat_prepare(struct sndstat_file *pf_self) /* append any input from userspace */ k = 0; TAILQ_FOREACH(pf, &sndstat_filelist, entry) { + struct sndstat_userdev *ud; + if (pf == pf_self) continue; - if (pf->out_offset == 0) + sx_xlock(&pf->lock); + if (TAILQ_EMPTY(&pf->userdev_list)) { + sx_unlock(&pf->lock); continue; + } if (!k++) sbuf_printf(s, "Installed devices from userspace:\n"); - sbuf_bcat(s, sbuf_data(&pf->sbuf), - sbuf_len(&pf->sbuf)); + TAILQ_FOREACH(ud, &pf->userdev_list, link) { + const char *caps = (ud->pchan && ud->rchan) ? + "play/rec" : + (ud->pchan ? "play" : (ud->rchan ? "rec" : "")); + sbuf_printf(s, "%s: <%s>", ud->nameunit, ud->desc); + sbuf_printf(s, " (%s)", caps); + sbuf_printf(s, "\n"); + } + sx_unlock(&pf->lock); } if (k == 0) sbuf_printf(s, "No devices installed from userspace.\n"); diff --git a/sys/dev/sound/pcm/sound.h b/sys/dev/sound/pcm/sound.h index cdae5e837cdc..7b4a4d3a46ca 100644 --- a/sys/dev/sound/pcm/sound.h +++ b/sys/dev/sound/pcm/sound.h @@ -64,6 +64,7 @@ #include <sys/poll.h> #include <sys/sbuf.h> #include <sys/soundcard.h> +#include <sys/sndstat.h> #include <sys/sysctl.h> #include <sys/kobj.h> #include <vm/vm.h> diff --git a/sys/sys/param.h b/sys/sys/param.h index 0f0854f359fd..5176dd1e9732 100644 --- a/sys/sys/param.h +++ b/sys/sys/param.h @@ -60,7 +60,7 @@ * in the range 5 to 9. */ #undef __FreeBSD_version -#define __FreeBSD_version 1400005 /* Master, propagated to newvers */ +#define __FreeBSD_version 1400006 /* Master, propagated to newvers */ /* * __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD, diff --git a/sys/sys/sndstat.h b/sys/sys/sndstat.h new file mode 100644 index 000000000000..995d474f8290 --- /dev/null +++ b/sys/sys/sndstat.h @@ -0,0 +1,95 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause-FreeBSD + * + * Copyright (c) 2020 The FreeBSD Foundation + * + * This software was developed by Ka Ho Ng + * under sponsorship from the FreeBSD Foundation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * $FreeBSD$ + */ + +#ifndef _SYS_SNDSTAT_H_ +#define _SYS_SNDSTAT_H_ + +#include <sys/types.h> +#ifndef _IOWR +#include <sys/ioccom.h> +#endif /* !_IOWR */ + +struct sndstat_nvlbuf_arg { + size_t nbytes; /* [IN/OUT] buffer size/number of bytes filled */ + void *buf; /* [OUT] buffer holding a packed nvlist */ +}; + +/* + * Common labels + */ +#define SNDSTAT_LABEL_DSPS "dsps" +#define SNDSTAT_LABEL_FROM_USER "from_user" +#define SNDSTAT_LABEL_PCHAN "pchan" +#define SNDSTAT_LABEL_RCHAN "rchan" +#define SNDSTAT_LABEL_PMINRATE "pminrate" +#define SNDSTAT_LABEL_PMAXRATE "pmaxrate" +#define SNDSTAT_LABEL_RMINRATE "rminrate" +#define SNDSTAT_LABEL_RMAXRATE "rmaxrate" +#define SNDSTAT_LABEL_PFMTS "pfmts" +#define SNDSTAT_LABEL_RFMTS "rfmts" +#define SNDSTAT_LABEL_NAMEUNIT "nameunit" +#define SNDSTAT_LABEL_DEVNODE "devnode" +#define SNDSTAT_LABEL_DESC "desc" +#define SNDSTAT_LABEL_PROVIDER "provider" +#define SNDSTAT_LABEL_PROVIDER_INFO "provider_info" + +/* + * sound(4)-specific labels + */ +#define SNDSTAT_LABEL_SOUND4_PROVIDER "sound(4)" +#define SNDSTAT_LABEL_SOUND4_UNIT "unit" +#define SNDSTAT_LABEL_SOUND4_BITPERFECT "bitperfect" +#define SNDSTAT_LABEL_SOUND4_PVCHAN "pvchan" +#define SNDSTAT_LABEL_SOUND4_RVCHAN "rvchan" + +#define SNDSTAT_REFRESH_DEVS _IO('D', 100) +#define SNDSTAT_GET_DEVS _IOWR('D', 101, struct sndstat_nvlbuf_arg) +#define SNDSTAT_ADD_USER_DEVS _IOWR('D', 102, struct sndstat_nvlbuf_arg) +#define SNDSTAT_FLUSH_USER_DEVS _IO('D', 103) + +#ifdef _KERNEL +#ifdef COMPAT_FREEBSD32 + +struct sndstat_nvlbuf_arg32 { + uint32_t nbytes; + uint32_t buf; +}; + +#define SNDSTAT_GET_DEVS32 \ + _IOC_NEWTYPE(SNDSTAT_GET_DEVS, struct sndstat_nvlbuf_arg32) +#define SNDSTAT_ADD_USER_DEVS32 \ + _IOC_NEWTYPE(SNDSTAT_ADD_USER_DEVS, struct sndstat_nvlbuf_arg32) + +#endif +#endif + +#endif /* !_SYS_SNDSTAT_H_ */ |