Skip to content

Commit a802fa7

Browse files
authored
Merge pull request #450 from hnez/linux-sysfs
Use sysfs interface on recent Linux kernels
2 parents 7099809 + 24e1540 commit a802fa7

File tree

2 files changed

+145
-26
lines changed

2 files changed

+145
-26
lines changed

README.md

+15
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,21 @@ Linux USB permissions
214214
=====================
215215

216216
On Linux, you should configure `udev` USB permissions (otherwise you will have to run it as root using `sudo uhubctl`).
217+
218+
Starting with Linux Kernel 6.0 there is a standard interface to turn USB hub ports on or off,
219+
and `uhubctl` will try to use it (instead of `libusb`) to set the port status.
220+
This is why there are additional rules for 6.0+ kernels.
221+
There is no harm in having these rules on systems running older kernel versions.
222+
217223
To fix USB permissions, first run `sudo uhubctl` and note all `vid:pid` for hubs you need to control.
218224
Then, add one or more udev rules like below to file `/etc/udev/rules.d/52-usb.rules` (replace 2001 with your vendor id):
219225

220226
SUBSYSTEM=="usb", ATTR{idVendor}=="2001", MODE="0666"
221227

228+
# Linux 6.0 or later (its ok to have this block present in older Linux):
229+
SUBSYSTEM=="usb", DRIVER=="hub", \
230+
RUN="/bin/sh -c \"chmod -f 666 $sys$devpath/*-port*/disable || true\""
231+
222232
Note that for USB3 hubs, some hubs use different vendor ID for USB2 vs USB3 components of the same chip,
223233
and both need permissions to make uhubctl work properly. E.g. for Raspberry Pi 4B, you need to add these 2 lines:
224234

@@ -229,6 +239,11 @@ If you don't like wide open mode `0666`, you can restrict access by group like t
229239

230240
SUBSYSTEM=="usb", ATTR{idVendor}=="2001", MODE="0664", GROUP="dialout"
231241

242+
# Linux 6.0 or later (its ok to have this block present in older Linux):
243+
SUBSYSTEM=="usb", DRIVER=="hub", \
244+
RUN+="/bin/sh -c \"chown -f root:dialout $sys$devpath/*-port*/disable || true\"" \
245+
RUN+="/bin/sh -c \"chmod -f 660 $sys$devpath/*-port*/disable || true\""
246+
232247
and then add permitted users to `dialout` group:
233248

234249
sudo usermod -a -G dialout $USER

uhubctl.c

