Add event queue handling. It triggers activities on events read from
/dev/usb. The actions are specified in the file /etc/usbd.conf. usbd.c: - Add event queue (/dev/usb) handling. - Add comments - Clean up code some more usbd.8: - Update manpage for the new command line flags - Remove a duplicate FreeBSD tag from it). usbd.conf, usbd.conf.5, Makefile: - Add the usbd.conf configuration file and the man page for it. NOTE: MAKEDEV already creates the /dev/usb device tree node, no change needed there anymore.
This commit is contained in:
parent
3cc1fd63ff
commit
7183c3f848
35
etc/usbd.conf
Normal file
35
etc/usbd.conf
Normal file
@ -0,0 +1,35 @@
|
||||
# Configuration file the USB daemon.
|
||||
#
|
||||
# See usbd.conf(5) for the description of the format of the file.
|
||||
|
||||
# Firmware download into the ActiveWire board. After the firmware download is done
|
||||
# the device detaches and reappears as something new and shiny.
|
||||
#
|
||||
device "ActiveWire board, firmware download"
|
||||
product 0x0100
|
||||
vendor 0x0854
|
||||
release 0x0000
|
||||
attach "/usr/local/bin/ezdownload -f /usr/local/share/usb/firmware/0854.0100.0_01.hex"
|
||||
|
||||
# The piece below has to be copied for every drive. It does not work for the
|
||||
# generic case, the umass storage class uses interface drivers. The info for
|
||||
# the interfaces is not yet exported.
|
||||
#
|
||||
device "USB Zip drive"
|
||||
product 0x0001
|
||||
vendor 0x059b
|
||||
release 0x0100
|
||||
attach "/usr/bin/camcontrol rescan bus 0"
|
||||
|
||||
# The entry below is for the Logitech mouse. Replace the product and vendor
|
||||
# id (and the device name of course) with the data for your mouse.
|
||||
#
|
||||
device "Logitech N48 USB mouse"
|
||||
product 0xc001
|
||||
vendor 0x046d
|
||||
attach "/usr/sbin/moused -p /dev/ums0 -I /var/run/moused.ums0.pid"
|
||||
|
||||
# The fallthrough entry: Nothing is specified, nothing is done. And it isn't
|
||||
# necessary at all :-). Just for pretty printing in debugging mode.
|
||||
#
|
||||
device "USB device"
|
@ -2,6 +2,7 @@
|
||||
|
||||
PROG= usbd
|
||||
MAN8= usbd.8
|
||||
MAN5= usbd.conf.5
|
||||
CFLAGS+=-I${.CURDIR}/../../sys
|
||||
|
||||
.include <bsd.prog.mk>
|
||||
|
@ -1,5 +1,4 @@
|
||||
.\" $NetBSD: usbd.8,v 1.2 1998/07/13 11:01:50 augustss Exp $
|
||||
.\" $FreeBSD$
|
||||
.\" Copyright (c) 1998 The NetBSD Foundation, Inc.
|
||||
.\" All rights reserved.
|
||||
.\"
|
||||
@ -53,6 +52,9 @@ handles the USB device attachment and detachment.
|
||||
.Pp
|
||||
The options are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Fl c Ar filename
|
||||
Name of configuration file. The default is
|
||||
.Pa /etc/usbd.conf.
|
||||
.It Fl d
|
||||
Enable debugging to the standard output,
|
||||
and do not disassociate from the controlling terminal.
|
||||
@ -62,22 +64,47 @@ Do one device tree exploration and then exit.
|
||||
Specify the pathname of a USB controller device file.
|
||||
The flag may be repeated to watch more than one USB controller.
|
||||
The default is
|
||||
.Pa /dev/usb0 ,
|
||||
.Pa /dev/usb1 ,
|
||||
.Pa /dev/usb2 ,
|
||||
and
|
||||
.Pa /dev/usb0
|
||||
through
|
||||
.Pa /dev/usb3 .
|
||||
Do not specify the device
|
||||
.Pa /dev/usb
|
||||
here. It is used for events only.
|
||||
.It Fl n
|
||||
Do not handle the event queue on /dev/usb.
|
||||
.It Fl t Ar timeout
|
||||
Set the timeout interval (in seconds) before an exploration happens
|
||||
without being triggered by a connect or disconnect.
|
||||
A timeout of 0 means that there is no timeout. The default is 30.
|
||||
.It Fl v
|
||||
Be verbose.
|
||||
Be verbose. Repeating the flag makes
|
||||
.Nm usbd
|
||||
more verbose.
|
||||
.El
|
||||
.Sh FILES
|
||||
.Bl -tag -width /etc/usbd.conf -compact
|
||||
.It Pa /etc/usbd.conf
|
||||
.It /dev/usb
|
||||
.It /dev/usb0
|
||||
.It /dev/usb1
|
||||
.It etc.
|
||||
.Sh SEE ALSO
|
||||
.Xr usb 4
|
||||
.Xr usb 4 ,
|
||||
.Xr usbd.conf 8
|
||||
.Sh HISTORY
|
||||
The
|
||||
.Nm
|
||||
command appeared in
|
||||
.Nx 1.4 .
|
||||
.Nx 4.0 .
|
||||
.Sh AUTHORS
|
||||
The
|
||||
.Nm
|
||||
driver was written by
|
||||
.An Lennart Augustsson Aq augustss@carlstedt.se
|
||||
for the
|
||||
.Nx
|
||||
project. The event queue handling in
|
||||
.Nm usbd
|
||||
was added by
|
||||
.An Nick Hibma Aq n_hibma@freebsd.org .
|
||||
|
||||
|
@ -37,55 +37,730 @@
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <err.h>
|
||||
/* USBD creates 'threads' in the kernel, used for doing discovery when a
|
||||
* device has attached or detached. This functionality should be removed
|
||||
* once kernel threads have been added to the kernel.
|
||||
* It also handles the event queue, and executing commands based on those
|
||||
* events.
|
||||
*
|
||||
* See usbd(8).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <ctype.h>
|
||||
#include <signal.h>
|
||||
#include <paths.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/time.h>
|
||||
#if defined(__FreeBSD__)
|
||||
#include <sys/ioctl.h>
|
||||
#endif
|
||||
#include <sys/errno.h>
|
||||
#include <sys/queue.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <dev/usb/usb.h>
|
||||
|
||||
/* default name of configuration file
|
||||
*/
|
||||
|
||||
#define CONFIGFILE "/etc/usbd.conf"
|
||||
|
||||
/* the name of the device spitting out usb attach/detach events as well as
|
||||
* the prefix for the individual busses (used as a semi kernel thread).
|
||||
*/
|
||||
#define USBDEV "/dev/usb"
|
||||
#define USBDEV "/dev/usb"
|
||||
|
||||
/* Maximum number of USB busses expected to be in a system
|
||||
* XXX should be replaced by dynamic allocation.
|
||||
*/
|
||||
#define MAXUSBDEV 4
|
||||
#define MAXUSBDEV 4
|
||||
|
||||
/*
|
||||
* Sometimes a device does not respond in time for interrupt
|
||||
/* Sometimes a device does not respond in time for interrupt
|
||||
* driven explore to find it. Therefore we run an exploration
|
||||
* at regular intervals to catch those.
|
||||
*/
|
||||
#define TIMEOUT 30
|
||||
#define TIMEOUT 30
|
||||
|
||||
/* The wildcard used in actions for strings and integers
|
||||
*/
|
||||
#define WILDCARD_STRING NULL
|
||||
#define WILDCARD_INT -1
|
||||
|
||||
|
||||
extern char *__progname;
|
||||
extern char *__progname; /* name of program */
|
||||
|
||||
char *configfile = CONFIGFILE; /* name of configuration file */
|
||||
|
||||
char *devs[MAXUSBDEV]; /* device names */
|
||||
int fds[MAXUSBDEV]; /* file descriptors for USBDEV\d+ */
|
||||
int ndevs = 0; /* number of entries in fds / devs */
|
||||
int fd = -1; /* file descriptor for USBDEV */
|
||||
|
||||
int lineno;
|
||||
int verbose = 0; /* print message on what it is doing */
|
||||
|
||||
typedef struct event_name_s {
|
||||
int type; /* event number (from usb.h) */
|
||||
char *name;
|
||||
} event_name_t;
|
||||
|
||||
event_name_t event_names[] = {
|
||||
{USB_EVENT_ATTACH, "attach"},
|
||||
{USB_EVENT_DETACH, "detach"},
|
||||
{0, NULL} /* NULL indicates end of list, not 0 */
|
||||
};
|
||||
|
||||
#define DEVICE_FIELD 0 /* descriptive field */
|
||||
|
||||
#define PRODUCT_FIELD 1 /* selective fields */
|
||||
#define VENDOR_FIELD 2
|
||||
#define RELEASE_FIELD 3
|
||||
#define CLASS_FIELD 4
|
||||
#define SUBCLASS_FIELD 5
|
||||
#define PROTOCOL_FIELD 6
|
||||
|
||||
#define ATTACH_FIELD 8 /* command fields */
|
||||
#define DETACH_FIELD 9
|
||||
|
||||
|
||||
typedef struct action_s {
|
||||
char *name; /* descriptive string */
|
||||
|
||||
int product; /* selection criteria */
|
||||
int vendor;
|
||||
int release;
|
||||
int class;
|
||||
int subclass;
|
||||
int protocol;
|
||||
|
||||
char *attach; /* commands to execute */
|
||||
char *detach;
|
||||
|
||||
STAILQ_ENTRY(action_s) next;
|
||||
} action_t;
|
||||
|
||||
STAILQ_HEAD(action_list, action_s) actions = STAILQ_HEAD_INITIALIZER(actions);
|
||||
|
||||
|
||||
/* the function returns 0 for failure, 1 for all arguments found and 2 for
|
||||
* arguments left over in trail.
|
||||
*/
|
||||
typedef int (*config_field_fn) __P((action_t *action, char *args,
|
||||
char **trail));
|
||||
|
||||
int set_device_field(action_t *action, char *args, char **trail);
|
||||
int set_product_field(action_t *action, char *args, char **trail);
|
||||
int set_vendor_field(action_t *action, char *args, char **trail);
|
||||
int set_release_field(action_t *action, char *args, char **trail);
|
||||
int set_class_field(action_t *action, char *args, char **trail);
|
||||
int set_subclass_field(action_t *action, char *args, char **trail);
|
||||
int set_protocol_field(action_t *action, char *args, char **trail);
|
||||
int set_attach_field(action_t *action, char *args, char **trail);
|
||||
int set_detach_field(action_t *action, char *args, char **trail);
|
||||
|
||||
/* the list of fields supported in an entry */
|
||||
typedef struct config_field_s {
|
||||
int event;
|
||||
char *name;
|
||||
config_field_fn function;
|
||||
} config_field_t;
|
||||
|
||||
config_field_t config_fields[] = {
|
||||
{DEVICE_FIELD, "device", set_device_field},
|
||||
|
||||
{PRODUCT_FIELD, "product", set_product_field},
|
||||
{VENDOR_FIELD, "vendor", set_vendor_field},
|
||||
{RELEASE_FIELD, "release", set_release_field},
|
||||
{CLASS_FIELD, "class", set_class_field},
|
||||
{SUBCLASS_FIELD, "subclass", set_subclass_field},
|
||||
{PROTOCOL_FIELD, "protocol", set_protocol_field},
|
||||
|
||||
{ATTACH_FIELD, "attach", set_attach_field},
|
||||
{DETACH_FIELD, "detach", set_detach_field},
|
||||
|
||||
{0, NULL, NULL} /* NULL is EOL marker, not the 0 */
|
||||
};
|
||||
|
||||
|
||||
/* prototypes for some functions */
|
||||
void print_event __P((struct usb_event *event));
|
||||
void print_action __P((action_t *action, int i));
|
||||
void print_actions __P((void));
|
||||
action_t *find_action __P((struct usb_device_info *devinfo));
|
||||
|
||||
void usage(void);
|
||||
|
||||
void
|
||||
usage(void)
|
||||
{
|
||||
fprintf(stderr, "Usage: %s [-d] [-e] [-f dev] [-t timeout] [-v]\n",
|
||||
fprintf(stderr, "Usage: %s [-d] [-v] [-t timeout] [-e] [-f dev]\n"
|
||||
" [-n] [-c config]\n",
|
||||
__progname);
|
||||
fprintf(stderr, " -d for debugging\n");
|
||||
fprintf(stderr, " -e only do 1 explore\n");
|
||||
fprintf(stderr, " -f dev for example /dev/usb0, "
|
||||
"and can be specified multiple times.");
|
||||
fprintf(stderr, " -t timeout timeout between explores\n");
|
||||
fprintf(stderr, " -v verbose output\n");
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
/* generic helper functions for the functions to set the fields of actions */
|
||||
int
|
||||
get_string(char *src, char **rdst, char **rsrc)
|
||||
{
|
||||
/* Takes the first string from src, taking quoting into account.
|
||||
* rsrc (if not NULL) is set to the first byte not included in the
|
||||
* string returned in rdst.
|
||||
*
|
||||
* Input is:
|
||||
* src = 'fir"st \'par"t second part';
|
||||
* Returned is:
|
||||
* *dst = 'hello \'world';
|
||||
* if (rsrc != NULL)
|
||||
* *rsrc = 'second part';
|
||||
*
|
||||
* Notice the fact that the single quote enclosed in double quotes is
|
||||
* returned. Also notice that before second part there is more than
|
||||
* one space, which is removed in rsrc.
|
||||
*
|
||||
* The string in src is not modified.
|
||||
*/
|
||||
|
||||
char *dst; /* destination string */
|
||||
int i; /* index into src */
|
||||
int j; /* index into dst */
|
||||
int quoted = 0; /* 1 for single, 2 for double quoted */
|
||||
|
||||
dst = malloc(strlen(src)); /* XXX allocation is too big, realloc?*/
|
||||
if (dst == NULL) { /* should not happen, really */
|
||||
fprintf(stderr, "%s:%d: Out of memory\n", configfile, lineno);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
/* find the end of the current string. If quotes are found the search
|
||||
* continues until the corresponding quote is found.
|
||||
* So,
|
||||
* hel'lo" "wor'ld
|
||||
* represents the string
|
||||
* hello" "world
|
||||
* and not (hello world).
|
||||
*/
|
||||
for (i = 0, j = 0; i < strlen(src); i++) {
|
||||
if (src[i] == '\'' && (quoted == 0 || quoted == 1)) {
|
||||
quoted = (quoted? 0:1);
|
||||
} else if (src[i] == '"' && (quoted == 0 || quoted == 2)) {
|
||||
quoted = (quoted? 0:2);
|
||||
} else if (isspace(src[i]) && !quoted) {
|
||||
/* found a space outside quotes -> terminates src */
|
||||
break;
|
||||
} else {
|
||||
dst[j++] = src[i]; /* copy character */
|
||||
}
|
||||
}
|
||||
|
||||
/* quotes being left open? */
|
||||
if (quoted) {
|
||||
fprintf(stderr, "%s:%d: Missing %s quote at end of '%s'\n",
|
||||
configfile, lineno,
|
||||
(quoted == 1? "single":"double"), src);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
/* skip whitespace for second part */
|
||||
for (/*i is set*/; i < strlen(src) && isspace(src[i]); i++)
|
||||
; /* nop */
|
||||
|
||||
dst[j] = '\0'; /* make sure it's NULL terminated */
|
||||
|
||||
*rdst = dst; /* and return the pointers */
|
||||
if (rsrc != NULL) /* if info wanted */
|
||||
*rsrc = &src[i];
|
||||
|
||||
if (*dst == '\0') { /* empty string */
|
||||
return 0;
|
||||
} else if (src[i] == '\0') { /* completely used (1 argument) */
|
||||
return 1;
|
||||
} else { /* 2 or more args, *rsrc is rest */
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
get_integer(char *src, int *dst, char **rsrc)
|
||||
{
|
||||
char *endptr;
|
||||
|
||||
/* Converts str to a number. If one argument was found in
|
||||
* str, 1 is returned and *dst is set to the value of the integer.
|
||||
* If 2 or more arguments were presented, 2 is returned,
|
||||
* *dst is set to the converted value and rsrc, if not null, points
|
||||
* at the start of the next argument (whitespace skipped).
|
||||
* Else 0 is returned and nothing else is valid.
|
||||
*/
|
||||
|
||||
if (src == NULL || *src == '\0') /* empty src */
|
||||
return(0);
|
||||
|
||||
*dst = (int) strtol(src, &endptr, 0);
|
||||
|
||||
/* skip over whitespace of second argument */
|
||||
while (isspace(*endptr))
|
||||
endptr++;
|
||||
|
||||
if (rsrc)
|
||||
*rsrc = endptr;
|
||||
|
||||
if (isspace(*endptr)) { /* partial match, 2 or more arguments */
|
||||
return(2);
|
||||
} else if (*endptr == '\0') { /* full match, 1 argument */
|
||||
return(1);
|
||||
} else { /* invalid src, no match */
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* functions to set the fields of the actions appropriately */
|
||||
int
|
||||
set_device_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_string(args, &action->name, trail));
|
||||
}
|
||||
int
|
||||
set_product_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_integer(args, &action->product, trail));
|
||||
}
|
||||
int
|
||||
set_vendor_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_integer(args, &action->vendor, trail));
|
||||
}
|
||||
int
|
||||
set_release_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_integer(args, &action->release, trail));
|
||||
}
|
||||
int
|
||||
set_class_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_integer(args, &action->class, trail));
|
||||
}
|
||||
int
|
||||
set_subclass_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_integer(args, &action->subclass, trail));
|
||||
}
|
||||
int
|
||||
set_protocol_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_integer(args, &action->protocol, trail));
|
||||
}
|
||||
int
|
||||
set_attach_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_string(args, &action->attach, trail));
|
||||
}
|
||||
int
|
||||
set_detach_field(action_t *action, char *args, char **trail)
|
||||
{
|
||||
return(get_string(args, &action->detach, trail));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
read_configuration(void)
|
||||
{
|
||||
FILE *file; /* file descriptor */
|
||||
char *line; /* current line */
|
||||
char *linez; /* current line, NULL terminated */
|
||||
char *field; /* first part, the field name */
|
||||
char *args; /* second part, arguments */
|
||||
char *trail; /* remaining part after parsing, should be '' */
|
||||
int len; /* length of current line */
|
||||
int i,j; /* loop counters */
|
||||
action_t *action = NULL; /* current action */
|
||||
|
||||
file = fopen(configfile, "r");
|
||||
if (file == NULL) {
|
||||
fprintf(stderr, "%s: Could not open for reading, %s\n",
|
||||
configfile, strerror(errno));
|
||||
exit(2);
|
||||
}
|
||||
|
||||
for (lineno = 1; /* nop */;lineno++) {
|
||||
|
||||
line = fgetln(file, &len);
|
||||
if (line == NULL) {
|
||||
if (feof(file)) /* EOF */
|
||||
break;
|
||||
if (ferror(file)) {
|
||||
fprintf(stderr, "%s:%d: Could not read, %s\n",
|
||||
configfile, lineno, strerror(errno));
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
/* skip initial spaces */
|
||||
while (*line != '\0' && isspace(*line)) {
|
||||
line++;
|
||||
len--;
|
||||
}
|
||||
|
||||
if (len == 0) /* empty line */
|
||||
continue;
|
||||
if (line[0] == '#') /* comment line */
|
||||
continue;
|
||||
|
||||
/* make a NULL terminated copy of the string */
|
||||
linez = malloc(len+1);
|
||||
if (linez == NULL) {
|
||||
fprintf(stderr, "%s:%d: Out of memory\n",
|
||||
configfile, lineno);
|
||||
exit(2);
|
||||
}
|
||||
strncpy(linez, line, len);
|
||||
linez[len+1] = '\0';
|
||||
|
||||
/* find the end of the current word (is field), that's the
|
||||
* start of the arguments
|
||||
*/
|
||||
field = linez;
|
||||
args = linez;
|
||||
while (*args != '\0' && !isspace(*args))
|
||||
args++;
|
||||
|
||||
/* If arguments is not the empty string, NULL terminate the
|
||||
* field and move the argument pointer to the first character
|
||||
* of the arguments.
|
||||
* If arguments is the empty string field and arguments both
|
||||
* are terminated (strlen(field) >= 0, strlen(arguments) == 0).
|
||||
*/
|
||||
if (*args != '\0') {
|
||||
*args = '\0';
|
||||
args++;
|
||||
}
|
||||
|
||||
/* Skip initial spaces */
|
||||
while (*args != '\0' && isspace(*args))
|
||||
args++;
|
||||
|
||||
/* Cut off trailing whitespace */
|
||||
for (i = 0, j = 0; args[i] != '\0'; i++)
|
||||
if (!isspace(args[i]))
|
||||
j = i+1;
|
||||
args[j] = '\0';
|
||||
|
||||
/* We now have the field and the argument separated into
|
||||
* two strings that are NULL terminated
|
||||
*/
|
||||
|
||||
/* If the field is 'device' we have to start a new action. */
|
||||
if (strcmp(field, "device") == 0) {
|
||||
/* Allocate a new action and set defaults */
|
||||
action = malloc(sizeof(*action));
|
||||
if (action == NULL) {
|
||||
fprintf(stderr, "%s:%d: Out of memory\n",
|
||||
configfile, lineno);
|
||||
exit(2);
|
||||
}
|
||||
memset(action, 0, sizeof(*action));
|
||||
action->product = WILDCARD_INT;
|
||||
action->vendor = WILDCARD_INT;
|
||||
action->release = WILDCARD_INT;
|
||||
action->class = WILDCARD_INT;
|
||||
action->subclass = WILDCARD_INT;
|
||||
action->protocol = WILDCARD_INT;
|
||||
|
||||
/* Add it to the end of the list to preserve order */
|
||||
STAILQ_INSERT_TAIL(&actions, action, next);
|
||||
}
|
||||
|
||||
if (action == NULL) {
|
||||
line[len] = '\0'; /* XXX zero terminate */
|
||||
fprintf(stderr, "%s:%d: Doesn't start with 'device' "
|
||||
"but '%s'\n", configfile, lineno, field);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
for (i = 0; config_fields[i].name ; i++) {
|
||||
/* does the field name match? */
|
||||
if (strcmp(config_fields[i].name, field) == 0) {
|
||||
/* execute corresponding set-field function */
|
||||
if ((config_fields[i].function)(action, args,
|
||||
&trail)
|
||||
!= 1) {
|
||||
fprintf(stderr,"%s:%d: "
|
||||
"Syntax error in '%s'\n",
|
||||
configfile, lineno, linez);
|
||||
exit(2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (config_fields[i].name == NULL) { /* Reached end of list*/
|
||||
fprintf(stderr, "%s:%d: Unknown field '%s'\n",
|
||||
configfile, lineno, field);
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
|
||||
if (verbose >= 2)
|
||||
print_actions();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
print_event(struct usb_event *event)
|
||||
{
|
||||
int i;
|
||||
struct timespec *timespec = &event->ue_time;
|
||||
struct usb_device_info *devinfo = &event->ue_device;
|
||||
|
||||
printf("%s: ", __progname);
|
||||
for (i = 0; event_names[i].name != NULL; i++) {
|
||||
if (event->ue_type == event_names[i].type) {
|
||||
printf("%s event", event_names[i].name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (event_names[i].name == NULL)
|
||||
printf("unknown event %d", event->ue_type);
|
||||
|
||||
printf(" at %ld.%09ld, %s, %s:\n",
|
||||
timespec->tv_sec, timespec->tv_nsec,
|
||||
devinfo->product, devinfo->vendor);
|
||||
|
||||
printf(" prdct=0x%04x vndr=0x%04x rlse=0x%04x "
|
||||
"clss=0x%04x subclss=0x%04x prtcl=0x%04x\n",
|
||||
devinfo->productNo, devinfo->vendorNo, devinfo->releaseNo,
|
||||
devinfo->class, devinfo->subclass, devinfo->protocol);
|
||||
}
|
||||
|
||||
void
|
||||
print_action(action_t *action, int i)
|
||||
{
|
||||
if (action == NULL)
|
||||
return;
|
||||
|
||||
printf("%s: action %d: %s\n",
|
||||
__progname, i,
|
||||
(action->name? action->name:""));
|
||||
if (action->product != WILDCARD_INT ||
|
||||
action->vendor != WILDCARD_INT ||
|
||||
action->release != WILDCARD_INT ||
|
||||
action->class != WILDCARD_INT ||
|
||||
action->subclass != WILDCARD_INT ||
|
||||
action->protocol != WILDCARD_INT)
|
||||
printf(" ");
|
||||
if (action->product != WILDCARD_INT)
|
||||
printf(" prdct=0x%04x", action->product);
|
||||
if (action->vendor != WILDCARD_INT)
|
||||
printf(" vndr=0x%04x", action->vendor);
|
||||
if (action->release != WILDCARD_INT)
|
||||
printf(" rlse=0x%04x", action->release);
|
||||
if (action->class != WILDCARD_INT)
|
||||
printf(" clss=0x%04x", action->class);
|
||||
if (action->subclass != WILDCARD_INT)
|
||||
printf(" subclss=0x%04x", action->subclass);
|
||||
if (action->protocol != WILDCARD_INT)
|
||||
printf(" prtcl=0x%04x", action->protocol);
|
||||
if (action->product != WILDCARD_INT ||
|
||||
action->vendor != WILDCARD_INT ||
|
||||
action->release != WILDCARD_INT ||
|
||||
action->class != WILDCARD_INT ||
|
||||
action->subclass != WILDCARD_INT ||
|
||||
action->protocol != WILDCARD_INT)
|
||||
printf("\n");
|
||||
|
||||
if (action->attach != NULL)
|
||||
printf("%s: attach='%s'\n",
|
||||
__progname, action->attach);
|
||||
if (action->detach != NULL)
|
||||
printf("%s: detach='%s'\n",
|
||||
__progname, action->detach);
|
||||
}
|
||||
|
||||
void
|
||||
print_actions()
|
||||
{
|
||||
int i = 0;
|
||||
action_t *action;
|
||||
|
||||
STAILQ_FOREACH(action, &actions, next)
|
||||
print_action(action, ++i);
|
||||
|
||||
printf("%s: %d action%s\n", __progname, i, (i == 1? "":"s"));
|
||||
}
|
||||
|
||||
action_t *
|
||||
find_action(struct usb_device_info *devinfo)
|
||||
{
|
||||
action_t *action;
|
||||
|
||||
STAILQ_FOREACH(action, &actions, next) {
|
||||
if ((action->product == WILDCARD_INT ||
|
||||
action->product == devinfo->productNo) &&
|
||||
(action->vendor == WILDCARD_INT ||
|
||||
action->vendor == devinfo->vendorNo) &&
|
||||
(action->release == WILDCARD_INT ||
|
||||
action->release == devinfo->releaseNo) &&
|
||||
(action->class == WILDCARD_INT ||
|
||||
action->class == devinfo->class) &&
|
||||
(action->subclass == WILDCARD_INT ||
|
||||
action->subclass == devinfo->subclass) &&
|
||||
(action->protocol == WILDCARD_INT ||
|
||||
action->protocol == devinfo->protocol)) {
|
||||
/* found match !*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
printf("%s: Found action '%s' for %s, %s\n",
|
||||
__progname, action->name,
|
||||
devinfo->product, devinfo->vendor);
|
||||
return(action);
|
||||
}
|
||||
|
||||
void
|
||||
execute_command(char *cmd)
|
||||
{
|
||||
pid_t pid;
|
||||
int pstat;
|
||||
struct sigaction ign, intact, quitact;
|
||||
sigset_t newsigblock, oldsigblock;
|
||||
int status;
|
||||
int i;
|
||||
|
||||
if (verbose)
|
||||
printf("%s: Executing '%s'\n", __progname, cmd);
|
||||
if (cmd == NULL)
|
||||
return;
|
||||
|
||||
/* The code below is directly taken from the system(3) call.
|
||||
* Added to it is the closing of open file descriptors.
|
||||
*/
|
||||
/*
|
||||
* Ignore SIGINT and SIGQUIT, block SIGCHLD. Remember to save
|
||||
* existing signal dispositions.
|
||||
*/
|
||||
ign.sa_handler = SIG_IGN;
|
||||
(void) sigemptyset(&ign.sa_mask);
|
||||
ign.sa_flags = 0;
|
||||
(void) sigaction(SIGINT, &ign, &intact);
|
||||
(void) sigaction(SIGQUIT, &ign, &quitact);
|
||||
(void) sigemptyset(&newsigblock);
|
||||
(void) sigaddset(&newsigblock, SIGCHLD);
|
||||
(void) sigprocmask(SIG_BLOCK, &newsigblock, &oldsigblock);
|
||||
pid = fork();
|
||||
if (pid == -1) {
|
||||
fprintf(stderr, "%s: fork failed, %s\n",
|
||||
__progname, strerror(errno));
|
||||
} else if (pid == 0) {
|
||||
/* child here */
|
||||
|
||||
/* close all open file handles for USBDEV\d* devices */
|
||||
for (i = 0; i < ndevs; i++)
|
||||
close(fds[i]); /* USBDEV\d+ */
|
||||
close(fd); /* USBDEV */
|
||||
|
||||
/* Restore original signal dispositions and exec the command. */
|
||||
(void) sigaction(SIGINT, &intact, NULL);
|
||||
(void) sigaction(SIGQUIT, &quitact, NULL);
|
||||
(void) sigprocmask(SIG_SETMASK, &oldsigblock, NULL);
|
||||
|
||||
execl(_PATH_BSHELL, "sh", "-c", cmd, (char *)NULL);
|
||||
|
||||
exit(127);
|
||||
} else {
|
||||
/* parent here */
|
||||
do {
|
||||
pid = waitpid(pid, &pstat, 0);
|
||||
} while (pid == -1 && errno == EINTR);
|
||||
}
|
||||
(void) sigaction(SIGINT, &intact, NULL);
|
||||
(void) sigaction(SIGQUIT, &quitact, NULL);
|
||||
(void) sigprocmask(SIG_SETMASK, &oldsigblock, NULL);
|
||||
|
||||
if (pid == -1) {
|
||||
fprintf(stderr, "%s: waitpid returned: %s\n",
|
||||
__progname, strerror(errno));
|
||||
} else if (pid == 0) {
|
||||
fprintf(stderr, "%s: waitpid returned 0 ?!\n",
|
||||
__progname);
|
||||
} else {
|
||||
if (status == -1) {
|
||||
fprintf(stderr, "%s: Could not start '%s'\n",
|
||||
__progname, cmd);
|
||||
} else if (status == 127) {
|
||||
fprintf(stderr, "%s: Shell failed for '%s'\n",
|
||||
__progname, cmd);
|
||||
} else if (WIFEXITED(status)) {
|
||||
fprintf(stderr, "%s: '%s' returned %d\n",
|
||||
__progname, cmd, WEXITSTATUS(status));
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
fprintf(stderr, "%s: '%s' caught signal %d\n",
|
||||
__progname, cmd, WTERMSIG(status));
|
||||
} else if (verbose) {
|
||||
printf("%s: '%s' is done\n", __progname, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
process_event_queue(int fd)
|
||||
{
|
||||
struct usb_event event;
|
||||
int len;
|
||||
action_t *action;
|
||||
|
||||
for (;;) {
|
||||
len = read(fd, &event, sizeof(event));
|
||||
if (len == -1) {
|
||||
if (errno == EWOULDBLOCK) {
|
||||
/* no more events */
|
||||
break;
|
||||
} else {
|
||||
fprintf(stderr,"%s: Could not read event, %s\n",
|
||||
__progname, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if (len == 0)
|
||||
break;
|
||||
if (len != sizeof(event)) {
|
||||
fprintf(stderr, "partial read on %s\n", USBDEV);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* we seem to have gotten a valid event */
|
||||
|
||||
if (verbose)
|
||||
print_event(&event);
|
||||
|
||||
/* handle the event appropriately */
|
||||
switch (event.ue_type) {
|
||||
case USB_EVENT_ATTACH:
|
||||
case USB_EVENT_DETACH:
|
||||
action = find_action(&event.ue_device);
|
||||
if (action == NULL)
|
||||
break;
|
||||
else if (verbose >= 2)
|
||||
print_action(action, 0);
|
||||
|
||||
if (event.ue_type == USB_EVENT_ATTACH && action->attach)
|
||||
execute_command(action->attach);
|
||||
if (event.ue_type == USB_EVENT_DETACH && action->detach)
|
||||
execute_command(action->detach);
|
||||
|
||||
break;
|
||||
default:
|
||||
printf("Unknown USB event %d\n", event.ue_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
@ -93,32 +768,35 @@ main(int argc, char **argv)
|
||||
int ch; /* getopt option */
|
||||
extern char *optarg; /* from getopt */
|
||||
extern int optind; /* from getopt */
|
||||
char *devs[MAXUSBDEV]; /* device names */
|
||||
int ndevs = 0; /* number of devices found */
|
||||
int verbose = 0; /* print message on what it is doing */
|
||||
int debug = 0; /* print debugging output */
|
||||
int explore = 0; /* don't do only explore */
|
||||
int explore_once = 0; /* don't do only explore */
|
||||
int handle_events = 1; /* do handle the event queue */
|
||||
int maxfd; /* maximum fd in use */
|
||||
char buf[50]; /* for creation of the filename */
|
||||
int fds[MAXUSBDEV]; /* open filedescriptors */
|
||||
fd_set fdset;
|
||||
int itimo = TIMEOUT; /* timeout for select */
|
||||
struct timeval timo;
|
||||
fd_set r,w;
|
||||
int itimeout = TIMEOUT; /* timeout for select */
|
||||
struct timeval tv;
|
||||
|
||||
while ((ch = getopt(argc, argv, "def:t:v")) != -1) {
|
||||
while ((ch = getopt(argc, argv, "c:def:nt:v")) != -1) {
|
||||
switch(ch) {
|
||||
case 'c':
|
||||
configfile = strdup(optarg);
|
||||
break;
|
||||
case 'd':
|
||||
debug++;
|
||||
break;
|
||||
case 'e':
|
||||
explore++;
|
||||
explore_once = 1;
|
||||
break;
|
||||
case 'f':
|
||||
if (ndevs < MAXUSBDEV)
|
||||
devs[ndevs++] = optarg;
|
||||
break;
|
||||
case 'n':
|
||||
handle_events = 0;
|
||||
break;
|
||||
case 't':
|
||||
itimo = atoi(optarg);
|
||||
itimeout = atoi(optarg);
|
||||
break;
|
||||
case 'v':
|
||||
verbose++;
|
||||
@ -131,16 +809,16 @@ main(int argc, char **argv)
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
/* open all the files /dev/usb\d+ or specified with -f */
|
||||
maxfd = 0;
|
||||
if (ndevs == 0) {
|
||||
/* open all the USBDEVS\d+ devices */
|
||||
for (i = 0; i < MAXUSBDEV; i++) {
|
||||
sprintf(buf, "%s%d", USBDEV, i);
|
||||
fds[ndevs] = open(buf, O_RDWR);
|
||||
if (fds[ndevs] >= 0) {
|
||||
devs[ndevs] = strdup(buf);
|
||||
if (verbose)
|
||||
printf("%s: opening %s\n",
|
||||
printf("%s: opened %s\n",
|
||||
__progname, devs[ndevs]);
|
||||
if (fds[ndevs] > maxfd)
|
||||
maxfd = fds[ndevs];
|
||||
@ -148,14 +826,23 @@ main(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* open all the files specified with -f */
|
||||
for (i = 0; i < ndevs; i++) {
|
||||
fds[i] = open(devs[i], O_RDWR);
|
||||
if (fds[i] < 0)
|
||||
err(1, "%s", devs[i]);
|
||||
else if (fds[i] > maxfd)
|
||||
maxfd = fds[i];
|
||||
if (fds[i] < 0) {
|
||||
fprintf(stderr, "%s: Could not open %s, %s\n",
|
||||
__progname, devs[i], strerror(errno));
|
||||
exit(1);
|
||||
} else {
|
||||
if (verbose)
|
||||
printf("%s: opened %s\n",
|
||||
__progname, devs[i]);
|
||||
if (fds[i] > maxfd)
|
||||
maxfd = fds[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ndevs == 0) {
|
||||
fprintf(stderr, "No USB host controllers found\n");
|
||||
exit(1);
|
||||
@ -163,37 +850,84 @@ main(int argc, char **argv)
|
||||
|
||||
|
||||
/* Do the explore once and exit */
|
||||
if (explore) {
|
||||
if (explore_once) {
|
||||
for (i = 0; i < ndevs; i++) {
|
||||
error = ioctl(fds[i], USB_DISCOVER);
|
||||
if (error < 0)
|
||||
err(1, "USB_DISCOVER");
|
||||
if (error < 0) {
|
||||
fprintf(stderr, "%s: ioctl(%s, USB_DISCOVER) "
|
||||
"failed, %s\n",
|
||||
__progname, devs[i], strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (handle_events) {
|
||||
if (verbose)
|
||||
printf("%s: reading configuration file %s\n",
|
||||
__progname, configfile);
|
||||
read_configuration();
|
||||
|
||||
fd = open(USBDEV, O_RDONLY | O_NONBLOCK);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "%s: Could not open %s, %s\n",
|
||||
__progname, USBDEV, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
if (verbose)
|
||||
printf("%s: opened %s\n", __progname, USBDEV);
|
||||
if (fd > maxfd)
|
||||
maxfd = fd;
|
||||
|
||||
process_event_queue(fd); /* dequeue the initial events */
|
||||
}
|
||||
|
||||
/* move to the background */
|
||||
if (!debug)
|
||||
daemon(0, 0);
|
||||
|
||||
/* start select on all the open file descriptors */
|
||||
for (;;) {
|
||||
FD_ZERO(&fdset);
|
||||
FD_ZERO(&r);
|
||||
FD_ZERO(&w);
|
||||
if (handle_events)
|
||||
FD_SET(fd, &r); /* device USBDEV */
|
||||
for (i = 0; i < ndevs; i++)
|
||||
FD_SET(fds[i], &fdset);
|
||||
timo.tv_usec = 0;
|
||||
timo.tv_sec = itimo;
|
||||
error = select(maxfd+1, 0, &fdset, 0, itimo ? &timo : 0);
|
||||
if (error < 0)
|
||||
warn("select failed\n");
|
||||
for (i = 0; i < ndevs; i++)
|
||||
if (error == 0 || FD_ISSET(fds[i], &fdset)) {
|
||||
if (verbose)
|
||||
FD_SET(fds[i], &w); /* device USBDEV\d+ */
|
||||
tv.tv_usec = 0;
|
||||
tv.tv_sec = itimeout;
|
||||
error = select(maxfd+1, &r, &w, 0, itimeout ? &tv : 0);
|
||||
if (error < 0) {
|
||||
fprintf(stderr, "%s: Select failed, %s\n",
|
||||
__progname, strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/* USBDEV\d+ devices have signaled change, do a usb_discover */
|
||||
for (i = 0; i < ndevs; i++) {
|
||||
if (error == 0 || FD_ISSET(fds[i], &w)) {
|
||||
if (verbose >= 2)
|
||||
printf("%s: doing %sdiscovery on %s\n",
|
||||
__progname,
|
||||
(error? "":"timeout "), devs[i]);
|
||||
if (ioctl(fds[i], USB_DISCOVER) < 0)
|
||||
err(1, "USB_DISCOVER");
|
||||
if (ioctl(fds[i], USB_DISCOVER) < 0) {
|
||||
fprintf(stderr, "%s: ioctl(%s, "
|
||||
"USB_DISCOVER) failed, %s\n",
|
||||
__progname, devs[i],
|
||||
strerror(errno));
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* check the event queue */
|
||||
if (handle_events && (FD_ISSET(fd, &r) || error == 0)) {
|
||||
if (verbose >= 2)
|
||||
printf("%s: processing event queue %son %s\n",
|
||||
__progname,
|
||||
(error? "":"due to timeout "), USBDEV);
|
||||
process_event_queue(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
128
usr.sbin/usbd/usbd.conf.5
Normal file
128
usr.sbin/usbd/usbd.conf.5
Normal file
@ -0,0 +1,128 @@
|
||||
.\"
|
||||
.\" Copyright (c) 1999 Nick Hibma. All rights reserved.
|
||||
.\"
|
||||
.\" 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.
|
||||
.\" 3. The name of the author may not be used to endorse or promote products
|
||||
.\" derived from this software without specific prior written permission.
|
||||
.\"
|
||||
.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 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$
|
||||
.\"
|
||||
.\" Many parts of this manual have been snarfed from the pccard.conf (5) man
|
||||
.\" page, copyright by Andrew McRae.
|
||||
.\"
|
||||
.Dd November 19, 1999
|
||||
.Dt USBD.CONF 5
|
||||
.Os FreeBSD
|
||||
.Sh NAME
|
||||
.Nm usbd.conf
|
||||
.Nd
|
||||
.Xr usbd 8
|
||||
configuration file
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Nm
|
||||
file is the configuration file for the
|
||||
.Xr usbd 8
|
||||
daemon. It provides information to allow execution of userland commands
|
||||
on events reported by the
|
||||
.Xr usb 4
|
||||
subsystem in the kernel. Currently the only events are device attach and
|
||||
detach, but could in the future be extended to include power management
|
||||
functions.
|
||||
.Pp
|
||||
The configuration file consists of a sorted list of entries. Each entry
|
||||
describes a set of criteria commands. When an event occurs, the criteria
|
||||
are checked and if met, the commands for that event are executed through
|
||||
a shell. The list is sorted and scanned from top to bottom. The first
|
||||
matching entry is used for an event.
|
||||
.Pp
|
||||
Each entry contains a number of fields. There are 3 types of fields:
|
||||
descriptive fields, selection criteria and commands to execute on
|
||||
events. The field name is case sensitive and should be all lower case.
|
||||
Each field can have one or more arguments.
|
||||
.Pp
|
||||
The following fields are available:
|
||||
.Bl -tag -width devicename\ <Id>
|
||||
.It device Ar string
|
||||
Start a new entry.
|
||||
.Ar string
|
||||
is an arbitrary string used for pretty printing.
|
||||
.It product Ar id
|
||||
Product Id
|
||||
.It vendor Ar id
|
||||
Vendor Id
|
||||
.It release Ar id
|
||||
Release Id, also called revision Id sometimes.
|
||||
.It class Ar id
|
||||
Device Class
|
||||
.It subclass Ar id
|
||||
Device Subclass
|
||||
.It protocol Ar id
|
||||
Device Protocol
|
||||
.It devicenames Ar string
|
||||
Device name, for example umass2, or ums0.
|
||||
.El
|
||||
.Pp
|
||||
String arguments may be quoted. If a string argument contains a space or
|
||||
tab character it needs to be enclosed in single or double quotes. If an
|
||||
argument contains a single or double quote, that quote needs to be
|
||||
enclosed in double or single quotes respectively. See below for
|
||||
examples.
|
||||
.Pp
|
||||
Numeric arguments can either be specified in decimal (42), octal (052) or
|
||||
hexadeximal (0x2a).
|
||||
.Pp
|
||||
The values for the fields
|
||||
.Li product , vendor , release, class , subclass
|
||||
and
|
||||
.Li protocol
|
||||
can be retrieved by killing the
|
||||
.Nm usbd
|
||||
daemon and running it with the
|
||||
.Fl -d
|
||||
and
|
||||
.Fl -v
|
||||
flags.
|
||||
.Pp
|
||||
.Sh EXAMPLES
|
||||
A sample entry to rescan the SCSI bus on connection of a
|
||||
.Tn "Iomega USB Zip Drive" .
|
||||
.Bd -literal
|
||||
device "USB Zip drive"
|
||||
product 0x0001
|
||||
vendor 0x059b
|
||||
release 0x0100
|
||||
attach "/usr/bin/camcontrol rescan bus 0"
|
||||
.Ed
|
||||
.Sh FILES
|
||||
.Bl -tag -width /etc/pccard.conf -compact
|
||||
.It Pa /etc/usbd.conf
|
||||
The
|
||||
.Nm usbd
|
||||
configuration file.
|
||||
.El
|
||||
.Sh SEE ALSO
|
||||
.Xr usb 4 ,
|
||||
.Xr usbd 8 ,
|
||||
.Xr usbdevs 8
|
||||
.Sh AUTHORS
|
||||
The man page for the usbd configuration file was written by
|
||||
.An Nick Hibma Aq n_hibma@freebsd.org .
|
Loading…
x
Reference in New Issue
Block a user