-
Notifications
You must be signed in to change notification settings - Fork 243
Add JSON support for uhubctl - addresses all PR #575 feedback #575
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your submission @benroeder!
Overall great job, but it needs some work to move on with this - please read comments below.
But main points are:
- Consider using encoder-only json C library that is as small as possible, e.g.
mkjson
- it is 10x smaller than cJSON. - Try making emitted json as small as possible: avoid emitting empty or false values. E.g. always emitting
"indicator": false
in list of flags is hardly useful. Try using shorter field names (but still make them understandable) - e.g. consider usingnports
instead ofnum_ports
- this will make json smaller. - Retain backwards compatibility as much as possible - e.g. existing code is using
ppps
for per-port power switching mode supported - consider using it instead ofper-port
. - For json generating functions, avoid copying large blocks from old non-json functions.
- Try to make output json as small as possible and don't emit info that is rarely used - basically anything that is not related to current port power state. If you want it to be still available, consider adding --verbose/-V flag that would make it really verbose.
Makefile
Outdated
$(RM) $(PROGRAM).o $(PROGRAM).dSYM $(PROGRAM) | ||
$(RM) $(OBJECTS) $(PROGRAM).dSYM $(PROGRAM) | ||
|
||
.PHONY: all install clean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing last newline
cJSON.h
Outdated
@@ -0,0 +1,300 @@ | |||
/* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uhubctl.c
Outdated
@@ -18,6 +18,10 @@ | |||
#include <getopt.h> | |||
#include <errno.h> | |||
#include <ctype.h> | |||
#include <stdbool.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need stdbool.h
- commenting this out still works - please remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You still don't need stdbool.h
@@ -180,6 +184,38 @@ struct usb_port_status { | |||
#define HUB_CHAR_TTTT 0x0060 /* TT Think Time mask */ | |||
#define HUB_CHAR_PORTIND 0x0080 /* per-port indicators (LEDs) */ | |||
|
|||
/* | |||
* USB Speed definitions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please link source where you got these speed definitions from? Perhaps USB spec or something?
uhubctl.c
Outdated
int is_mass_storage_device(struct libusb_device *dev) | ||
{ | ||
struct libusb_config_descriptor *config; | ||
int ret = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use rc
instead of ret
to be consistent with other code
uhubctl.c
Outdated
const char* power_switching_mode; | ||
switch (hub->lpsm) { | ||
case HUB_CHAR_INDV_PORT_LPSM: | ||
power_switching_mode = "per-port"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mode is already set to ppps
in get_device_description, make it consistent?
uhubctl.c
Outdated
power_switching_mode = "ganged"; | ||
break; | ||
default: | ||
power_switching_mode = "unknown"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is set to nops
in get_device_description (no power switching).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is still set to nops
when running without -j
. Better to make it consistent.
But is ok to set it to none
in both places.
struct libusb_device_handle* devh = NULL; | ||
int rc = libusb_open(hub->dev, &devh); | ||
if (rc == 0) { | ||
for (int port = 1; port <= hub->nports; port++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code has big copy/paste chunk from get_device_description(). Is there a way to avoid this? Ideally make it into one function?
uhubctl.c
Outdated
int mask; | ||
const char* name; | ||
} flag_defs[] = { | ||
{USB_PORT_STAT_CONNECTION, "connection"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For brevity, I would not emit any flag that has false
value - it would considerably reduce output size
uhubctl.c
Outdated
} | ||
|
||
// Helper function to determine port speed | ||
void get_port_speed(int port_status, char** speed_str, int64_t* speed_bits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
speed_bits
should be speed_bps
(bits per second). That should be in emitted json as well.
Hello ! What are the missing steps to merge ? |
@gillg Looks like the author of the PR never followed up on the comments... Someone would probably need to take over and implement the requested changes. They are quite straightforward, so I might take it up to contribute to this awesome project at least a bit :) |
While brevity is good in general, I wonder if saving 3 bytes per key at the expense of (a little bit of) clarity make sense :) |
@piit79 Did you get anywhere on this? Don't see a |
Based on PR mvp#575 feedback, this commit replaces the large cJSON library with the much smaller mkjson library for JSON output generation. Changes: - Replace cJSON (3,443 lines) with mkjson (357 lines) - ~10x reduction - Optimize JSON output to only emit true boolean flags - Use shorter field names (vid, pid, nports, ppps) - Fix code style issues (remove stdbool.h, change ret to rc) - Fix USB Full Speed detection for USB 2.0 devices - Add USB speed reference citation - Add missing newline at end of Makefile The JSON output format remains compatible while significantly reducing the library size as requested.
- Replace cJSON library (3,443 lines) with lightweight mkjson (357 lines) - Fix mkjson bug: change %Ld to %lld for proper 64-bit integer handling - Add missing JSON fields: device descriptions, USB3 link states, serial numbers - Fix USB3 detection by checking hub->super_speed flag instead of port status bits - Fix buffer allocations with proper size calculations - Handle all alternate settings in interfaces array - Replace unsafe string operations (strcpy/strcat) with snprintf - Change 'speed_bits' field to 'speed_bps' for consistency - Document JSON output option (-j) in README Addresses feedback from PR mvp#575. Tested on macOS and Linux.
I've completely reworked this PR based on your feedback. Major changes: Critical Bug Fix in mkjsonI discovered and fixed a critical bug in mkjson library: The library was using Changes Made
Testing
The code is ready for review. |
I have added cjson to to the repo This is my first pass of adding -j, there is more work to do
Based on PR mvp#575 feedback, this commit replaces the large cJSON library with the much smaller mkjson library for JSON output generation. Changes: - Replace cJSON (3,443 lines) with mkjson (357 lines) - ~10x reduction - Optimize JSON output to only emit true boolean flags - Use shorter field names (vid, pid, nports, ppps) - Fix code style issues (remove stdbool.h, change ret to rc) - Fix USB Full Speed detection for USB 2.0 devices - Add USB speed reference citation - Add missing newline at end of Makefile The JSON output format remains compatible while significantly reducing the library size as requested.
- Fix missing device descriptions by correcting mkjson parameter count - Add USB3 link states (U0, U1, U2, Rx.Detect, etc.) to JSON output - Fix USB Full Speed detection for USB 2.0 devices These changes ensure JSON output includes all information shown in text output.
Serial numbers are now included in the JSON output for devices that have them. This completes the JSON output enhancements to match the text output. Example: "serial": "00015919101424154549"
- Replace cJSON library (3,443 lines) with lightweight mkjson (357 lines) - Fix mkjson bug: change %Ld to %lld for proper 64-bit integer handling - Add missing JSON fields: device descriptions, USB3 link states, serial numbers - Fix USB3 detection by checking hub->super_speed flag instead of port status bits - Fix buffer allocations with proper size calculations - Handle all alternate settings in interfaces array - Replace unsafe string operations (strcpy/strcat) with snprintf - Change 'speed_bits' field to 'speed_bps' for consistency - Document JSON output option (-j) in README Addresses feedback from PR mvp#575. Tested on macOS and Linux.
Add comprehensive examples showing how to use uhubctl's JSON output with jq for various use cases: - Finding devices by vendor, serial, or class - Generating control commands - Monitoring device changes - Creating device maps - Finding empty ports - Exporting to CSV The examples demonstrate the power and flexibility of the JSON output format for automation and scripting.
I note that mkjson is unmaintained (repo is archived, no commits to the two forks), so it might not be a good choice for this. cJSON is at least maintained and available in Linux distros like Debian. If you want to keep mkjson, please at least make a fork of the main repo that has the fixes you added, so the project can be maintained and depended on by uhubctl. |
- Original mkjson repository was archived on Apr 19, 2025 - Created fork at https://github.com/benroeder/mkjson - Fixed critical bug: %Ld -> %lld format specifier for 64-bit integers - Converted to git submodule for easier maintenance - Updated build process and documentation The bug fix prevents truncation of large 64-bit values such as USB 3.0 speeds (5 Gbps = 5,000,000,000 bps).
I've updated this PR based on your feedback:
The implementation is now complete and ready for review. All JSON output is properly escaped and secure. |
As they say no good deed goes un punished :-) |
@bradeyh I sadly never found the time... Great work, @benroeder, thank you! 👌 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for second round of this @benroeder!
I still have few high level issues with your new approach.
- Originally, I was under impression that
-j
would support all actions and emit all messages it emits today in json format, including power transitions. However, as far as I can tell, with your approach,-j
is not compatible with any power actions. As you wrote this, it only supports showing status, making uhubctl very convenient exploration tool for USB devices and hubs, and print everything that one may need to know about USB in JSON format.
This may not be bad idea in its own right, but it also means uhubctl integrations (e.g.pyhubctl
python integration) cannot really use-j
for easier parsing - they still need to parse existing non-json interface to actually provide full power switching support. - Great job on importing mkjson and interfacing with it. However, I don't support idea of importing it as submodule. I want to make sure that all downstream uhubctl users (which is most Linux distros, homebrew, etc) can easily update to new uhubctl version without much trouble. Importing submodule is not exactly straightforward for them, and I'd rather avoid this issue completely by copying mkson.[c,h] verbatim in uhubctl repo. It can still receive updates from your forked mkjson repo, but manually. Note that I don't really care if mkjson original repo is no longer supported by original maintainer, as long as we carry it over and support it ourselves for our quite limited purpose.
- (Minor) I am not sure if it is good idea to have separate README and json_usage_examples. I am leaning towards adding json section in main README (but make it shorter than yours).
- I do like mkjson a lot more that cjson (mostly due to small size and ability to carry it over as a whole), but I have to admit that it is very painful to prepare list of arguments for
mkjson()
varags calls, especially when it comes to counting parameters. I would like to add different non-varargs function of similar nature to mkjson which would use null terminated array instead to eliminate a need for counting in first place - I will play with this myself. - (Minor) Another issue with
mkjson()
is json output is not pretty - everything is packed in one line. It is not an issue for API usage, but if we are to ever support power transitions with json output it will be quite annoying because it is not really human readable. I wonder if we can patch mkjson to support pretty printing.
.gitmodules
Outdated
[submodule "mkjson"] | ||
path = mkjson | ||
url = https://github.com/benroeder/mkjson.git |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am against using submodule for this. I would rather copy 2 tiny .c and .h files into uhubctl repo. Its ok to keep updating it manually if mkjson changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mkjson has been archived, which why I forked and fixed it
@@ -14,7 +14,7 @@ PKG_CONFIG ?= pkg-config | |||
|
|||
CC ?= gcc | |||
CFLAGS ?= -g -O0 | |||
CFLAGS += -Wall -Wextra -Wno-zero-length-array -std=c99 -pedantic | |||
CFLAGS += -Wall -Wextra -Wno-zero-length-array -std=c99 -pedantic -I. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would not be needed if we keep mkjson local copy
README.md
Outdated
git clone https://github.com/mvp/uhubctl | ||
cd uhubctl | ||
git submodule init | ||
git submodule update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets not use submodule. But if we did, perhaps cleaner approach is
git clone --recurse-submodules https://github.com/mvp/uhubctl
uhubctl.c
Outdated
@@ -18,6 +18,10 @@ | |||
#include <getopt.h> | |||
#include <errno.h> | |||
#include <ctype.h> | |||
#include <stdbool.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You still don't need stdbool.h
@@ -273,6 +312,8 @@ static const struct option long_options[] = { | |||
{ "reset", no_argument, NULL, 'R' }, | |||
{ "version", no_argument, NULL, 'v' }, | |||
{ "help", no_argument, NULL, 'h' }, | |||
{ "json", no_argument, NULL, 'j' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
help should be the last option
uhubctl.c
Outdated
// If no action is specified or JSON output is requested, just print status | ||
if (opt_action == POWER_KEEP || opt_json) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please enforce check that -j
is only compatible with action POWER_KEEP
before doing anything - we should print error message if say -j -a 0
is requested.
Then this if can be left as is
continue; | ||
if (k == 1 && opt_action == POWER_KEEP) | ||
continue; | ||
/* if toggle requested, do it only once when `k == 0` */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why removing comment about toggle?
if (k==0 && hubs[i].super_speed) | ||
sleep_ms(150); | ||
|
||
if (!opt_json) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you enforce -j incompatibility check with -a, you would not need these ifs
uhubctl.c
Outdated
char* create_status_flags_json(int port_status, int is_usb3_hub) | ||
{ | ||
// Use the provided hub USB version information | ||
int is_usb3 = is_usb3_hub; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why create another variable which is simply a copy of the same thing?
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
many superficial empty lines
FTR, distros strongly prefer to not have embedded code copies, for example the Debian advice. The same can apply to git submodules but to a lesser extent, since the GitHub tarballs don't have the code copy, but then distros still have to patch the build system to work with the separately packaged version. |
Or best of both worlds, subrepo ? https://github.com/ingydotnet/git-subrepo No submodule, no git reference, the code is locally copied, but subrepo adds a manifest to make easy a rebase or other things. In the end, it's just a local commit of all the files from the target version but you can track from where it's coming. |
This PR addresses all feedback from mvp on PR mvp#575 and implements comprehensive JSON support for uhubctl. Major changes: - Replace cJSON with mkjson (10x smaller library, ~400 lines vs ~3,400) - Add JSON support for all power actions (on/off/toggle/cycle/flash) - Implement real-time JSON event streaming for power operations - Add pretty-printing with 2-space indentation (always enabled) - Create mkjson_array() wrapper to eliminate manual parameter counting - Add comprehensive status decoding with human-readable descriptions Technical improvements: - Remove git submodule dependency, embed mkjson directly - Rename is_usb3_hub → super_speed for clarity - Rename speed_bits → speed_bps for consistency - Add memory management documentation for JSON functions - Convert all // comments to /* */ C-style - Remove trailing whitespace and fix code formatting - Move help option to end of command line parser - Only output true boolean flags to minimize JSON size - Remove verbose fields (nconfigs, interfaces) from default output JSON output features: - Hub information with VID/PID, USB version, port count - Port status with raw hex, decoded state, and bit breakdown - Connected device details (vendor, product, serial, class) - Real-time power action events with success/failure status - Human-readable flag descriptions - Consistent field naming and structure Testing: - Valgrind clean (no memory leaks) - Works with complex USB hub topologies - All existing command-line functionality preserved - JSON is opt-in via -j flag Documentation: - Added JSON section to README.md with examples - Included jq usage examples for common tasks - Memory management documented in function headers
This function was created but never used since we removed the interfaces field from JSON output per mvp's feedback. Removing it cleans up the code and reduces binary size.
This addresses mvp's feedback about 'big copy/paste chunk from get_device_description()'. The solution: 1. Extended descriptor_strings struct to include additional fields needed for JSON output (vid, pid, device_class, etc.) 2. Updated get_device_description() to populate these fields 3. Removed duplicate libusb_get_device_descriptor() call and parsing logic from create_port_status_json() 4. Added forward declarations for helper functions This eliminates ~30 lines of duplicate code while making the JSON output more efficient by reusing already-parsed device info.
This eliminates duplicate device enumeration loops that were repeated throughout the codebase. The new find_device_on_hub_port() helper function consolidates the logic for finding devices connected to specific hub ports. Changes: - Added find_device_on_hub_port() helper function - Replaced 3 duplicate enumeration loops with helper calls - Reduced code duplication by ~40 lines - Improved maintainability and consistency Testing: - Builds cleanly on both macOS and Linux - JSON and text output work correctly - Valgrind shows no memory leaks
This eliminates duplicate hex formatting code scattered throughout the codebase. The new format_hex_id() helper function ensures consistent formatting of VID/PID values. Changes: - Added format_hex_id() helper function - Replaced 5 duplicate snprintf calls with helper - Consistent "0x%04x" formatting throughout codebase - Improved maintainability and consistency Testing: - Builds cleanly on both platforms - JSON output format unchanged - All hex values formatted correctly
This eliminates duplicate USB version formatting code that was repeated multiple times throughout the codebase. The new format_usb_version() helper ensures consistent formatting. Changes: - Added format_usb_version() helper function - Replaced 3 duplicate snprintf calls with helper - Consistent "x.xx" version formatting throughout - Improved maintainability and consistency Testing: - Builds cleanly on both platforms - USB version formatting works correctly - JSON output shows proper version strings
From a distro point of view, git-subrepo sounds the ultimately same, copying code instead of it remaining an external dependency, which is packaged separately in distros. Also, mkjson uses static linking, which is almost as bad from a distro point of view, slightly better though, since distros don't have to patch all reverse dependencies, but just rebuild them, that process takes manual distro work though, while shared libraries just need one distro update, and users to restart processes or reboot. The mkjson security bug @benroeder found and fixed illustrates this problem, if things that use it had copies of it, some of them probably would not find out about the security problem, and some of them probably would not fix the issue in their copy, and users could be affected by the issue. With a shared library, distros patch that once, and every mkjson user is automatically fixed soon enough. Also, @benroeder you might want to consider getting a CVE for the mkjson security issue (IIRC GitHub makes that easy). All that said, no distros have mkjson packaged, and there are no copies of it in other source code in Debian, and it is mostly just one small function, so maybe it is fine. OTOH there has already been one security issue. Also, there is a naming conflict with a mkjson command-line tool written in Haskell, might be worth renaming the mkjson C lib to libmkjson-c or similar instead. |
Summary
This PR addresses all feedback from mvp on PR #575 and implements comprehensive JSON support for uhubctl.
Major Changes
Library Replacement
JSON Output Features
Technical Improvements
JSON Output Structure
Status Queries
Power Action Events
Outputs real-time events:
Usage Examples
Testing
-j
flagImplementation Notes
This implementation provides a complete JSON interface for uhubctl while maintaining full backwards compatibility and addressing all feedback from the initial review.