Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use sysfs interface on recent Linux kernels #450

Merged
merged 3 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand Down
156 changes: 130 additions & 26 deletions uhubctl.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ int snprintf(char * __restrict __str, size_t __size, const char * __restrict __f
#include <time.h> /* for nanosleep */
#endif

#ifdef __gnu_linux__
#include <fcntl.h> /* for open() / O_WRONLY */
#endif

/* cross-platform sleep function */

void sleep_ms(int milliseconds)
Expand Down Expand Up @@ -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' },
Expand All @@ -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' },
Expand All @@ -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"
Expand Down Expand Up @@ -507,6 +520,108 @@ 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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If user is not using sudo, this path exists but cannot be opened for writing, perhaps it should complain here about permissions?
E.g. we can use access() to check if file exists. If it does not, we fall back to libusb interface silently.
But if disable file exists and we cannot open it for writing, user should be aware that he is doing something wrong.

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.
*/

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, 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);
}


/*
* Get USB device descriptor strings and summary description.
*
Expand Down Expand Up @@ -904,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 */
Expand Down Expand Up @@ -964,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;
Expand Down Expand Up @@ -1060,45 +1180,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, &hubs[i], 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
);
Expand Down