From 9d66811175ea59ebcddab9a80a1274302fedd351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Fri, 2 Sep 2022 11:30:16 +0200 Subject: [PATCH 1/3] Split out port status setting logic into separate function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split out current libusb based power on/off logic into a separate function in anticipation of a other power switching implementations. Signed-off-by: Leonard Göhrs --- uhubctl.c | 75 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/uhubctl.c b/uhubctl.c index 67c7fcb..a74bd0d 100644 --- a/uhubctl.c +++ b/uhubctl.c @@ -507,6 +507,47 @@ static int get_port_status(struct libusb_device_handle *devh, int port) } +/* + * Use a control transfer via libusb to turn a port off/on. + * Returns >= 0 on success. + */ + +static int set_port_status_libusb(struct libusb_device_handle *devh, int port, int on) +{ + int rc = 0; + int request = on ? LIBUSB_REQUEST_SET_FEATURE + : LIBUSB_REQUEST_CLEAR_FEATURE; + int repeat = on ? 1 : opt_repeat; + + while (repeat-- > 0) { + rc = libusb_control_transfer(devh, + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_OTHER, + request, USB_PORT_FEAT_POWER, + port, NULL, 0, USB_CTRL_GET_TIMEOUT + ); + if (rc < 0) { + perror("Failed to control port power!\n"); + } + if (repeat > 0) { + sleep_ms(opt_wait); + } + } + + return rc; +} + + +/* + * Try different methods to power a port off/on. + * Return >= 0 on success. + */ + +static int set_port_status(struct libusb_device_handle *devh, int port, int on) +{ + return set_port_status_libusb(devh, port, on); +} + + /* * Get USB device descriptor strings and summary description. * @@ -1060,45 +1101,29 @@ int main(int argc, char *argv[]) if (rc == 0) { /* will operate on these ports */ int ports = ((1 << hubs[i].nports) - 1) & opt_ports; - int request = (k == 0) ? LIBUSB_REQUEST_CLEAR_FEATURE - : LIBUSB_REQUEST_SET_FEATURE; + int should_be_on = k; + int port; for (port=1; port <= hubs[i].nports; port++) { if ((1 << (port-1)) & ports) { int port_status = get_port_status(devh, port); int power_mask = hubs[i].super_speed ? USB_SS_PORT_STAT_POWER : USB_PORT_STAT_POWER; - int powered_on = port_status & power_mask; + int is_on = (port_status & power_mask) != 0; + if (opt_action == POWER_TOGGLE) { - request = powered_on ? LIBUSB_REQUEST_CLEAR_FEATURE - : LIBUSB_REQUEST_SET_FEATURE; + should_be_on = !is_on; } - if (k == 0 && !powered_on && opt_action != POWER_TOGGLE) - continue; - if (k == 1 && powered_on) - continue; - int repeat = powered_on ? opt_repeat : 1; - while (repeat-- > 0) { - rc = libusb_control_transfer(devh, - LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_OTHER, - request, USB_PORT_FEAT_POWER, - port, NULL, 0, USB_CTRL_GET_TIMEOUT - ); - if (rc < 0) { - perror("Failed to control port power!\n"); - } - if (repeat > 0) { - sleep_ms(opt_wait); - } + + if (is_on != should_be_on) { + rc = set_port_status(devh, port, should_be_on); } } } /* USB3 hubs need extra delay to actually turn off: */ if (k==0 && hubs[i].super_speed) sleep_ms(150); - printf("Sent power %s request\n", - request == LIBUSB_REQUEST_CLEAR_FEATURE ? "off" : "on" - ); + printf("Sent power %s request\n", should_be_on ? "on" : "off"); printf("New status for hub %s [%s]\n", hubs[i].location, hubs[i].ds.description ); From aa7fc0a126b7afb1516d0e7b789917e3ad83b0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Fri, 2 Sep 2022 11:12:09 +0200 Subject: [PATCH 2/3] Add support for Linux sysfs based power switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starting with Linux kernel 6.0[1] there will be a sysfs interface to power USB ports off/on from userspace. Try to use this interface before falling back to the usual libusb based power switching (e.g. when running on a kernel <6.0 or if file permissions do not allow using the sysfs interface). The main benefit of using the sysfs interface is that the kernel does not get confused about the state of a port, so retrying should no longer be required. [1]: https://lore.kernel.org/all/20220607114522.3359148-1-m.grzeschik@pengutronix.de/ Signed-off-by: Leonard Göhrs --- uhubctl.c | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/uhubctl.c b/uhubctl.c index a74bd0d..9c99ebe 100644 --- a/uhubctl.c +++ b/uhubctl.c @@ -47,6 +47,10 @@ int snprintf(char * __restrict __str, size_t __size, const char * __restrict __f #include /* for nanosleep */ #endif +#ifdef __gnu_linux__ +#include /* for open() / O_WRONLY */ +#endif + /* cross-platform sleep function */ void sleep_ms(int milliseconds) @@ -222,6 +226,9 @@ static int opt_exact = 0; /* exact location match - disable USB3 duality handl static int opt_reset = 0; /* reset hub after operation(s) */ static int opt_force = 0; /* force operation even on unsupported hubs */ static int opt_nodesc = 0; /* skip querying device description */ +#ifdef __gnu_linux__ +static int opt_nosysfs = 0; /* don't use the Linux sysfs port disable interface, even if available */ +#endif static const struct option long_options[] = { { "location", required_argument, NULL, 'l' }, @@ -236,6 +243,9 @@ static const struct option long_options[] = { { "exact", no_argument, NULL, 'e' }, { "force", no_argument, NULL, 'f' }, { "nodesc", no_argument, NULL, 'N' }, +#ifdef __gnu_linux__ + { "nosysfs", no_argument, NULL, 'S' }, +#endif { "reset", no_argument, NULL, 'R' }, { "version", no_argument, NULL, 'v' }, { "help", no_argument, NULL, 'h' }, @@ -262,6 +272,9 @@ static int print_usage() "--exact, -e - exact location (no USB3 duality handling).\n" "--force, -f - force operation even on unsupported hubs.\n" "--nodesc, -N - do not query device description (helpful for unresponsive devices).\n" +#ifdef __gnu_linux__ + "--nosysfs, -S - do not use the Linux sysfs port disable interface.\n" +#endif "--reset, -R - reset hub after each power-on action, causing all devices to reassociate.\n" "--wait, -w - wait before repeat power off [%d ms].\n" "--version, -v - print program version.\n" @@ -507,6 +520,59 @@ static int get_port_status(struct libusb_device_handle *devh, int port) } +#ifdef __gnu_linux__ +/* + * Try to use the Linux sysfs interface to power a port off/on. + * Returns 0 on success. + */ + +static int set_port_status_linux(struct libusb_device_handle *devh, struct hub_info *hub, int port, int on) +{ + int configuration = 0; + char disable_path[PATH_MAX]; + + int rc = libusb_get_configuration(devh, &configuration); + if (rc < 0) { + return rc; + } + + // The "disable" sysfs interface is available starting with kernel version 6.0. + // For earlier kernel versions the open() call will fail and we fall + // back to using libusb. + snprintf(disable_path, PATH_MAX, + "/sys/bus/usb/devices/%s:%d.0/%s-port%i/disable", + hub->location, configuration, hub->location, port + ); + + int disable_fd = open(disable_path, O_WRONLY); + if (disable_fd >= 0) { + rc = write(disable_fd, on ? "0" : "1", 1); + close(disable_fd); + } + + if (disable_fd < 0 || rc < 0) { + // ENOENT is the expected error when running on Linux kernel < 6.0 where + // the interface does not exist yet. No need to report anything in this case. + // If the file exists but another error occurs it is most likely a permission + // issue. Print an error message mostly geared towards setting up udev. + if (errno != ENOENT) { + fprintf(stderr, + "Failed to set port status by writing to %s (%s).\n" + "Follow https://git.io/JIB2Z to make sure that udev is set up correctly.\n" + "Falling back to libusb based port control.\n" + "Use -S to skip trying the sysfs interface and printing this message.\n", + disable_path, strerror(errno) + ); + } + + return -1; + } + + return 0; +} +#endif + + /* * Use a control transfer via libusb to turn a port off/on. * Returns >= 0 on success. @@ -542,8 +608,16 @@ static int set_port_status_libusb(struct libusb_device_handle *devh, int port, i * Return >= 0 on success. */ -static int set_port_status(struct libusb_device_handle *devh, int port, int on) +static int set_port_status(struct libusb_device_handle *devh, struct hub_info *hub, int port, int on) { +#ifdef __gnu_linux__ + if (!opt_nosysfs) { + if (set_port_status_linux(devh, hub, port, on) == 0) { + return 0; + } + } +#endif + return set_port_status_libusb(devh, port, on); } @@ -945,7 +1019,7 @@ int main(int argc, char *argv[]) int option_index = 0; for (;;) { - c = getopt_long(argc, argv, "l:L:n:a:p:d:r:w:s:hvefRN", + c = getopt_long(argc, argv, "l:L:n:a:p:d:r:w:s:hvefRNS", long_options, &option_index); if (c == -1) break; /* no more options left */ @@ -1005,6 +1079,11 @@ int main(int argc, char *argv[]) case 'N': opt_nodesc = 1; break; +#ifdef __gnu_linux__ + case 'S': + opt_nosysfs = 1; + break; +#endif case 'e': opt_exact = 1; break; @@ -1116,7 +1195,7 @@ int main(int argc, char *argv[]) } if (is_on != should_be_on) { - rc = set_port_status(devh, port, should_be_on); + rc = set_port_status(devh, &hubs[i], port, should_be_on); } } } From 24e15400065fb245f029eadbc1445975920e2a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonard=20G=C3=B6hrs?= Date: Wed, 21 Sep 2022 15:07:22 +0200 Subject: [PATCH 3/3] Add udev rules for Linux sysfs power switching to README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The udev rules for the sysfs case are a bit more complex than those for the libusb based interface, as udev has built-in support for changing permissions on device files but not for sysfs attributes. Instead we have to use chmod / chown to set permissions and owners. You may notice the " || true" parts in the RUN part of the rules in addition to the -f parameters. These are there to make sure that no error is logged by udev even if the disable attribute does not exist. chmod/chown still return an error exit code even with -f if the requested path does not exist. " || true" makes sure this error is not propagated to udev. Signed-off-by: Leonard Göhrs --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 9471e21..980f885 100644 --- a/README.md +++ b/README.md @@ -212,11 +212,21 @@ Linux USB permissions ===================== On Linux, you should configure `udev` USB permissions (otherwise you will have to run it as root using `sudo uhubctl`). + +Starting with Linux Kernel 6.0 there is a standard interface to turn USB hub ports on or off, +and `uhubctl` will try to use it (instead of `libusb`) to set the port status. +This is why there are additional rules for 6.0+ kernels. +There is no harm in having these rules on systems running older kernel versions. + To fix USB permissions, first run `sudo uhubctl` and note all `vid:pid` for hubs you need to control. Then, add one or more udev rules like below to file `/etc/udev/rules.d/52-usb.rules` (replace 2001 with your vendor id): SUBSYSTEM=="usb", ATTR{idVendor}=="2001", MODE="0666" + # Linux 6.0 or later (its ok to have this block present in older Linux): + SUBSYSTEM=="usb", DRIVER=="hub", \ + RUN="/bin/sh -c \"chmod -f 666 $sys$devpath/*-port*/disable || true\"" + Note that for USB3 hubs, some hubs use different vendor ID for USB2 vs USB3 components of the same chip, and both need permissions to make uhubctl work properly. E.g. for Raspberry Pi 4B, you need to add these 2 lines: @@ -227,6 +237,11 @@ If you don't like wide open mode `0666`, you can restrict access by group like t SUBSYSTEM=="usb", ATTR{idVendor}=="2001", MODE="0664", GROUP="dialout" + # Linux 6.0 or later (its ok to have this block present in older Linux): + SUBSYSTEM=="usb", DRIVER=="hub", \ + RUN+="/bin/sh -c \"chown -f root:dialout $sys$devpath/*-port*/disable || true\"" \ + RUN+="/bin/sh -c \"chmod -f 660 $sys$devpath/*-port*/disable || true\"" + and then add permitted users to `dialout` group: sudo usermod -a -G dialout $USER