diff --git a/share/man/man4/Makefile b/share/man/man4/Makefile --- a/share/man/man4/Makefile +++ b/share/man/man4/Makefile @@ -526,6 +526,7 @@ 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 --- /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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 --- a/sys/dev/sound/pcm/sndstat.c +++ b/sys/dev/sound/pcm/sndstat.c @@ -3,8 +3,12 @@ * * Copyright (c) 2005-2009 Ariff Abdullah * Copyright (c) 2001 Cameron Grant + * 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 +#include +#include +#include +#include +#include +#ifdef COMPAT_FREEBSD32 +#include +#endif + #include #include #include -#include + SND_DECLARE_FILE("$FreeBSD$"); @@ -47,12 +61,14 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ } } 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 @@ /* 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 --- a/sys/dev/sound/pcm/sound.h +++ b/sys/dev/sound/pcm/sound.h @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include diff --git a/sys/sys/param.h b/sys/sys/param.h --- 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 --- /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 +#ifndef _IOWR +#include +#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_ */