+130-26
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ int snprintf(char * __restrict __str, size_t __size, const char * __restrict __f
4747
#include <time.h> /* for nanosleep */
4848
#endif
4949

50+
#ifdef __gnu_linux__
51+
#include <fcntl.h> /* for open() / O_WRONLY */
52+
#endif
53+
5054
/* cross-platform sleep function */
5155

5256
void sleep_ms(int milliseconds)
@@ -222,6 +226,9 @@ static int opt_exact = 0; /* exact location match - disable USB3 duality handl
222226
static int opt_reset = 0; /* reset hub after operation(s) */
223227
static int opt_force = 0; /* force operation even on unsupported hubs */
224228
static int opt_nodesc = 0; /* skip querying device description */
229+
#ifdef __gnu_linux__
230+
static int opt_nosysfs = 0; /* don't use the Linux sysfs port disable interface, even if available */
231+
#endif
225232

226233
static const struct option long_options[] = {
227234
{ "location", required_argument, NULL, 'l' },
@@ -236,6 +243,9 @@ static const struct option long_options[] = {
236243
{ "exact", no_argument, NULL, 'e' },
237244
{ "force", no_argument, NULL, 'f' },
238245
{ "nodesc", no_argument, NULL, 'N' },
246+
#ifdef __gnu_linux__
247+
{ "nosysfs", no_argument, NULL, 'S' },
248+
#endif
239249
{ "reset", no_argument, NULL, 'R' },
240250
{ "version", no_argument, NULL, 'v' },
241251
{ "help", no_argument, NULL, 'h' },
@@ -262,6 +272,9 @@ static int print_usage()
262272
"--exact, -e - exact location (no USB3 duality handling).\n"
263273
"--force, -f - force operation even on unsupported hubs.\n"
264274
"--nodesc, -N - do not query device description (helpful for unresponsive devices).\n"
275+
#ifdef __gnu_linux__
276+
"--nosysfs, -S - do not use the Linux sysfs port disable interface.\n"
277+
#endif
265278
"--reset, -R - reset hub after each power-on action, causing all devices to reassociate.\n"
266279
"--wait, -w - wait before repeat power off [%d ms].\n"
267280
"--version, -v - print program version.\n"
@@ -507,6 +520,108 @@ static int get_port_status(struct libusb_device_handle *devh, int port)
507520
}
508521

509522

523+
#ifdef __gnu_linux__
524+
/*
525+
* Try to use the Linux sysfs interface to power a port off/on.
526+
* Returns 0 on success.
527+
*/
528+
529+
static int set_port_status_linux(struct libusb_device_handle *devh, struct hub_info *hub, int port, int on)
530+
{
531+
int configuration = 0;
532+
char disable_path[PATH_MAX];
533+
534+
int rc = libusb_get_configuration(devh, &configuration);
535+
if (rc < 0) {
536+
return rc;
537+
}
538+
539+
// The "disable" sysfs interface is available starting with kernel version 6.0.
540+
// For earlier kernel versions the open() call will fail and we fall
541+
// back to using libusb.
542+
snprintf(disable_path, PATH_MAX,
543+
"/sys/bus/usb/devices/%s:%d.0/%s-port%i/disable",
544+
hub->location, configuration, hub->location, port
545+
);
546+
547+
int disable_fd = open(disable_path, O_WRONLY);
548+
if (disable_fd >= 0) {
549+
rc = write(disable_fd, on ? "0" : "1", 1);
550+
close(disable_fd);
551+
}
552+
553+
if (disable_fd < 0 || rc < 0) {
554+
// ENOENT is the expected error when running on Linux kernel < 6.0 where
555+
// the interface does not exist yet. No need to report anything in this case.
556+
// If the file exists but another error occurs it is most likely a permission
557+
// issue. Print an error message mostly geared towards setting up udev.
558+
if (errno != ENOENT) {
559+
fprintf(stderr,
560+
"Failed to set port status by writing to %s (%s).\n"
561+
"Follow https://git.io/JIB2Z to make sure that udev is set up correctly.\n"
562+
"Falling back to libusb based port control.\n"
563+
"Use -S to skip trying the sysfs interface and printing this message.\n",
564+
disable_path, strerror(errno)
565+
);
566+
}
567+
568+
return -1;
569+
}
570+
571+
return 0;
572+
}
573+
#endif
574+
575+
576+
/*
577+
* Use a control transfer via libusb to turn a port off/on.
578+
* Returns >= 0 on success.
579+
*/
580+
581+
static int set_port_status_libusb(struct libusb_device_handle *devh, int port, int on)
582+
{
583+
int rc = 0;
584+
int request = on ? LIBUSB_REQUEST_SET_FEATURE
585+
: LIBUSB_REQUEST_CLEAR_FEATURE;
586+
int repeat = on ? 1 : opt_repeat;
587+
588+
while (repeat-- > 0) {
589+
rc = libusb_control_transfer(devh,
590+
LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_OTHER,
591+
request, USB_PORT_FEAT_POWER,
592+
port, NULL, 0, USB_CTRL_GET_TIMEOUT
593+
);
594+
if (rc < 0) {
595+
perror("Failed to control port power!\n");
596+
}
597+
if (repeat > 0) {
598+
sleep_ms(opt_wait);
599+
}
600+
}
601+
602+
return rc;
603+
}
604+
605+
606+
/*
607+
* Try different methods to power a port off/on.
608+
* Return >= 0 on success.
609+
*/
610+
611+
static int set_port_status(struct libusb_device_handle *devh, struct hub_info *hub, int port, int on)
612+
{
613+
#ifdef __gnu_linux__
614+
if (!opt_nosysfs) {
615+
if (set_port_status_linux(devh, hub, port, on) == 0) {
616+
return 0;
617+
}
618+
}
619+
#endif
620+
621+
return set_port_status_libusb(devh, port, on);
622+
}
623+
624+
510625
/*
511626
* Get USB device descriptor strings and summary description.
512627
*
@@ -904,7 +1019,7 @@ int main(int argc, char *argv[])
9041019
int option_index = 0;
9051020

9061021
for (;;) {
907-
c = getopt_long(argc, argv, "l:L:n:a:p:d:r:w:s:hvefRN",
1022+
c = getopt_long(argc, argv, "l:L:n:a:p:d:r:w:s:hvefRNS",
9081023
long_options, &option_index);
9091024
if (c == -1)
9101025
break; /* no more options left */
@@ -964,6 +1079,11 @@ int main(int argc, char *argv[])
9641079
case 'N':
9651080
opt_nodesc = 1;
9661081
break;
1082+
#ifdef __gnu_linux__
1083+
case 'S':
1084+
opt_nosysfs = 1;
1085+
break;
1086+
#endif
9671087
case 'e':
9681088
opt_exact = 1;
9691089
break;
@@ -1060,45 +1180,29 @@ int main(int argc, char *argv[])
10601180
if (rc == 0) {
10611181
/* will operate on these ports */
10621182
int ports = ((1 << hubs[i].nports) - 1) & opt_ports;
1063-
int request = (k == 0) ? LIBUSB_REQUEST_CLEAR_FEATURE
1064-
: LIBUSB_REQUEST_SET_FEATURE;
1183+
int should_be_on = k;
1184+
10651185
int port;
10661186
for (port=1; port <= hubs[i].nports; port++) {
10671187
if ((1 << (port-1)) & ports) {
10681188
int port_status = get_port_status(devh, port);
10691189
int power_mask = hubs[i].super_speed ? USB_SS_PORT_STAT_POWER
10701190
: USB_PORT_STAT_POWER;
1071-
int powered_on = port_status & power_mask;
1191+
int is_on = (port_status & power_mask) != 0;
1192+
10721193
if (opt_action == POWER_TOGGLE) {
1073-
request = powered_on ? LIBUSB_REQUEST_CLEAR_FEATURE
1074-
: LIBUSB_REQUEST_SET_FEATURE;
1194+
should_be_on = !is_on;
10751195
}
1076-
if (k == 0 && !powered_on && opt_action != POWER_TOGGLE)
1077-
continue;
1078-
if (k == 1 && powered_on)
1079-
continue;
1080-
int repeat = powered_on ? opt_repeat : 1;
1081-
while (repeat-- > 0) {
1082-
rc = libusb_control_transfer(devh,
1083-
LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_OTHER,
1084-
request, USB_PORT_FEAT_POWER,
1085-
port, NULL, 0, USB_CTRL_GET_TIMEOUT
1086-
);
1087-
if (rc < 0) {
1088-
perror("Failed to control port power!\n");
1089-
}
1090-
if (repeat > 0) {
1091-
sleep_ms(opt_wait);
1092-
}
1196+
1197+
if (is_on != should_be_on) {
1198+
rc = set_port_status(devh, &hubs[i], port, should_be_on);
10931199
}
10941200
}
10951201
}
10961202
/* USB3 hubs need extra delay to actually turn off: */
10971203
if (k==0 && hubs[i].super_speed)
10981204
sleep_ms(150);
1099-
printf("Sent power %s request\n",
1100-
request == LIBUSB_REQUEST_CLEAR_FEATURE ? "off" : "on"
1101-
);
1205+
printf("Sent power %s request\n", should_be_on ? "on" : "off");
11021206
printf("New status for hub %s [%s]\n",
11031207
hubs[i].location, hubs[i].ds.description
11041208
);

0 commit comments

Comments
 (0)