├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── benches ├── get.rs └── profile.rs ├── doc ├── _cyme ├── _cyme.ps1 ├── cli-tree.png ├── cyme.1 ├── cyme.bash ├── cyme.fish └── cyme_example_config.json ├── examples ├── extra_data.rs ├── filter_devices.rs ├── print_devices.rs └── walk_sp_data.rs ├── scripts └── release_version.sh ├── src ├── colour.rs ├── config.rs ├── display.rs ├── error.rs ├── icon.rs ├── lib.rs ├── lsusb.rs ├── lsusb │ ├── audio_dumps.rs │ ├── bos_dumps.rs │ ├── names.rs │ └── video_dumps.rs ├── main.rs ├── profiler.rs ├── profiler │ ├── libusb.rs │ ├── macos.rs │ ├── nusb.rs │ ├── types.rs │ └── watch.rs ├── types.rs ├── udev.rs ├── udev_ffi.rs ├── usb.rs ├── usb │ ├── descriptors.rs │ ├── descriptors │ │ ├── audio.rs │ │ ├── bos.rs │ │ ├── cdc.rs │ │ └── video.rs │ └── path.rs └── watch.rs └── tests ├── common └── mod.rs ├── data ├── config_missing_args.json ├── config_no_theme.json ├── cyme_libusb_linux.json ├── cyme_libusb_linux_tree.json ├── cyme_libusb_macos_tree.json ├── cyme_libusb_merge_macos_tree.json ├── cyme_sp_macos_tree.json ├── cyme_sp_tree_json_dump.json ├── lsusb_list.txt ├── lsusb_tree.txt ├── lsusb_tree_verbose.txt ├── lsusb_verbose.txt └── system_profiler_dump.json ├── integration_test.rs └── integration_test_lsusb_display.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: tuna-f1sh 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: tunaf1sh 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | release: 7 | types: 8 | - created 9 | workflow_dispatch: 10 | 11 | env: 12 | CARGO_CMD: cargo 13 | RUSTFLAGS: "-Dwarnings" 14 | 15 | name: Test, build and package 16 | jobs: 17 | crate_metadata: 18 | name: Extract crate metadata 19 | runs-on: ubuntu-latest 20 | if: github.event_name == 'release' 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Extract crate information 24 | id: crate_metadata 25 | run: | 26 | cargo metadata --no-deps --format-version 1 | jq -r '"name=" + .packages[0].version' | tee -a $GITHUB_OUTPUT 27 | cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT 28 | cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT 29 | cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT 30 | cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT 31 | outputs: 32 | name: ${{ steps.crate_metadata.outputs.name }} 33 | version: ${{ steps.crate_metadata.outputs.version }} 34 | maintainer: ${{ steps.crate_metadata.outputs.maintainer }} 35 | homepage: ${{ steps.crate_metadata.outputs.homepage }} 36 | msrv: ${{ steps.crate_metadata.outputs.msrv }} 37 | 38 | format: 39 | name: Ensure 'cargo fmt' has been run 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: dtolnay/rust-toolchain@stable 43 | with: 44 | components: rustfmt 45 | - uses: actions/checkout@v4 46 | - run: cargo fmt -- --check 47 | 48 | # Clippy pre-check would be nice but OS dependant features and libusb deps for all-features requires in matrix 49 | 50 | build: 51 | name: ${{ matrix.job.os }}-${{ matrix.job.target }} 52 | runs-on: ${{ matrix.job.os }} 53 | needs: [format] 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | job: 58 | # default features for all targets 59 | - { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, use-cross: false, feature-flags: "" } 60 | - { os: windows-latest, target: x86_64-pc-windows-gnu, use-cross: false, feature-flags: "" } 61 | - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, use-cross: false, feature-flags: "" } 62 | - { os: macos-latest, target: universal-apple-darwin, use-cross: false, feature-flags: "" } 63 | outputs: 64 | # could use these for release job? 65 | # pkg-linux-aarch64: ${{ steps.package.outputs.pkg-aarch64-unknown-linux-gnu }} 66 | # pkg-linux-x86_64: ${{ steps.package.outputs.pkg-x86_64-unknown-linux-gnu }} 67 | # pkg-windows-x86_64: ${{ steps.package.outputs.pkg-x86_64-pc-windows-gnu }} 68 | # pkg-macos-x86_64: ${{ steps.package.outputs.pkg-universal-apple-darwin }} 69 | homebrew-pkg-name: ${{ steps.package.outputs.MACOS_PKG_NAME }} 70 | steps: 71 | - uses: actions/checkout@v4 72 | - uses: actions/cache@v4 73 | with: 74 | path: | 75 | ~/.cargo/registry/index/ 76 | ~/.cargo/registry/cache/ 77 | ~/.cargo/git/db/ 78 | ~/.cargo/bin/cargo-deb 79 | target/ 80 | key: ${{ matrix.job.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 81 | - uses: dtolnay/rust-toolchain@stable 82 | 83 | # Could remove this step if not clippy/testing --all-features as only required for non-native profiler 84 | - name: Install prerequisites 85 | shell: bash 86 | run: | 87 | case ${{ matrix.job.target }} in 88 | *-linux-*) 89 | sudo apt-get -y update; 90 | sudo apt-get -y install libudev-dev libusb-1.0-0-dev; 91 | # install cargo-deb if not cached 92 | if ! command -v cargo-deb &> /dev/null; then 93 | cargo install cargo-deb 94 | fi 95 | ;; 96 | *) 97 | ;; 98 | esac 99 | 100 | - name: Rustup add target 101 | if: matrix.job.use-cross == false 102 | shell: bash 103 | run: | 104 | case ${{ matrix.job.target }} in 105 | universal-apple-*) 106 | rustup target add x86_64-apple-darwin 107 | rustup target add aarch64-apple-darwin 108 | ;; 109 | *) 110 | rustup target add ${{ matrix.job.target }} 111 | ;; 112 | esac 113 | 114 | - name: Install cross 115 | shell: bash 116 | if: matrix.job.use-cross == true 117 | run: | 118 | echo "CARGO_CMD=cross" >> "$GITHUB_ENV" 119 | cargo install cross 120 | 121 | - name: Clippy check no warnings 122 | id: clippy 123 | shell: bash 124 | # cross targets should be covered 125 | if: matrix.job.use-cross == false 126 | run: cargo clippy --all-targets --all-features 127 | 128 | - name: Test 129 | id: test 130 | shell: bash 131 | # cross is buggy with QEMU and slow 132 | if: matrix.job.use-cross == false 133 | run: make test 134 | env: 135 | CARGO_FLAGS: ${{ matrix.job.feature-flags }} 136 | TARGET: ${{ matrix.job.target }} 137 | 138 | - name: Generated files up to date 139 | id: generate 140 | shell: bash 141 | if: matrix.job.use-cross == false 142 | run: | 143 | make generated 144 | git diff --exit-code 145 | env: 146 | CARGO_FLAGS: ${{ matrix.job.feature-flags }} 147 | TARGET: ${{ matrix.job.target }} 148 | 149 | - name: Build release 150 | id: build 151 | shell: bash 152 | run: echo "bin-${TARGET}=$(make release | tail -n1)" >> "$GITHUB_OUTPUT" 153 | env: 154 | CARGO_FLAGS: ${{ matrix.job.feature-flags }} 155 | TARGET: ${{ matrix.job.target }} 156 | 157 | - name: Create tarball 158 | id: package 159 | shell: bash 160 | run: | 161 | PKG_PATH="$(make package | tail -n1)" 162 | echo "pkg-${TARGET}=${PKG_PATH}" >> "$GITHUB_OUTPUT" 163 | echo "PKG_NAME=$(basename ${PKG_PATH})" >> "$GITHUB_OUTPUT" 164 | echo "PKG_PATH=${PKG_PATH}" >> "$GITHUB_OUTPUT" 165 | if [[ "${TARGET}" == *"apple"* ]]; then 166 | echo "MACOS_PKG_NAME=$(basename ${PKG_PATH})" >> "$GITHUB_OUTPUT" 167 | fi 168 | env: 169 | CARGO_FLAGS: ${{ matrix.job.feature-flags }} 170 | TARGET: ${{ matrix.job.target }} 171 | 172 | - name: Create Debian package 173 | id: debian-package 174 | shell: bash 175 | if: contains(matrix.job.target, 'linux') 176 | run: | 177 | DPKG_PATH="$(make dpkg | tail -n1)" 178 | # replace _ with - 179 | DPKG_NAME="$(sed 's/_/-/g' <<< $(basename ${DPKG_PATH}))" 180 | mv "${DPKG_PATH}" "$(dirname ${DPKG_PATH})/${DPKG_NAME}" 181 | DPKG_PATH="$(dirname ${DPKG_PATH})/${DPKG_NAME}" 182 | echo "dpkg-${TARGET}=${DPKG_PATH}" >> "$GITHUB_OUTPUT" 183 | echo "DPKG_NAME=$(basename ${DPKG_PATH})" >> "$GITHUB_OUTPUT" 184 | echo "DPKG_PATH=${DPKG_PATH}" >> "$GITHUB_OUTPUT" 185 | env: 186 | CARGO_FLAGS: ${{ matrix.job.feature-flags }} 187 | TARGET: ${{ matrix.job.target }} 188 | 189 | - name: Upload package artifact 190 | uses: actions/upload-artifact@master 191 | with: 192 | name: ${{ steps.package.outputs.PKG_NAME }} 193 | path: ${{ steps.package.outputs.PKG_PATH }} 194 | 195 | - name: Upload dpkg artifact 196 | uses: actions/upload-artifact@master 197 | if: steps.debian-package.outputs.DPKG_NAME 198 | with: 199 | name: ${{ steps.debian-package.outputs.DPKG_NAME }} 200 | path: ${{ steps.debian-package.outputs.DPKG_PATH }} 201 | 202 | release: 203 | name: Release 204 | runs-on: ubuntu-latest 205 | needs: [build, crate_metadata] 206 | if: github.event_name == 'release' 207 | steps: 208 | - name: Download build artifacts 209 | uses: actions/download-artifact@v4 210 | with: 211 | path: artifacts 212 | 213 | - name: Upload release artifacts 214 | uses: softprops/action-gh-release@v2 215 | with: 216 | files: | 217 | artifacts/**/*.tar.gz 218 | artifacts/**/*.zip 219 | artifacts/**/*.deb 220 | token: ${{ secrets.GITHUB_TOKEN }} 221 | 222 | - name: Bump Homebrew formula 223 | uses: mislav/bump-homebrew-formula-action@v3 224 | with: 225 | formula-name: cyme 226 | formula-path: Formula/cyme.rb 227 | homebrew-tap: tuna-f1sh/homebrew-taps 228 | download-url: https://github.com/tuna-f1sh/cyme/releases/download/v${{ needs.crate_metadata.outputs.version }}/${{ needs.build.outputs.homebrew-pkg-name }} 229 | commit-message: | 230 | {{formulaName}} {{version}} 231 | 232 | Created by https://github.com/mislav/bump-homebrew-formula-action 233 | env: 234 | COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} 235 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/data/config_save.json 2 | log 3 | /target 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.2.2] - 2025-05-20 4 | 5 | ### Fixed 6 | 7 | - watch: bus assignment based on Windows bus_id ([#68](https://github.com/tuna-f1sh/cyme/pull/68)). 8 | 9 | ## [2.2.1] - 2025-05-06 10 | 11 | ### Added 12 | 13 | - Support for `--json` with `watch` sub-command ([#66](https://github.com/tuna-f1sh/cyme/pull/66)). 14 | 15 | ## [2.2.0] - 2025-04-21 16 | 17 | `cyme watch` subcommand to watch for USB device hotplug events and _live_ edit display settings. A simple TUI proof of concept that grew beyond just showing hotplug events into something quite handy for exploring enumerated devices. It can also edit display settings and save them to the cyme config file. 18 | 19 | It's a nice way to customise display blocks: press 'b' to use the editor and 'Ctrl-s' to save. Use '?' for other keybindings. Navigation is mostly Vim-like. Try connecting and disconnecting a device while running `cyme watch` to see the hotplug events. 20 | 21 | The interface is simplistic at the moment but could be re-skinned with something like Ratatui in the future. 22 | 23 | Here's a quick demo: https://youtu.be/ohRBrVBRolA?si=OY8zEtqF-8x_Lp7u 24 | 25 | ### Added 26 | 27 | - `cyme watch` subcommand to watch for USB device hotplug events and 'live' edit display settings ([#58](https://github.com/tuna-f1sh/cyme/pull/58)). 28 | - no_color option to config. Clearer parity/merge with CLI args. 29 | - device event and DeviceBlock::LastEvent, DeviceBlock::EventIcon. Default is profiled time (P: %y-%m-%d %H:%M:%S) but used by `cyme watch` to show connect/disconnect events. 30 | - benches for profiling. 31 | - RUST_LOG can be module level eg: `RUST_LOG=nusb=info,cyme=debug`. 32 | 33 | ### Changed 34 | 35 | - build: Makefile targets used in CI ([#64](https://github.com/tuna-f1sh/cyme/pull/64)). 36 | - custom PortPath type used for get_ methods improves look-up 70-100%. Makes profiler faster as it uses these methods to build the device tree. ([801aa](https://github.com/tuna-f1sh/cyme/commit/801aa3fba28aae7be988d747b1a42bedbc06e496)). 37 | - simple_logger now logs to stderr so can be redirected without effecting display output: `cyme watch 2> log`. 38 | - path args String to PathBuf. 39 | 40 | ## [2.1.3] - 2024-04-03 41 | 42 | ### Fixed 43 | 44 | - lsusb-verbose: hub dump not reading full descriptor for bcd >= 0x0300 so missing hub descriptor ([#63](https://github.com/tuna-f1sh/cyme/pull/63)). 45 | - lsusb-verbose: verbose white space and some strings. 46 | 47 | ### Changed 48 | 49 | - build: hide `--gen` behind `cli_generate` feature ([#61](https://github.com/tuna-f1sh/cyme/pull/61)). 50 | - lsusb: brought upto date with v018 releae and some pre-v019 features ([#62](https://github.com/tuna-f1sh/cyme/pull/62)). 51 | 52 | ### Added 53 | 54 | - display: negotiated-speed block to show the actual operating speed of the connected device. 55 | 56 | ## [2.1.2] - 2024-02-21 57 | 58 | Mostly housekeeping and minor fixes. Did a dependency audit and updated some crates. Working towards a hotplug 'watch' subcommand. 59 | 60 | ### Fixed 61 | 62 | - control read endpoint stall will be re-attempted after clearing halt ([#54](https://github.com/tuna-f1sh/cyme/pull/54)). 63 | - udev-hwdb: native supports hwdb lookup again ([#59](https://github.com/tuna-f1sh/cyme/pull/59)). 64 | - lsusb: fallback to desccriptor strings in verbose dump for idProduct and idVendor ([#55](https://github.com/tuna-f1sh/cyme/issues/55)). 65 | - Bus::is_empty was inverse but display::prepare_devices filter accounted by also inverting. No real bug but fixed for clarity. 66 | 67 | ### Changed 68 | 69 | - macOS: claim interface when reading Debug Descriptors. 70 | - nusb: use cached device descriptor rather than reading manually with control message ([nusb #102](https://github.com/kevinmehall/nusb/pull/102)). 71 | - log now outputs to stderr so can be redirected. 72 | - lazy_static dropped for LazyLock. 73 | - rand replaced with fast_rand. 74 | 75 | ### Added 76 | 77 | - Example usage in README. 78 | 79 | ## [2.1.1] - 2024-12-01 80 | 81 | Minor updates to match `lsusb` updates. Fixing bugs playing with USB gadgets! 82 | 83 | ### Fixed 84 | 85 | - Linux root_hubs now read_link pci driver like lsusb for driver field. 86 | - lsusb verbose would print all audio BmControl2 bits and show ILLEGAL VALUE for 0 bits. 87 | 88 | ### Changed 89 | 90 | - lsusb tree number padding is now 3 digits for bus and device numbers to match lsusb. 91 | 92 | ## [2.1.0] - 2024-10-30 93 | 94 | ### Fixed 95 | 96 | - Linux root\_hub missing from it's own devices; lsusb mode listing with libusb feature. 97 | - nusb feature not profiling root\_hub devices and so not gathering BOS, hub descriptors and status etc. 98 | - Attempt to claim HID interfaces on Linux to avoid dmesg warning. Note that if a kernel module is loaded for a device, it may still be claimed by that module and so not available to cyme. cyme could detach the kernel module but this is not done for usability reasons. The behaviour is the same as lsusb. ([#52](https://github.com/tuna-f1sh/cyme/pull/52)). 99 | 100 | ### Changed 101 | 102 | - Logging should be more useful in debug mode. 103 | 104 | ## [2.0.0] - 2024-10-18 105 | 106 | Big release after almost two years since the first commit: `cyme` is now native Rust\* by default! Thanks to support from [nusb](https://github.com/kevinmehall/nusb), the system profiler is much improved for all platforms. 107 | 108 | See the updated README for target configuration changes. 109 | 110 | \*Native crates. The OS interfaces behind the scenes (currently sysfs, IOKit and WinUSB) are in their respective code but this opens the door for Rust OSes, which the previous 'libusb' profiler could not facilitate. 111 | 112 | ### Added 113 | 114 | - Bus information is now profiled on non-Linux platforms using 'nusb' - much nicer output for macOS and Windows. 115 | - pci.ids vendor and device information for buses where IDs are available. 116 | 117 | ### Changed 118 | 119 | - `cyme` default target now uses native Rust profiling thanks to [nusb](https://github.com/kevinmehall/nusb) ([#26](https://github.com/tuna-f1sh/cyme/pull/26)). 120 | - Default Driver and Interface display blocks now include driver and sysfs on Linux but not on other platforms ([#41](https://github.com/tuna-f1sh/cyme/issues/41)). 121 | - macOS `system_profiler` is not used by default with 'nusb' since IOKit is used directly. It can be forced with `--system_profiler`. The macOS mod is now only compiled for macOS targets. 122 | - 'sysfs' read/readlink is now attempted first for Linux driver information then udev (if feature enabled) ([#45](https://github.com/tuna-f1sh/cyme/pull/45)). 123 | 124 | ## [1.8.5] - 2024-10-11 125 | 126 | ### Added 127 | 128 | - risv64 support ([#37](https://github.com/tuna-f1sh/cyme/pull/37)). 129 | 130 | ### Fixed 131 | 132 | - MixerUnit1 number of channels index incorrect causing OoB panic ([#38](https://github.com/tuna-f1sh/cyme/issues/38)). 133 | 134 | ## [1.8.4] - 2024-09-27 135 | 136 | ### Changed 137 | 138 | - Default sort by bus number and device address within buses for all display modes (matching lsusb) ([#33](https://github.com/tuna-f1sh/cyme/issues/33)). 139 | - Default Rust udev feature no longer supports hwdb lookup as it's broken - usb-ids is used. Use `--no-default-features -F=udevlib -F=udev_hwdb` if really wishing to use local 'hwdb.bin'. ([#35](https://github.com/tuna-f1sh/cyme/issues/35)). 140 | 141 | ## [1.8.3] - 2024-09-20 142 | 143 | ### Fixes 144 | 145 | - Fix panic when using auto-width and utf-8 characters landing on non-char boundary ([#30](https://github.com/tuna-f1sh/cyme/issues/32)). 146 | - Corrected some typos ([#28](https://github.com/tuna-f1sh/cyme/pull/28)). 147 | - Fix lintian errors with cargo-deb package ([#29](https://github.com/tuna-f1sh/cyme/pull/31)). 148 | 149 | ## [1.8.2] - 2024-08-20 150 | 151 | ### Changed 152 | 153 | - Standard cyme list now excludes root_hubs (`--tree` shows them as buses as before). `--lsusb` list mode will still show them. Use `--list-root-hubs` (or in config) to include them in the cyme list on Linux as before. 154 | 155 | ### Fixes 156 | 157 | - Fix length and offset calculation in lsusb::dump_hub that would print some incorrect data. 158 | - Minor formatting fixes for `lsusb --verbose` mode; indent in dump_interface, min 1 space between fields, wTotalLength as hex. 159 | 160 | ## [1.8.1] - 2024-07-16 161 | 162 | ### Fixes 163 | 164 | - Fix panic due to potential subtraction overflow in `lsusb --verbose` mode ([#24](https://github.com/tuna-f1sh/cyme/issues/25)). 165 | 166 | ## [1.8.0] - 2024-07-15 167 | 168 | `cyme` should now match `lsusb --verbose` mode with full device descriptor dumps, including using USB control messages to get BOS, Hub device status, HID reports and more. It's been a lot of grunt work and lines of code (not very creative lines!) creating all the types but it should be useful as a USB profiling crate moving forwards and I think more robust than `lsusb` in some cases. There may still be some formatting differences but the data _should_ be the same. `cyme` without `--lsusb --verbose` display isn't changed for the most part, since the dumping is extremely device specific and verbose. I may add device status as a display block in future. 169 | 170 | ### Added 171 | 172 | - Full dumps of device descriptors for matching `--lsusb --verbose` [#23](https://github.com/tuna-f1sh/cyme/pull/23) ([#15](https://github.com/tuna-f1sh/cyme/issues/15)) 173 | - Device name pattern matching for icon with `Icon::name(String)` ([#22](https://github.com/tuna-f1sh/cyme/pull/22)) 174 | 175 | ### Changed 176 | 177 | - `cyme` is now in [Homebrew core](https://formulae.brew.sh/formula/cyme). One can `brew uninstall cyme`, `brew untap tuna-f1sh/taps`, then install with `brew install cyme` ([#21](https://github.com/tuna-f1sh/cyme/pull/21)). 178 | - Update `--lsusb` mode to match updated lsusb behaviour if driver/names missing (print '[none]'/'[unknown]'). 179 | 180 | ## [1.7.0] - 2024-06-25 181 | 182 | ### Changed 183 | 184 | - Replace [udev-rs](https://github.com/Smithay/udev-rs) and indirectly libudev-sys with Rust native [udev](https://github.com/cr8t/udev); libudev dependency (and system requirement) is now optional but can be used with `--no-default-features -F=udevlib`. ([#19](https://github.com/tuna-f1sh/cyme/pull/19)) 185 | 186 | ### Fixes 187 | 188 | - Replace more font-awesome icons in default look-up that have been deprecated ([#20](https://github.com/tuna-f1sh/cyme/issues/20)) 189 | 190 | ## [1.6.1] - 2024-06-13 191 | 192 | ### Fixes 193 | 194 | - Replace font-awesome icons in default look-up that have been deprecated. 195 | 196 | ## [1.6.0] - 2023-11-23 197 | 198 | _A release of patches, PRs and merges :), thanks to support_ 199 | 200 | ### Added 201 | 202 | - Support udev/sysfs iString lookup ([#14](https://github.com/tuna-f1sh/cyme/pull/14)) (@haata). 203 | - Add fully defined USB Device based on class code triplet. 204 | - Support bLength, wTotalLength and bDescriptorType fields in `lsusb --verbose` with ([rusb/#185](https://github.com/a1ien/rusb/pull/185)). This completes the `lsusb --verbose` support apart from extra descriptors. 205 | - Add `lsusb::names` mod that ports 'usbutils/names.c' to match the behaviour using `lsusb --verbose`. This means class, sub-class and protocol udev-hwdb names are included now in `lsusb --verbose` ([b99e87](https://github.com/tuna-f1sh/cyme/commit/b99e87a586248fdd6dbf72d5624e5e61e993ff5a)). 206 | - Add the display blocks `uid-class`, `uid-subc-lass`, `uid-protocol`, `class`, and `class-value` for `DeviceBlock`s and `InterfaceBlock`s. These are also added for `--more`. 207 | - Add `feature=udev_hwdb` to guard against systems that have udev but not hwdb support ([cross/#1377](https://github.com/cross-rs/cross/issues/1377))/([libudev-sys/#16](https://github.com/dcuddeback/libudev-sys/pull/16)). 208 | 209 | ### Changed 210 | 211 | - 'usb-ids' crate is now a dependency rather than optional to support `lsusb::names` lookup without udev_hwdb (non-Linux). ([usb-ids.rs/#50](https://github.com/woodruffw/usb-ids.rs/pull/50)) will add extra descriptor parsing in future. 212 | - iString descriptors will now be retrieved in order libusb descriptor -> sysfs cache (Linux) -> udev_hwdb (bundled usb-ids `--feature=udev_hwdb`) -> usb-ids. 213 | 214 | ### Fixes 215 | 216 | - Fix BaseClass as u8 in lsusb --verbose being enum index not repr(c) base class byte. 217 | - Fix BaseClass as u8 in icon serializer being enum index not repc(c) base class byte. 218 | 219 | ## [1.5.2] - 2023-11-01 220 | 221 | _Changelog started._ 222 | 223 | ## [0.2.0] - 2022-11-16 224 | 225 | _First release._ 226 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cyme" 3 | authors = ["John Whittington "] 4 | description = "List system USB buses and devices. A modern cross-platform lsusb" 5 | repository = "https://github.com/tuna-f1sh/cyme" 6 | readme = "README.md" 7 | license = "GPL-3.0-or-later" 8 | rust-version = "1.82" 9 | version = "2.2.2" 10 | edition = "2021" 11 | keywords = ["usb", "lsusb", "system_profiler", "macos", "libusb"] 12 | categories = ["command-line-utilities"] 13 | exclude = [".github", "scripts"] 14 | 15 | [dependencies] 16 | clap = { version = "4.0.22", features = ["derive", "wrap_help"] } # CLI argument parsing 17 | clap_complete = { version = "4.0.6", optional = true } # CLI completions 18 | clap_mangen = { version = "0.2.5", optional = true } # for generating man - could manually do this 19 | colored = "3.0.0" # terminal colouring helper 20 | cansi = { version = "=2.2.1", optional = true } # ANSI escape code helper; decolored - no dependencies 21 | itertools = "0.10.5" # iterator methods used for building device tree 22 | rusb = { version = "0.9.4", optional = true } # libusb bindings 23 | # nusb = { git = "https://github.com/kevinmehall/nusb", rev = "dad53d26", optional = true } # pure Rust USB library 24 | nusb = { git = "https://github.com/kevinmehall/nusb", tag = "v0.2.0-beta.1", optional = true } # pure Rust USB library 25 | serde = { version = "1.0", features = ["derive"] } # --json serialisation and --from-json deserialisation 26 | serde_json = "1.0.87" 27 | serde_with = "2.0.1" 28 | log = "0.4.17" 29 | simple_logger = { version = "4.0.0", features = ["stderr"], optional = false } # perhaps make this optional in the future; only required by bin targets 30 | usb-ids = { version = "1" } # USB ID database 31 | heck = "0.4.0" # common case conversions - could be internal but simple crate with no dependencies 32 | dirs = "6.0.0" # cross-platform XDG_CONFIG_HOME - could be internal since only this path 33 | fastrand = "2.1.1" # fast random number generator for masking serials 34 | terminal_size = "0.2.5" # terminal size for automatic column width during display 35 | strum = "0.26" # enum to string conversion 36 | strum_macros = "0.26" # enum to string conversion 37 | regex = { version = "1.10.5", optional = true } # icon name lookup with regex 38 | uuid = { version = "1.9.1", features = ["serde"] } # descriptor UUID field support as type 39 | pci-ids = "0.2.5" # PCI ID database 40 | unicode-width = "0.2.0" # ensure USB device table is printed with equal width columns - zero dependencies 41 | crossterm = { version = "0.28.1", optional = true } # watch: terminal manipulation 42 | futures-lite = { version = "2.6.0", optional = true } # watch: async helper 43 | chrono = { version = "0.4.39", features = ["serde"] } # watch: event times as human readable 44 | 45 | [dev-dependencies] 46 | diff = "0.1" 47 | assert-json-diff = "2.0.2" 48 | criterion = "0.5.1" 49 | 50 | [target.'cfg(target_os="linux")'.dependencies] 51 | udevrs = { version = "^0.4.0", optional = true } 52 | udevlib = { package = "udev", version = "^0.8.0", optional = true } 53 | 54 | [target.'cfg(target_os="macos")'.dependencies] 55 | core-foundation = "0.9.3" 56 | core-foundation-sys = "0.8.4" 57 | io-kit-sys = "0.4.0" 58 | 59 | [features] 60 | libusb = ["dep:rusb"] # libusb bindings rather than nusb Rust 61 | udev = ["dep:udevrs"] # udev device info lookup 62 | udev_hwdb = ["udevlib?/hwdb"] # udev hardware database lookup rather than usb-ids 63 | udevlib = ["dep:udevlib"] # udev libc bindings rather than Rust 64 | usb_test = [] # testing with phyiscal USB devices 65 | regex_icon = ["dep:regex"] # icon name lookup with regex 66 | cli_generate = ["dep:clap_complete", "dep:clap_mangen"] # for generating man and completions 67 | native = ["nusb", "udev"] # pure Rust USB and udev bindings 68 | ffi = ["libusb", "udevlib"] # C bindings for libusb and libudev 69 | watch = ["crossterm", "futures-lite", "nusb", "cansi"] # watch mode 70 | bin = [] 71 | default = ["native", "regex_icon", "watch", "bin"] # default native Rust USB (nusb, udevrs) with regex icon name lookup 72 | 73 | [[bin]] 74 | name = "cyme" 75 | path = "src/main.rs" 76 | 77 | [[bench]] 78 | name = "get" 79 | harness = false 80 | 81 | [[bench]] 82 | name = "profile" 83 | harness = false 84 | 85 | [profile.release] 86 | lto = true 87 | strip = true 88 | panic = "abort" 89 | codegen-units = 1 # quicker binary, slower build 90 | 91 | [package.metadata.cross.target.arm-unknown-linux-gnueabihf] 92 | pre-build = ["dpkg --add-architecture armhf && apt-get update && apt-get install --assume-yes libusb-1.0-0-dev:armhf libudev-dev:armhf"] 93 | 94 | [package.metadata.cross.target.aarch64-unknown-linux-gnu] 95 | pre-build = ["dpkg --add-architecture arm64 && apt-get update && apt-get install --assume-yes libusb-1.0-0-dev:arm64 libudev-dev:arm64"] 96 | 97 | [package.metadata.cross.target.i686-unknown-linux-gnu] 98 | pre-build = ["dpkg --add-architecture i386 && apt-get update && apt-get install --assume-yes libusb-1.0-0-dev:i386 libudev-dev:i386"] 99 | 100 | [package.metadata.cross.target.x86_64-unknown-linux-gnu] 101 | pre-build = ["apt-get update && apt-get install --assume-yes libusb-1.0-0-dev libudev-dev"] 102 | 103 | [package.metadata.cross.target.aarch64-linux-android] 104 | image = "ghcr.io/cross-rs/aarch64-linux-android:main" 105 | 106 | [package.metadata.deb] 107 | section = "utility" 108 | copyright = "2024, John Whittington " 109 | changelog = "CHANGELOG.md" 110 | extended-description = """Profiles system USB buses and the devices on those buses, including full device descriptors. Compatable with lsusb arguments and output whilst adding new features.""" 111 | assets = [ 112 | ["target/release/cyme", "usr/bin/", "755"], 113 | ["README.md", "usr/share/doc/cyme/README", "644"], 114 | ["doc/cyme.1", "/usr/share/man/man1/cyme.1", "644"], 115 | ] 116 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].name') 2 | VERSION := $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') 3 | OS := $(shell uname) 4 | 5 | RSRCS += $(wildcard src/*.rs) $(wildcard src/**/*.rs) 6 | AUTOCOMPLETES = doc/_$(PROJECT_NAME) doc/$(PROJECT_NAME).bash doc/$(PROJECT_NAME).fish doc/_$(PROJECT_NAME).ps1 7 | DOCS = $(AUTOCOMPLETES) doc/$(PROJECT_NAME).1 doc/cyme_example_config.json 8 | 9 | # ?= allows overriding from command line with 'cross' 10 | CARGO_CMD ?= cargo 11 | CARGO_TARGET_DIR ?= target 12 | PACKAGE_DIR ?= $(CARGO_TARGET_DIR)/packages 13 | CARGO_FLAGS += --locked 14 | 15 | ifeq ($(TARGET),) 16 | PACKAGE_BASE := $(PROJECT_NAME)-v$(VERSION)-$(OS) 17 | TARGET_DIR := $(CARGO_TARGET_DIR) 18 | else 19 | PACKAGE_BASE := $(PROJECT_NAME)-v$(VERSION)-$(TARGET) 20 | TARGET_DIR := $(CARGO_TARGET_DIR)/$(TARGET) 21 | ifneq ($(TARGET),universal-apple-darwin) 22 | CARGO_FLAGS += --target $(TARGET) 23 | endif 24 | endif 25 | RELEASE_BIN := $(TARGET_DIR)/release/$(PROJECT_NAME) 26 | 27 | ifeq ($(findstring windows,$(TARGET)),windows) 28 | ARCHIVE_EXT := zip 29 | else 30 | ARCHIVE_EXT := tar.gz 31 | endif 32 | ARCHIVE := $(PACKAGE_DIR)/$(PACKAGE_BASE).$(ARCHIVE_EXT) 33 | 34 | ifeq ($(OS), Darwin) 35 | PREFIX ?= /usr/local 36 | else 37 | PREFIX ?= /usr 38 | endif 39 | 40 | BIN_PATH ?= $(PREFIX)/bin 41 | BASH_COMPLETION_PATH ?= $(PREFIX)/share/bash-completion/completions 42 | ZSH_COMPLETION_PATH ?= $(PREFIX)/share/zsh/site-functions 43 | MAN_PAGE_PATH ?= $(PREFIX)/share/man/man1 44 | 45 | .PHONY: release install clean generated docs gen enter_version new_version release_version test package dpkg 46 | 47 | release: $(RELEASE_BIN) 48 | @echo "$(RELEASE_BIN)" 49 | 50 | install: release 51 | @echo "Installing $(PROJECT_NAME) $(VERSION)" 52 | install -Dm755 "$(RELEASE_BIN)" "$(DESTDIR)$(BIN_PATH)/$(PROJECT_NAME)" 53 | install -Dm644 ./doc/$(PROJECT_NAME).1 "$(DESTDIR)$(MAN_PAGE_PATH)/$(PROJECT_NAME).1" 54 | @if [ -d "$(DESTDIR)$(BASH_COMPLETION_PATH)" ]; then \ 55 | install -vDm0644 ./doc/$(PROJECT_NAME).bash "$(DESTDIR)$(BASH_COMPLETION_PATH)/$(PROJECT_NAME).bash"; \ 56 | fi 57 | @if [ -d "$(DESTDIR)$(ZSH_COMPLETION_PATH)" ]; then \ 58 | install -vDm0644 ./doc/_$(PROJECT_NAME) "$(DESTDIR)$(ZSH_COMPLETION_PATH)/_$(PROJECT_NAME)"; \ 59 | fi 60 | 61 | clean: 62 | $(CARGO_CMD) clean 63 | 64 | generated: $(DOCS) 65 | # I'm lazy to remember what I called it! 66 | docs: $(DOCS) 67 | gen: $(DOCS) 68 | 69 | enter_version: 70 | @echo "Current version: $(VERSION)" 71 | @echo "Enter new version: " 72 | @read new_version; \ 73 | sed -i "s/^version = .*/version = \"$$new_version\"/" Cargo.toml 74 | # update because Cargo.lock references self for tests 75 | $(CARGO_CMD) update 76 | 77 | new_version: test enter_version gen 78 | 79 | release_version: 80 | @exec scripts/release_version.sh 81 | 82 | test: 83 | $(CARGO_CMD) test $(CARGO_FLAGS) $(CARGO_TEST_FLAGS) 84 | # test with libusb profiler 85 | $(CARGO_CMD) test $(CARGO_FLAGS) $(CARGO_TEST_FLAGS) --no-default-features -F=ffi 86 | 87 | package: $(ARCHIVE) 88 | @echo "$(ARCHIVE)" 89 | 90 | dpkg: $(RELEASE_BIN) 91 | ifeq ($(TARGET),) 92 | cargo deb --no-strip --no-build 93 | else 94 | cargo deb --target $(TARGET) --no-strip --no-build 95 | endif 96 | 97 | $(DOCS): Cargo.toml $(RSRCS) 98 | @echo "Generating docs for $(PROJECT_NAME) $(VERSION)" 99 | $(CARGO_CMD) run $(CARGO_FLAGS) -F=cli_generate -- --gen 100 | 101 | $(RELEASE_BIN): Cargo.lock $(RSRCS) 102 | ifeq ($(TARGET),universal-apple-darwin) 103 | cargo build --target aarch64-apple-darwin $(CARGO_FLAGS) --release 104 | cargo build --target x86_64-apple-darwin $(CARGO_FLAGS) --release 105 | mkdir -p $(shell dirname $(RELEASE_BIN)) 106 | lipo -create -output $(RELEASE_BIN) \ 107 | $(CARGO_TARGET_DIR)/aarch64-apple-darwin/release/$(PROJECT_NAME) \ 108 | $(CARGO_TARGET_DIR)/x86_64-apple-darwin/release/$(PROJECT_NAME) 109 | else 110 | $(CARGO_CMD) build $(CARGO_FLAGS) --release 111 | endif 112 | 113 | $(ARCHIVE): $(RELEASE_BIN) README.md LICENSE CHANGELOG.md $(DOCS) 114 | mkdir -p $(PACKAGE_DIR)/$(PACKAGE_BASE) 115 | cp $(RELEASE_BIN) $(PACKAGE_DIR)/$(PACKAGE_BASE)/ 116 | cp README.md LICENSE CHANGELOG.md $(PACKAGE_DIR)/$(PACKAGE_BASE)/ 117 | cp 'doc/$(PROJECT_NAME).1' $(PACKAGE_DIR)/$(PACKAGE_BASE)/ 118 | mkdir -p $(PACKAGE_DIR)/$(PACKAGE_BASE)/autocomplete 119 | cp $(AUTOCOMPLETES) $(PACKAGE_DIR)/$(PACKAGE_BASE)/autocomplete/ 120 | ifeq ($(ARCHIVE_EXT),zip) 121 | cd $(PACKAGE_DIR) && 7z -y a $(PACKAGE_BASE).zip $(PACKAGE_BASE) 122 | else 123 | cd $(PACKAGE_DIR) && tar czf $(PACKAGE_BASE).tar.gz $(PACKAGE_BASE) 124 | endif 125 | rm -rf $(PACKAGE_DIR)/$(PACKAGE_BASE) 126 | 127 | %.deb: 128 | ifeq ($(TARGET),) 129 | cargo deb --no-strip --no-build --output $@ 130 | else 131 | cargo deb --target $(TARGET) --no-strip --no-build --output $@ 132 | endif 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | o 3 | o /---o 4 | /---/---o 5 | o---/ 6 | \---\---o 7 | o \---o 8 | o 9 | ``` 10 | # Cyme 11 | 12 | [![Crates.io](https://img.shields.io/crates/v/cyme?style=flat-square)](https://crates.io/crates/cyme) 13 | [![docs.rs](https://img.shields.io/docsrs/cyme?style=flat-square)](https://docs.rs/cyme/latest/cyme/) 14 | 15 | List system USB buses and devices. A modern cross-platform `lsusb` that attempts to maintain compatibility with, but also add new features. Profiles system USB buses and the devices on those buses, including full device descriptors. 16 | 17 | As a developer of embedded devices, I use a USB list tool on a frequent basis and developed this to cater to what I believe are the short comings of `lsusb`: verbose dump is mostly _too_ verbose, tree doesn't contain useful data on the whole, it barely works on non-Linux platforms and modern terminals support features that make glancing through the data easier. 18 | 19 | The project started as a quick replacement for the barely working [lsusb script](https://github.com/jlhonora/lsusb) and a Rust project to keep me up to date! Like most fun projects, it quickly experienced feature creep as I developed it into a cross-platform replacement for `lsusb`. It started as a macOS `system_profiler` parser, evolved to include a 'libusb' based profiler for reading full device descriptors and now defaults to a pure Rust profiler using [nusb](https://github.com/kevinmehall/nusb). 20 | 21 | It's not perfect as it started out as a Rust refresher but I had a lot of fun developing it and hope others will find it useful and can contribute. Reading around the [lsusb source code](https://github.com/gregkh/usbutils/blob/master/lsusb.c), USB-IF and general USB information was also a good knowledge builder. 22 | 23 | The name comes from the technical term for the type of blossom on a Apple tree: [cyme](https://en.wikipedia.org/wiki/Inflorescence#Determinate_or_cymose) - it is Apple related and also looks like a USB device tree 😃🌸. 24 | 25 | ![cli tree output](./doc/cli-tree.png) 26 | 27 | # Features 28 | 29 | * Compatible with `lsusb` using `--lsusb` argument. Supports all arguments including `--verbose` output - fully parsed device descriptors! Output is identical for use with no args (list), tree (excluding drivers on non-Linux) and should match for verbose (perhaps formatting differences). 30 | * Default build is a native Rust profiler using [nusb](https://docs.rs/nusb/latest/nusb). 31 | * Filters like `lsusb` but that also work when printing `--tree`. Adds `--filter-name`, `--filter-serial`, `--filter-class` and option to hide empty `--hide-buses`/`--hide-hubs`. 32 | * Improved `--tree` mode; shows device, configurations, interfaces and endpoints as tree depending on level of `--verbose`. 33 | * Controllable display `--blocks` for device, bus `--bus-blocks`, configurations `--config-blocks`, interfaces `--interface-blocks` and endpoints `--endpoint-blocks`. Use `--more` to see more by default. 34 | * Modern terminal features with coloured output, utf-8 characters and icon look-up based device data. Can be turned off and customised. See `--encoding` (glyphs [default], utf8 and ascii), which can keep icons/tree within a certain encoding, `--color` (auto [default], always and never) and `--icon` (auto [default], always and never). Auto `--icon` will only show icons if all icons to be shown are supported by the `--encoding`. 35 | * Can be used as a library too with system profiler module, USB descriptor modules and `display` module for printing amongst others. 36 | * `--json` output that honours filters and `--tree`. 37 | * `--headers` to show meta data only when asked and not take space otherwise. 38 | * `--mask-serials` to either '\*' or randomise serial string for sharing dumps with sensitive serial numbers. 39 | * Auto-scaling to terminal width. Variable length strings such as descriptors will be truncated with a '...' to indicate this. Can be disabled with config option 'no-auto-width' and a fixed max defined with 'max-variable-string-len'. 40 | * `cyme watch` subcommand to watch for USB device hotplug events and also live edit display settings. Works with all global flags. 41 | * Targets for Linux, macOS and Windows. 42 | 43 | ## Demo 44 | 45 | * [General use asciicast](https://asciinema.org/a/IwYyZMrGMbXL4g15qDIaUViyM) 46 | * [Watch sub-command](https://youtu.be/ohRBrVBRolA) 47 | 48 | # Install 49 | 50 | ## Requirements 51 | 52 | For pre-compiled binaries, see the [releases](https://github.com/tuna-f1sh/cyme/releases). Pre-compiled builds use native profiling backends and should require no extra dependencies. 53 | 54 | From crates.io with a Rust tool-chain installed: `cargo install cyme --git https://github.com/tuna-f1sh/cyme` (from GitHub as crates.io pinned at the moment). To do it from within a local clone: `cargo install --path .`. 55 | 56 | ### Package Managers 57 | 58 | * [Homebrew 'cyme'](https://formulae.brew.sh/formula/cyme) which will also install a man page, completions and the 'libusb' dependency: 59 | 60 | ```bash 61 | brew install cyme 62 | ``` 63 | 64 | * [Arch Linux official package](https://archlinux.org/packages/extra/x86_64/cyme/) 65 | 66 | ```bash 67 | pacman -S cyme 68 | ``` 69 | 70 | * [Debian packages as part of release](https://github.com/tuna-f1sh/cyme/releases) - need a Debian maintainer for this. 71 | 72 | More package managers to come/package distribution, please feel free to create a PR if you want to help out here. 73 | 74 | ## Alias `lsusb` 75 | 76 | If one wishes to create a macOS version of lsusb or just use this instead, create an alias one's environment with the `--lsusb` compatibility flag: 77 | 78 | `alias lsusb='cyme --lsusb'` 79 | 80 | ## Linux udev Information 81 | 82 | > [!NOTE] 83 | > Only supported on Linux targets. 84 | 85 | To obtain device and interface drivers being used on Linux like `lsusb`, one can use the `--features udev` feature when building - it's a default feature. The feature uses the Rust crate [udevrs](https://crates.io/crates/udevrs) to obtain the information. To use the C FFI libudev library, use `--no-default-features --features udevlib` which will use the 'libudev' crate. Note that this will require 'libudev-dev' to be installed on the host machine. 86 | 87 | To lookup USB IDs from the udev hwdb as well (like `lsusb`) use `--features udev_hwdb`. Without hwdb, `cyme` will use the 'usb-ids' crate, which is the same source as the hwdb binary data but the bundled hwdb may differ due to customisations or last update ('usb-ids' will be most up to date). 88 | 89 | ## Profilers and Feature Flags 90 | 91 | ### Native 92 | 93 | Uses native Rust [nusb](https://docs.rs/nusb/latest/nusb) and [udevrs](https://crates.io/crates/udevrs) for profiling devices: sysfs (Linux), IOKit (macOS) and WinUSB. 94 | 95 | It is the default profiler as of 2.0.0. Use `--feature=native` ('nusb' and 'udevrs' on Linux) or `--feature=nusb` to manually specify. 96 | 97 | ### Libusb 98 | 99 | Uses 'libusb' for profiling devices. Requires [libusb 1.0.0](https://libusb.info) to be installed: `brew install libusb`, `sudo apt install libusb-1.0-0-dev` or one's package manager of choice. 100 | 101 | Was the default feature before 2.0.0 for gathering verbose information. It is the profiler used by `lsusb` but there should be no difference in output between the two, since cyme uses control messages to gather the same information. If one wishes to use 'libusb', use `--no-default-features` and `--feature=libusb` or `--feature=ffi` for udevlib too. 102 | 103 | > [!NOTE] 104 | > 'libusb' does not profile buses on non-Linux systems (since it relies on root\_hubs). On these platforms, `cyme` will generate generic bus information. 105 | 106 | ### macOS `system_profiler` 107 | 108 | Uses the macOS `system_profiler SPUSBDataType` command to profile devices. 109 | 110 | Was the default feature before 2.0.0 for macOS systems to provide the base information; 'libusb' was used to open devices for verbose information. It is not used anymore if using the default native profiler but can be forced with `--system-profiler` - the native profiler uses the same IOKit backend but is much faster as it is not deserializing JSON. It also always captures bus numbers where `system_profiler` does not. 111 | 112 | > [!TIP] 113 | > If wishing to use only macOS `system_profiler` and not obtain more verbose information, remove default features with `cargo install --no-default-features cyme`. There is not much to be gained by this considering that the default native profiler uses the same IOKit as a backend, can open devices to read descriptors (verbose mode) and is much faster. 114 | 115 | # Usage 116 | 117 | Use `cyme --help` for basic usage or `man ./doc/cyme.1`. There are also autocompletions in './doc'. 118 | 119 | ## Examples 120 | 121 | ### Tree 122 | 123 | ```bash 124 | # List all USB devices and buses in a tree format with default display blocks 125 | cyme --tree 126 | # As above but with configurations too 127 | cyme --tree --verbose 128 | # And with interfaces and endpoints - each verbose level goes futher down the USB descriptor tree. Using short arg here. 129 | cyme --tree -vvv 130 | # List all USB devices and buses in a tree format with more display blocks, all verbose levels and headings to show what is being displayed 131 | cyme --tree --more --headings 132 | # Export the tree to a JSON file - --json works with all options 133 | cyme --tree --verbose --json > tree.json 134 | # Then import the JSON file to view the system USB tree as it was when exported. All cyme args can be used with this static import as if it was profiled data. 135 | cyme --from-json tree.json 136 | ``` 137 | 138 | ### lsusb 139 | 140 | ```bash 141 | # List all USB devices and buses like 'lsusb' 142 | cyme --lsusb 143 | # lsusb verbose device dump including all descriptor informaion 144 | cyme --lsusb --verbose 145 | # lsusb tree mode (can add verbose levels [-v]) 146 | cyme --lsusb --tree 147 | ``` 148 | 149 | ### Blocks 150 | 151 | See `cyme --help` for blocks available. One can also omit the value to the arg to show options. Specifying multiple blocks requires multiple args. 152 | 153 | ```bash 154 | # List USB devices with more display blocks 155 | cyme --more 156 | # List USB devices with chosen blocks: name, vid, pid, serial, speed (can use short -b) 157 | cyme --blocks name --blocks vendor-id --blocks product-id --blocks serial -b speed 158 | # Customise other blocks - it's probably easier to use Config at this point 159 | cyme --blocks name --bus-blocks name --config-blocks name --interface-blocks class --endpoint-blocks number 160 | ``` 161 | 162 | ### Filtering 163 | 164 | ```bash 165 | # Filter for only Apple devices (vid:pid is base16) 166 | cyme -d 0x05ac 167 | # Specifically an Apple Headset, masking the serial number with '*' 168 | cyme -d 05ac:8103 --mask-serials hide 169 | # Filter for only devices with a certain name and class (filters can be combined) 170 | cyme --filter-name "Black Magic" --filter-class cdc-data 171 | ``` 172 | 173 | ## Crate 174 | 175 | For usage as a library for profiling system USB devices, the crate is 100% documented so look at [docs.rs](https://docs.rs/cyme/latest/cyme/). The main useful modules for import are [profiler](https://docs.rs/cyme/latest/cyme/profiler/index.html), and [usb](https://docs.rs/cyme/latest/cyme/usb/index.html). 176 | 177 | There are also some examples in 'examples/', these can be run with `cargo run --example filter_devices`. It wasn't really written from the ground-up to be a crate but all the USB descriptors might be useful for high level USB profiling. 178 | 179 | ## Config 180 | 181 | `cyme` will check for a 'cyme.json' config file in: 182 | 183 | * Linux: "$XDG\_CONFIG\_HOME/cyme or $HOME/.config/cyme" 184 | * macOS: "$HOME/Library/Application Support/cyme" 185 | * Windows: "{FOLDERID\_RoamingAppData}/cyme" 186 | 187 | One can also be supplied with `--config`. Copy or refer to './doc/cyme\_example\_config.json' for configurables. The file is essentially the default args; supplied args will override these. Use `--debug` to see where it is looking or if it's not loading. 188 | 189 | `cyme watch` can also be used to live edit display settings then save the config to the default location with 'Ctrl-s'. It's probably the easiest way to customise display blocks. 190 | 191 | ### Custom Icons and Colours 192 | 193 | See './doc/cyme\_example\_config.json' for an example of how icons can be defined and also the [docs](https://docs.rs/cyme/latest/cyme/icon/enum.Icon.html). The config can exclude the "user"/"colours" keys if one wishes not to define any new icons/colours. 194 | 195 | Icons are looked up in an order of User -> Default. For devices: `Name` -> `VidPid` -> `VidPidMsb` -> `Vid` -> `UnknownVendor` -> `get_default_vidpid_icon`, classes: `ClassifierSubProtocol` -> `Classifier` -> `UndefinedClassifier` -> `get_default_classifier_icon`. User supplied colours override all internal; if a key is missing, it will be `None`. 196 | 197 | #### Icons not Showing/Boxes with Question Marks 198 | 199 | Copied from [lsd](https://github.com/lsd-rs/lsd#icons-not-showing-up): For `cyme` to be able to display icons, the font has to include special font glyphs. This might not be the case for most fonts that you download. Thankfully, you can patch most fonts using [NerdFont](https://www.nerdfonts.com/) and add these icons. Or you can just download an already patched version of your favourite font from [NerdFont font download page](https://www.nerdfonts.com/font-downloads). 200 | Here is a guide on how to setup fonts on [macOS](https://github.com/Peltoche/lsd/issues/199#issuecomment-494218334) and [Android](https://github.com/Peltoche/lsd/issues/423). 201 | 202 | To check if the font you are using is setup correctly, try running the following snippet in a shell and see if that [prints a folder icon](https://github.com/Peltoche/lsd/issues/510#issuecomment-860000306). If it prints a box, or question mark or something else, then you might have some issues in how you setup the font or how your terminal emulator renders the font. 203 | 204 | ```sh 205 | echo $'\uf115' 206 | ``` 207 | 208 | If one does not want icons, provide a config file with custom blocks not including the any 'icon\*' blocks - see the example config. Alternatively, to only use standard UTF-8 characters supported by all fonts (no private use area) pass `--encoding utf8` and `--icon auto` (default). The `--icon auto` will drop the icon blocks if the characters matched are not supported by the `--encoding`. 209 | 210 | For no icons at all, use the hidden `--no-icons` or `--icon never` args. 211 | 212 | # Known Issues 213 | 214 | * `sudo` is required to open and read Linux root\_hub string descriptors and potentially all devices if the user does not have [permissions](https://docs.rs/nusb/latest/nusb/#linux). The program works fine without these however, as will use sysfs/hwdb/'usb-ids' like lsusb. Use debugging `-z` to see what devices failed to read. The env CYME_PRINT_NON_CRITICAL_PROFILER_STDERR can be used to print these to stderr. `--lsusb --verbose` will print a message to stderr always to match the 'lsusb' behaviour. 215 | * Users cannot open special non-user devices on Apple buses (VHCI); T2 chip for example. These will still be listed with 'native' and `system_profiler` but not `--force-libusb`. They will not print verbose information however and log an error if `--verbose` is used/print if `--lsusb`. 216 | -------------------------------------------------------------------------------- /benches/get.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use cyme::profiler; 3 | use cyme::usb::{DevicePath, EndpointPath, PortPath}; 4 | use std::sync::LazyLock; 5 | 6 | fn bench_dump() -> profiler::SystemProfile { 7 | profiler::read_json_dump("./tests/data/cyme_libusb_macos_tree.json").unwrap() 8 | } 9 | 10 | static DUMP: LazyLock = LazyLock::new(bench_dump); 11 | 12 | pub fn get_node(c: &mut Criterion) { 13 | let dump = &DUMP; 14 | c.bench_function("bench_get_device", |b| { 15 | b.iter(|| { 16 | let result = dump.get_node(&PortPath::new(2, vec![2, 3, 1])); 17 | black_box(result); 18 | }); 19 | }); 20 | c.bench_function("bench_get_root", |b| { 21 | b.iter(|| { 22 | let result = dump.get_node(&PortPath::new(2, vec![0])); 23 | black_box(result); 24 | }); 25 | }); 26 | } 27 | 28 | pub fn get_interface(c: &mut Criterion) { 29 | let dump = &DUMP; 30 | c.bench_function("bench_get_interface", |b| { 31 | b.iter(|| { 32 | let result = 33 | dump.get_interface(&DevicePath::new(20, vec![3, 3], Some(1), Some(5), None)); 34 | black_box(result); 35 | }); 36 | }); 37 | } 38 | 39 | pub fn get_endpoint(c: &mut Criterion) { 40 | let dump = &DUMP; 41 | c.bench_function("bench_get_endpoint", |b| { 42 | b.iter(|| { 43 | let result = dump.get_endpoint(&EndpointPath::new(20, vec![3, 3], 1, 5, 0, 0x85)); 44 | black_box(result); 45 | }); 46 | }); 47 | } 48 | 49 | criterion_group!(single_benches, get_node, get_interface, get_endpoint); 50 | criterion_main!(single_benches); 51 | -------------------------------------------------------------------------------- /benches/profile.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use cyme::profiler; 3 | 4 | pub fn profile(c: &mut Criterion) { 5 | c.bench_function("bench_gather_system_profile", |b| { 6 | b.iter(|| { 7 | black_box(profiler::get_spusb_with_extra().unwrap()); 8 | }); 9 | }); 10 | #[cfg(target_os = "macos")] 11 | c.bench_function("bench_gather_system_profile_sp", |b| { 12 | b.iter(|| { 13 | black_box(profiler::macos::get_spusb().unwrap()); 14 | }); 15 | }); 16 | } 17 | 18 | criterion_group!(single_benches, profile); 19 | criterion_main!(single_benches); 20 | -------------------------------------------------------------------------------- /doc/_cyme.ps1: -------------------------------------------------------------------------------- 1 | 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | Register-ArgumentCompleter -Native -CommandName 'cyme' -ScriptBlock { 6 | param($wordToComplete, $commandAst, $cursorPosition) 7 | 8 | $commandElements = $commandAst.CommandElements 9 | $command = @( 10 | 'cyme' 11 | for ($i = 1; $i -lt $commandElements.Count; $i++) { 12 | $element = $commandElements[$i] 13 | if ($element -isnot [StringConstantExpressionAst] -or 14 | $element.StringConstantType -ne [StringConstantType]::BareWord -or 15 | $element.Value.StartsWith('-') -or 16 | $element.Value -eq $wordToComplete) { 17 | break 18 | } 19 | $element.Value 20 | }) -join ';' 21 | 22 | $completions = @(switch ($command) { 23 | 'cyme' { 24 | [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Show only devices with the specified vendor and product ID numbers (in hexadecimal) in format VID:[PID]') 25 | [CompletionResult]::new('--vidpid', '--vidpid', [CompletionResultType]::ParameterName, 'Show only devices with the specified vendor and product ID numbers (in hexadecimal) in format VID:[PID]') 26 | [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Show only devices with specified device and/or bus numbers (in decimal) in format [[bus]:][devnum]') 27 | [CompletionResult]::new('--show', '--show', [CompletionResultType]::ParameterName, 'Show only devices with specified device and/or bus numbers (in decimal) in format [[bus]:][devnum]') 28 | [CompletionResult]::new('-D', '-D ', [CompletionResultType]::ParameterName, 'Selects which device lsusb will examine - supplied as Linux /dev/bus/usb/BBB/DDD style path') 29 | [CompletionResult]::new('--device', '--device', [CompletionResultType]::ParameterName, 'Selects which device lsusb will examine - supplied as Linux /dev/bus/usb/BBB/DDD style path') 30 | [CompletionResult]::new('--filter-name', '--filter-name', [CompletionResultType]::ParameterName, 'Filter on string contained in name') 31 | [CompletionResult]::new('--filter-serial', '--filter-serial', [CompletionResultType]::ParameterName, 'Filter on string contained in serial') 32 | [CompletionResult]::new('--filter-class', '--filter-class', [CompletionResultType]::ParameterName, 'Filter on USB class code') 33 | [CompletionResult]::new('-b', '-b', [CompletionResultType]::ParameterName, 'Specify the blocks which will be displayed for each device and in what order. Supply arg multiple times to specify multiple blocks') 34 | [CompletionResult]::new('--blocks', '--blocks', [CompletionResultType]::ParameterName, 'Specify the blocks which will be displayed for each device and in what order. Supply arg multiple times to specify multiple blocks') 35 | [CompletionResult]::new('--bus-blocks', '--bus-blocks', [CompletionResultType]::ParameterName, 'Specify the blocks which will be displayed for each bus and in what order. Supply arg multiple times to specify multiple blocks') 36 | [CompletionResult]::new('--config-blocks', '--config-blocks', [CompletionResultType]::ParameterName, 'Specify the blocks which will be displayed for each configuration and in what order. Supply arg multiple times to specify multiple blocks') 37 | [CompletionResult]::new('--interface-blocks', '--interface-blocks', [CompletionResultType]::ParameterName, 'Specify the blocks which will be displayed for each interface and in what order. Supply arg multiple times to specify multiple blocks') 38 | [CompletionResult]::new('--endpoint-blocks', '--endpoint-blocks', [CompletionResultType]::ParameterName, 'Specify the blocks which will be displayed for each endpoint and in what order. Supply arg multiple times to specify multiple blocks') 39 | [CompletionResult]::new('--sort-devices', '--sort-devices', [CompletionResultType]::ParameterName, 'Sort devices operation') 40 | [CompletionResult]::new('--group-devices', '--group-devices', [CompletionResultType]::ParameterName, 'Group devices by value when listing') 41 | [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'Output coloring mode') 42 | [CompletionResult]::new('--encoding', '--encoding', [CompletionResultType]::ParameterName, 'Output character encoding') 43 | [CompletionResult]::new('--icon', '--icon', [CompletionResultType]::ParameterName, 'When to print icon blocks') 44 | [CompletionResult]::new('--from-json', '--from-json', [CompletionResultType]::ParameterName, 'Read from json output rather than profiling system') 45 | [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Path to user config file to use for custom icons, colours and default settings') 46 | [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Path to user config file to use for custom icons, colours and default settings') 47 | [CompletionResult]::new('--mask-serials', '--mask-serials', [CompletionResultType]::ParameterName, 'Mask serial numbers with ''*'' or random chars') 48 | [CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'Attempt to maintain compatibility with lsusb output') 49 | [CompletionResult]::new('--lsusb', '--lsusb', [CompletionResultType]::ParameterName, 'Attempt to maintain compatibility with lsusb output') 50 | [CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'Dump USB device hierarchy as a tree') 51 | [CompletionResult]::new('--tree', '--tree', [CompletionResultType]::ParameterName, 'Dump USB device hierarchy as a tree') 52 | [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Verbosity level (repeat provides count): 1 prints device configurations; 2 prints interfaces; 3 prints interface endpoints; 4 prints everything and more blocks') 53 | [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Verbosity level (repeat provides count): 1 prints device configurations; 2 prints interfaces; 3 prints interface endpoints; 4 prints everything and more blocks') 54 | [CompletionResult]::new('-m', '-m', [CompletionResultType]::ParameterName, 'Print more blocks by default at each verbosity') 55 | [CompletionResult]::new('--more', '--more', [CompletionResultType]::ParameterName, 'Print more blocks by default at each verbosity') 56 | [CompletionResult]::new('--sort-buses', '--sort-buses', [CompletionResultType]::ParameterName, 'Sort devices by bus number. If using any sort-devices other than no-sort, this happens automatically') 57 | [CompletionResult]::new('--hide-buses', '--hide-buses', [CompletionResultType]::ParameterName, 'Hide empty buses when printing tree; those with no devices') 58 | [CompletionResult]::new('--hide-hubs', '--hide-hubs', [CompletionResultType]::ParameterName, 'Hide empty hubs when printing tree; those with no devices. When listing will hide hubs regardless of whether empty of not') 59 | [CompletionResult]::new('--list-root-hubs', '--list-root-hubs', [CompletionResultType]::ParameterName, 'Show root hubs when listing; Linux only') 60 | [CompletionResult]::new('--decimal', '--decimal', [CompletionResultType]::ParameterName, 'Show base16 values as base10 decimal instead') 61 | [CompletionResult]::new('--no-padding', '--no-padding', [CompletionResultType]::ParameterName, 'Disable padding to align blocks - will cause --headings to become maligned') 62 | [CompletionResult]::new('--no-color', '--no-color', [CompletionResultType]::ParameterName, 'Disable coloured output, can also use NO_COLOR environment variable') 63 | [CompletionResult]::new('--ascii', '--ascii', [CompletionResultType]::ParameterName, 'Disables icons and utf-8 characters') 64 | [CompletionResult]::new('--no-icons', '--no-icons', [CompletionResultType]::ParameterName, 'Disables all Block icons by not using any IconTheme. Providing custom XxxxBlocks without any icons is a nicer way to do this') 65 | [CompletionResult]::new('--headings', '--headings', [CompletionResultType]::ParameterName, 'Show block headings') 66 | [CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'Output as json format after sorting, filters and tree settings are applied; without -tree will be flattened dump of devices') 67 | [CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Force pure libusb profiler on macOS rather than combining system_profiler output') 68 | [CompletionResult]::new('--force-libusb', '--force-libusb', [CompletionResultType]::ParameterName, 'Force pure libusb profiler on macOS rather than combining system_profiler output') 69 | [CompletionResult]::new('-z', '-z', [CompletionResultType]::ParameterName, 'Turn debugging information on. Alternatively can use RUST_LOG env: INFO, DEBUG, TRACE') 70 | [CompletionResult]::new('--debug', '--debug', [CompletionResultType]::ParameterName, 'Turn debugging information on. Alternatively can use RUST_LOG env: INFO, DEBUG, TRACE') 71 | [CompletionResult]::new('--gen', '--gen', [CompletionResultType]::ParameterName, 'Generate cli completions and man page') 72 | [CompletionResult]::new('--system-profiler', '--system-profiler', [CompletionResultType]::ParameterName, 'Use the system_profiler command on macOS to get USB data') 73 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') 74 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')') 75 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 76 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 77 | [CompletionResult]::new('watch', 'watch', [CompletionResultType]::ParameterValue, 'Watch for USB devices being connected and disconnected') 78 | [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') 79 | break 80 | } 81 | 'cyme;watch' { 82 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 83 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 84 | break 85 | } 86 | 'cyme;help' { 87 | [CompletionResult]::new('watch', 'watch', [CompletionResultType]::ParameterValue, 'Watch for USB devices being connected and disconnected') 88 | [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') 89 | break 90 | } 91 | 'cyme;help;watch' { 92 | break 93 | } 94 | 'cyme;help;help' { 95 | break 96 | } 97 | }) 98 | 99 | $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | 100 | Sort-Object -Property ListItemText 101 | } 102 | -------------------------------------------------------------------------------- /doc/cli-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuna-f1sh/cyme/5f71876c8ccd906bba830b04eeb7061f78618296/doc/cli-tree.png -------------------------------------------------------------------------------- /doc/cyme.bash: -------------------------------------------------------------------------------- 1 | _cyme() { 2 | local i cur prev opts cmd 3 | COMPREPLY=() 4 | if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then 5 | cur="$2" 6 | else 7 | cur="${COMP_WORDS[COMP_CWORD]}" 8 | fi 9 | prev="$3" 10 | cmd="" 11 | opts="" 12 | 13 | for i in "${COMP_WORDS[@]:0:COMP_CWORD}" 14 | do 15 | case "${cmd},${i}" in 16 | ",$1") 17 | cmd="cyme" 18 | ;; 19 | cyme,help) 20 | cmd="cyme__help" 21 | ;; 22 | cyme,watch) 23 | cmd="cyme__watch" 24 | ;; 25 | cyme__help,help) 26 | cmd="cyme__help__help" 27 | ;; 28 | cyme__help,watch) 29 | cmd="cyme__help__watch" 30 | ;; 31 | *) 32 | ;; 33 | esac 34 | done 35 | 36 | case "${cmd}" in 37 | cyme) 38 | opts="-l -t -d -s -D -v -b -m -F -c -z -h -V --lsusb --tree --vidpid --show --device --filter-name --filter-serial --filter-class --verbose --blocks --bus-blocks --config-blocks --interface-blocks --endpoint-blocks --more --sort-devices --sort-buses --group-devices --hide-buses --hide-hubs --list-root-hubs --decimal --no-padding --color --no-color --encoding --ascii --no-icons --icon --headings --json --from-json --force-libusb --config --debug --mask-serials --gen --system-profiler --help --version watch help" 39 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 40 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 41 | return 0 42 | fi 43 | case "${prev}" in 44 | --vidpid) 45 | COMPREPLY=($(compgen -f "${cur}")) 46 | return 0 47 | ;; 48 | -d) 49 | COMPREPLY=($(compgen -f "${cur}")) 50 | return 0 51 | ;; 52 | --show) 53 | COMPREPLY=($(compgen -f "${cur}")) 54 | return 0 55 | ;; 56 | -s) 57 | COMPREPLY=($(compgen -f "${cur}")) 58 | return 0 59 | ;; 60 | --device) 61 | COMPREPLY=($(compgen -f "${cur}")) 62 | return 0 63 | ;; 64 | -D) 65 | COMPREPLY=($(compgen -f "${cur}")) 66 | return 0 67 | ;; 68 | --filter-name) 69 | COMPREPLY=($(compgen -f "${cur}")) 70 | return 0 71 | ;; 72 | --filter-serial) 73 | COMPREPLY=($(compgen -f "${cur}")) 74 | return 0 75 | ;; 76 | --filter-class) 77 | COMPREPLY=($(compgen -W "use-interface-descriptor audio cdc-communications hid physical image printer mass-storage hub cdc-data smart-card content-security video personal-healthcare audio-video billboard usb-type-c-bridge bdp mctp i3c-device diagnostic wireless-controller miscellaneous application-specific-interface vendor-specific-class" -- "${cur}")) 78 | return 0 79 | ;; 80 | --blocks) 81 | COMPREPLY=($(compgen -W "bus-number device-number branch-position port-path sys-path driver icon vendor-id product-id name manufacturer product-name vendor-name serial speed negotiated-speed tree-positions bus-power bus-power-used extra-current-used bcd-device bcd-usb base-class sub-class protocol uid-class uid-sub-class uid-protocol class base-value last-event event-icon" -- "${cur}")) 82 | return 0 83 | ;; 84 | -b) 85 | COMPREPLY=($(compgen -W "bus-number device-number branch-position port-path sys-path driver icon vendor-id product-id name manufacturer product-name vendor-name serial speed negotiated-speed tree-positions bus-power bus-power-used extra-current-used bcd-device bcd-usb base-class sub-class protocol uid-class uid-sub-class uid-protocol class base-value last-event event-icon" -- "${cur}")) 86 | return 0 87 | ;; 88 | --bus-blocks) 89 | COMPREPLY=($(compgen -W "bus-number icon name host-controller host-controller-vendor host-controller-device pci-vendor pci-device pci-revision port-path" -- "${cur}")) 90 | return 0 91 | ;; 92 | --config-blocks) 93 | COMPREPLY=($(compgen -W "name number num-interfaces attributes icon-attributes max-power" -- "${cur}")) 94 | return 0 95 | ;; 96 | --interface-blocks) 97 | COMPREPLY=($(compgen -W "name number port-path base-class sub-class protocol alt-setting driver sys-path num-endpoints icon uid-class uid-sub-class uid-protocol class base-value" -- "${cur}")) 98 | return 0 99 | ;; 100 | --endpoint-blocks) 101 | COMPREPLY=($(compgen -W "number direction transfer-type sync-type usage-type max-packet-size interval" -- "${cur}")) 102 | return 0 103 | ;; 104 | --sort-devices) 105 | COMPREPLY=($(compgen -W "device-number branch-position no-sort" -- "${cur}")) 106 | return 0 107 | ;; 108 | --group-devices) 109 | COMPREPLY=($(compgen -W "no-group bus" -- "${cur}")) 110 | return 0 111 | ;; 112 | --color) 113 | COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) 114 | return 0 115 | ;; 116 | --encoding) 117 | COMPREPLY=($(compgen -W "glyphs utf8 ascii" -- "${cur}")) 118 | return 0 119 | ;; 120 | --icon) 121 | COMPREPLY=($(compgen -W "auto always never" -- "${cur}")) 122 | return 0 123 | ;; 124 | --from-json) 125 | COMPREPLY=($(compgen -f "${cur}")) 126 | return 0 127 | ;; 128 | --config) 129 | COMPREPLY=($(compgen -f "${cur}")) 130 | return 0 131 | ;; 132 | -c) 133 | COMPREPLY=($(compgen -f "${cur}")) 134 | return 0 135 | ;; 136 | --mask-serials) 137 | COMPREPLY=($(compgen -W "hide scramble replace" -- "${cur}")) 138 | return 0 139 | ;; 140 | *) 141 | COMPREPLY=() 142 | ;; 143 | esac 144 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 145 | return 0 146 | ;; 147 | cyme__help) 148 | opts="watch help" 149 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 150 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 151 | return 0 152 | fi 153 | case "${prev}" in 154 | *) 155 | COMPREPLY=() 156 | ;; 157 | esac 158 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 159 | return 0 160 | ;; 161 | cyme__help__help) 162 | opts="" 163 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then 164 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 165 | return 0 166 | fi 167 | case "${prev}" in 168 | *) 169 | COMPREPLY=() 170 | ;; 171 | esac 172 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 173 | return 0 174 | ;; 175 | cyme__help__watch) 176 | opts="" 177 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then 178 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 179 | return 0 180 | fi 181 | case "${prev}" in 182 | *) 183 | COMPREPLY=() 184 | ;; 185 | esac 186 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 187 | return 0 188 | ;; 189 | cyme__watch) 190 | opts="-h --help" 191 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then 192 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 193 | return 0 194 | fi 195 | case "${prev}" in 196 | *) 197 | COMPREPLY=() 198 | ;; 199 | esac 200 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 201 | return 0 202 | ;; 203 | esac 204 | } 205 | 206 | if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then 207 | complete -F _cyme -o nosort -o bashdefault -o default cyme 208 | else 209 | complete -F _cyme -o bashdefault -o default cyme 210 | fi 211 | -------------------------------------------------------------------------------- /doc/cyme.fish: -------------------------------------------------------------------------------- 1 | # Print an optspec for argparse to handle cmd's options that are independent of any subcommand. 2 | function __fish_cyme_global_optspecs 3 | string join \n l/lsusb t/tree d/vidpid= s/show= D/device= filter-name= filter-serial= filter-class= v/verbose b/blocks= bus-blocks= config-blocks= interface-blocks= endpoint-blocks= m/more sort-devices= sort-buses group-devices= hide-buses hide-hubs list-root-hubs decimal no-padding color= no-color encoding= ascii no-icons icon= headings json from-json= F/force-libusb c/config= z/debug mask-serials= gen system-profiler h/help V/version 4 | end 5 | 6 | function __fish_cyme_needs_command 7 | # Figure out if the current invocation already has a command. 8 | set -l cmd (commandline -opc) 9 | set -e cmd[1] 10 | argparse -s (__fish_cyme_global_optspecs) -- $cmd 2>/dev/null 11 | or return 12 | if set -q argv[1] 13 | # Also print the command, so this can be used to figure out what it is. 14 | echo $argv[1] 15 | return 1 16 | end 17 | return 0 18 | end 19 | 20 | function __fish_cyme_using_subcommand 21 | set -l cmd (__fish_cyme_needs_command) 22 | test -z "$cmd" 23 | and return 1 24 | contains -- $cmd[1] $argv 25 | end 26 | 27 | complete -c cyme -n "__fish_cyme_needs_command" -s d -l vidpid -d 'Show only devices with the specified vendor and product ID numbers (in hexadecimal) in format VID:[PID]' -r 28 | complete -c cyme -n "__fish_cyme_needs_command" -s s -l show -d 'Show only devices with specified device and/or bus numbers (in decimal) in format [[bus]:][devnum]' -r 29 | complete -c cyme -n "__fish_cyme_needs_command" -s D -l device -d 'Selects which device lsusb will examine - supplied as Linux /dev/bus/usb/BBB/DDD style path' -r 30 | complete -c cyme -n "__fish_cyme_needs_command" -l filter-name -d 'Filter on string contained in name' -r 31 | complete -c cyme -n "__fish_cyme_needs_command" -l filter-serial -d 'Filter on string contained in serial' -r 32 | complete -c cyme -n "__fish_cyme_needs_command" -l filter-class -d 'Filter on USB class code' -r -f -a "use-interface-descriptor\t'Device class is unspecified, interface descriptors are used to determine needed drivers' 33 | audio\t'Speaker, microphone, sound card, MIDI' 34 | cdc-communications\t'The modern serial interface; appears as a UART/RS232 port on most systems' 35 | hid\t'Human Interface Device; game controllers, keyboards, mice etc. Also commonly used as a device data interface rather then creating something from scratch' 36 | physical\t'Force feedback joystick' 37 | image\t'Still imaging device; scanners, cameras' 38 | printer\t'Laser printer, inkjet printer, CNC machine' 39 | mass-storage\t'Mass storage devices (MSD): USB flash drive, memory card reader, digital audio player, digital camera, external drive' 40 | hub\t'High speed USB hub' 41 | cdc-data\t'Used together with class 02h (Communications and CDC Control) above' 42 | smart-card\t'USB smart card reader' 43 | content-security\t'Fingerprint reader' 44 | video\t'Webcam' 45 | personal-healthcare\t'Pulse monitor (watch)' 46 | audio-video\t'Webcam, TV' 47 | billboard\t'Describes USB-C alternate modes supported by device' 48 | usb-type-c-bridge\t'An interface to expose and configure the USB Type-C capabilities of Connectors on USB Hubs or Alternate Mode Adapters' 49 | bdp\t'This base class is defined for devices that conform to the “VESA USB BDP Device Specification” found at the VESA website. This specification defines the usable set of SubClass and Protocol values. Values outside of this defined spec are reserved. These class codes can only be used in Interface Descriptors' 50 | mctp\t'This base class is defined for devices that conform to the “MCTP over USB” found at the DMTF website as DSP0283. This specification defines the usable set of SubClass and Protocol values. Values outside of this defined spec are reserved. These class codes can only be used in Interface Descriptors' 51 | i3c-device\t'An interface to expose and configure I3C function within a USB device to allow interaction between host software and the I3C device, to drive transaction on the I3C bus to/from target devices' 52 | diagnostic\t'Trace and debugging equipment' 53 | wireless-controller\t'Wireless controllers: Bluetooth adaptors, Microsoft RNDIS' 54 | miscellaneous\t'This base class is defined for miscellaneous device definitions. Some matching SubClass and Protocols are defined on the USB-IF website' 55 | application-specific-interface\t'This base class is defined for devices that conform to several class specifications found on the USB-IF website' 56 | vendor-specific-class\t'This base class is defined for vendors to use as they please'" 57 | complete -c cyme -n "__fish_cyme_needs_command" -s b -l blocks -d 'Specify the blocks which will be displayed for each device and in what order. Supply arg multiple times to specify multiple blocks' -r -f -a "bus-number\t'Number of bus device is attached' 58 | device-number\t'Bus issued device number' 59 | branch-position\t'Position of device in parent branch' 60 | port-path\t'Linux style port path' 61 | sys-path\t'Linux udev reported syspath' 62 | driver\t'Linux udev reported driver loaded for device' 63 | icon\t'Icon based on VID/PID' 64 | vendor-id\t'Unique vendor identifier - purchased from USB IF' 65 | product-id\t'Vendor unique product identifier' 66 | name\t'The device name as reported in descriptor or using usb_ids if None' 67 | manufacturer\t'The device manufacturer as provided in descriptor or using usb_ids if None' 68 | product-name\t'The device product name as reported by usb_ids vidpid lookup' 69 | vendor-name\t'The device vendor name as reported by usb_ids vid lookup' 70 | serial\t'Device serial string as reported by descriptor' 71 | speed\t'Advertised device capable speed' 72 | negotiated-speed\t'Negotiated device speed as connected' 73 | tree-positions\t'Position along all branches back to trunk device' 74 | bus-power\t'macOS system_profiler only - actually bus current in mA not power!' 75 | bus-power-used\t'macOS system_profiler only - actually bus current used in mA not power!' 76 | extra-current-used\t'macOS system_profiler only - actually bus current used in mA not power!' 77 | bcd-device\t'The device version' 78 | bcd-usb\t'The supported USB version' 79 | base-class\t'Base class enum of interface provided by USB IF - only available when using libusb' 80 | sub-class\t'Sub-class value of interface provided by USB IF - only available when using libusb' 81 | protocol\t'Prototol value for interface provided by USB IF - only available when using libusb' 82 | uid-class\t'Class name from USB IDs repository' 83 | uid-sub-class\t'Sub-class name from USB IDs repository' 84 | uid-protocol\t'Protocol name from USB IDs repository' 85 | class\t'Fully defined USB Class Code enum based on BaseClass/SubClass/Protocol triplet' 86 | base-value\t'Base class as number value rather than enum' 87 | last-event\t'Last time device was seen' 88 | event-icon\t'Event icon'" 89 | complete -c cyme -n "__fish_cyme_needs_command" -l bus-blocks -d 'Specify the blocks which will be displayed for each bus and in what order. Supply arg multiple times to specify multiple blocks' -r -f -a "bus-number\t'System bus number identifier' 90 | icon\t'Icon based on VID/PID' 91 | name\t'System internal bus name based on Root Hub device name' 92 | host-controller\t'System internal bus provider name' 93 | host-controller-vendor\t'Vendor name of PCI Host Controller from pci.ids' 94 | host-controller-device\t'Device name of PCI Host Controller from pci.ids' 95 | pci-vendor\t'PCI vendor ID (VID)' 96 | pci-device\t'PCI device ID (PID)' 97 | pci-revision\t'PCI Revsision ID' 98 | port-path\t'syspath style port path to bus, applicable to Linux only'" 99 | complete -c cyme -n "__fish_cyme_needs_command" -l config-blocks -d 'Specify the blocks which will be displayed for each configuration and in what order. Supply arg multiple times to specify multiple blocks' -r -f -a "name\t'Name from string descriptor' 100 | number\t'Number of config, bConfigurationValue; value to set to enable to configuration' 101 | num-interfaces\t'Interfaces available for this configuruation' 102 | attributes\t'Attributes of configuration, bmAttributes' 103 | icon-attributes\t'Icon representation of bmAttributes' 104 | max-power\t'Maximum current consumption in mA'" 105 | complete -c cyme -n "__fish_cyme_needs_command" -l interface-blocks -d 'Specify the blocks which will be displayed for each interface and in what order. Supply arg multiple times to specify multiple blocks' -r -f -a "name\t'Name from string descriptor' 106 | number\t'Interface number' 107 | port-path\t'Interface port path, applicable to Linux' 108 | base-class\t'Base class enum of interface provided by USB IF' 109 | sub-class\t'Sub-class value of interface provided by USB IF' 110 | protocol\t'Prototol value for interface provided by USB IF' 111 | alt-setting\t'Interfaces can have the same number but an alternate settings defined here' 112 | driver\t'Driver obtained from udev on Linux only' 113 | sys-path\t'syspath obtained from udev on Linux only' 114 | num-endpoints\t'An interface can have many endpoints' 115 | icon\t'Icon based on BaseClass/SubCode/Protocol' 116 | uid-class\t'Class name from USB IDs repository' 117 | uid-sub-class\t'Sub-class name from USB IDs repository' 118 | uid-protocol\t'Protocol name from USB IDs repository' 119 | class\t'Fully defined USB Class Code based on BaseClass/SubClass/Protocol triplet' 120 | base-value\t'Base class as number value rather than enum'" 121 | complete -c cyme -n "__fish_cyme_needs_command" -l endpoint-blocks -d 'Specify the blocks which will be displayed for each endpoint and in what order. Supply arg multiple times to specify multiple blocks' -r -f -a "number\t'Endpoint number on interface' 122 | direction\t'Direction of data into endpoint' 123 | transfer-type\t'Type of data transfer endpoint accepts' 124 | sync-type\t'Synchronisation type (Iso mode)' 125 | usage-type\t'Usage type (Iso mode)' 126 | max-packet-size\t'Maximum packet size in bytes endpoint can send/recieve' 127 | interval\t'Interval for polling endpoint data transfers. Value in frame counts. Ignored for Bulk & Control Endpoints. Isochronous must equal 1 and field may range from 1 to 255 for interrupt endpoints'" 128 | complete -c cyme -n "__fish_cyme_needs_command" -l sort-devices -d 'Sort devices operation' -r -f -a "device-number\t'Sort by bus device number' 129 | branch-position\t'Sort by position in parent branch' 130 | no-sort\t'No sorting; whatever order it was parsed'" 131 | complete -c cyme -n "__fish_cyme_needs_command" -l group-devices -d 'Group devices by value when listing' -r -f -a "no-group\t'No grouping' 132 | bus\t'Group into buses with bus info as heading - like a flat tree'" 133 | complete -c cyme -n "__fish_cyme_needs_command" -l color -d 'Output coloring mode' -r -f -a "auto\t'Show colours if the output goes to an interactive console' 134 | always\t'Always apply colouring to the output' 135 | never\t'Never apply colouring to the output'" 136 | complete -c cyme -n "__fish_cyme_needs_command" -l encoding -d 'Output character encoding' -r -f -a "glyphs\t'Use UTF-8 private use area characters such as those used by NerdFont to show glyph icons' 137 | utf8\t'Use only standard UTF-8 characters for the output; no private use area glyph icons' 138 | ascii\t'Use only ASCII characters for the output; 0x00 - 0x7F (127 chars)'" 139 | complete -c cyme -n "__fish_cyme_needs_command" -l icon -d 'When to print icon blocks' -r -f -a "auto\t'Show icon blocks if the [`Encoding`] supports icons matched in the [`icon::IconTheme`]' 140 | always\t'Always print icon blocks if included in configured blocks' 141 | never\t'Never print icon blocks'" 142 | complete -c cyme -n "__fish_cyme_needs_command" -l from-json -d 'Read from json output rather than profiling system' -r -F 143 | complete -c cyme -n "__fish_cyme_needs_command" -s c -l config -d 'Path to user config file to use for custom icons, colours and default settings' -r -F 144 | complete -c cyme -n "__fish_cyme_needs_command" -l mask-serials -d 'Mask serial numbers with \'*\' or random chars' -r -f -a "hide\t'Hide with \'*\' char' 145 | scramble\t'Mask by randomising existing chars' 146 | replace\t'Mask by replacing length with random chars'" 147 | complete -c cyme -n "__fish_cyme_needs_command" -s l -l lsusb -d 'Attempt to maintain compatibility with lsusb output' 148 | complete -c cyme -n "__fish_cyme_needs_command" -s t -l tree -d 'Dump USB device hierarchy as a tree' 149 | complete -c cyme -n "__fish_cyme_needs_command" -s v -l verbose -d 'Verbosity level (repeat provides count): 1 prints device configurations; 2 prints interfaces; 3 prints interface endpoints; 4 prints everything and more blocks' 150 | complete -c cyme -n "__fish_cyme_needs_command" -s m -l more -d 'Print more blocks by default at each verbosity' 151 | complete -c cyme -n "__fish_cyme_needs_command" -l sort-buses -d 'Sort devices by bus number. If using any sort-devices other than no-sort, this happens automatically' 152 | complete -c cyme -n "__fish_cyme_needs_command" -l hide-buses -d 'Hide empty buses when printing tree; those with no devices' 153 | complete -c cyme -n "__fish_cyme_needs_command" -l hide-hubs -d 'Hide empty hubs when printing tree; those with no devices. When listing will hide hubs regardless of whether empty of not' 154 | complete -c cyme -n "__fish_cyme_needs_command" -l list-root-hubs -d 'Show root hubs when listing; Linux only' 155 | complete -c cyme -n "__fish_cyme_needs_command" -l decimal -d 'Show base16 values as base10 decimal instead' 156 | complete -c cyme -n "__fish_cyme_needs_command" -l no-padding -d 'Disable padding to align blocks - will cause --headings to become maligned' 157 | complete -c cyme -n "__fish_cyme_needs_command" -l no-color -d 'Disable coloured output, can also use NO_COLOR environment variable' 158 | complete -c cyme -n "__fish_cyme_needs_command" -l ascii -d 'Disables icons and utf-8 characters' 159 | complete -c cyme -n "__fish_cyme_needs_command" -l no-icons -d 'Disables all Block icons by not using any IconTheme. Providing custom XxxxBlocks without any icons is a nicer way to do this' 160 | complete -c cyme -n "__fish_cyme_needs_command" -l headings -d 'Show block headings' 161 | complete -c cyme -n "__fish_cyme_needs_command" -l json -d 'Output as json format after sorting, filters and tree settings are applied; without -tree will be flattened dump of devices' 162 | complete -c cyme -n "__fish_cyme_needs_command" -s F -l force-libusb -d 'Force pure libusb profiler on macOS rather than combining system_profiler output' 163 | complete -c cyme -n "__fish_cyme_needs_command" -s z -l debug -d 'Turn debugging information on. Alternatively can use RUST_LOG env: INFO, DEBUG, TRACE' 164 | complete -c cyme -n "__fish_cyme_needs_command" -l gen -d 'Generate cli completions and man page' 165 | complete -c cyme -n "__fish_cyme_needs_command" -l system-profiler -d 'Use the system_profiler command on macOS to get USB data' 166 | complete -c cyme -n "__fish_cyme_needs_command" -s h -l help -d 'Print help (see more with \'--help\')' 167 | complete -c cyme -n "__fish_cyme_needs_command" -s V -l version -d 'Print version' 168 | complete -c cyme -n "__fish_cyme_needs_command" -f -a "watch" -d 'Watch for USB devices being connected and disconnected' 169 | complete -c cyme -n "__fish_cyme_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' 170 | complete -c cyme -n "__fish_cyme_using_subcommand watch" -s h -l help -d 'Print help' 171 | complete -c cyme -n "__fish_cyme_using_subcommand help; and not __fish_seen_subcommand_from watch help" -f -a "watch" -d 'Watch for USB devices being connected and disconnected' 172 | complete -c cyme -n "__fish_cyme_using_subcommand help; and not __fish_seen_subcommand_from watch help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' 173 | -------------------------------------------------------------------------------- /doc/cyme_example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "icons": { 3 | "user": { 4 | "classifier#02": "", 5 | "classifier-sub-protocol#fe:01:01": "", 6 | "name#.*^[sS][dD]\\s[cC]ard\\s[rR]eader.*": "", 7 | "undefined-classifier": "☶", 8 | "unknown-vendor": "", 9 | "vid#05ac": "", 10 | "vid#2e8a": "", 11 | "vid-pid#1d50:6018": "", 12 | "vid-pid-msb#0483:37": "" 13 | }, 14 | "tree": { 15 | "endpoint_in": "→", 16 | "endpoint_out": "←", 17 | "tree-blank": " ", 18 | "tree-bus-start": "●", 19 | "tree-configuration-terminator": "•", 20 | "tree-corner": "└──", 21 | "tree-device-terminator": "○", 22 | "tree-disconnected-terminator": "✕", 23 | "tree-edge": "├──", 24 | "tree-interface-terminator": "◦", 25 | "tree-line": "│ " 26 | } 27 | }, 28 | "colours": { 29 | "name": "bright blue", 30 | "serial": "green", 31 | "manufacturer": "blue", 32 | "driver": "bright magenta", 33 | "string": "blue", 34 | "icon": null, 35 | "location": "magenta", 36 | "path": "bright cyan", 37 | "number": "cyan", 38 | "speed": "magenta", 39 | "vid": "bright yellow", 40 | "pid": "yellow", 41 | "class_code": "bright yellow", 42 | "sub_code": "yellow", 43 | "protocol": "yellow", 44 | "attributes": "magenta", 45 | "power": "red", 46 | "tree": "bright black", 47 | "tree_bus_start": "bright black", 48 | "tree_bus_terminator": "bright black", 49 | "tree_configuration_terminator": "bright black", 50 | "tree_interface_terminator": "bright black", 51 | "tree_endpoint_in": "yellow", 52 | "tree_endpoint_out": "magenta" 53 | }, 54 | "blocks": [ 55 | "bus-number", 56 | "device-number", 57 | "icon", 58 | "vendor-id", 59 | "product-id", 60 | "name", 61 | "serial", 62 | "driver", 63 | "speed" 64 | ], 65 | "bus-blocks": [ 66 | "port-path", 67 | "name", 68 | "host-controller", 69 | "host-controller-device" 70 | ], 71 | "config-blocks": [ 72 | "number", 73 | "icon-attributes", 74 | "max-power", 75 | "name" 76 | ], 77 | "interface-blocks": [ 78 | "port-path", 79 | "icon", 80 | "alt-setting", 81 | "base-class", 82 | "sub-class", 83 | "protocol", 84 | "name", 85 | "driver" 86 | ], 87 | "endpoint-blocks": [ 88 | "number", 89 | "direction", 90 | "transfer-type", 91 | "sync-type", 92 | "usage-type", 93 | "max-packet-size" 94 | ], 95 | "mask-serials": null, 96 | "max-variable-string-len": null, 97 | "no-auto-width": false, 98 | "lsusb": false, 99 | "tree": false, 100 | "verbose": 0, 101 | "more": false, 102 | "hide-buses": false, 103 | "hide-hubs": false, 104 | "list-root-hubs": false, 105 | "decimal": false, 106 | "no-padding": false, 107 | "no-color": false, 108 | "ascii": false, 109 | "no-icons": false, 110 | "headings": false, 111 | "force-libusb": false, 112 | "print-non-critical-profiler-stderr": false 113 | } -------------------------------------------------------------------------------- /examples/extra_data.rs: -------------------------------------------------------------------------------- 1 | use cyme::profiler; 2 | 3 | fn main() -> Result<(), String> { 4 | // get all system devices - this time with extra data which contain the Configuration, driver data (with udev) 5 | let sp_usb = profiler::get_spusb_with_extra() 6 | .map_err(|e| format!("Failed to gather system USB data from libusb, Error({})", e))?; 7 | 8 | let devices = sp_usb.flattened_devices(); 9 | 10 | // print all configurations 11 | for device in devices { 12 | if let Some(extra) = device.extra.as_ref() { 13 | println!("Device {} has configurations:", device.name); 14 | for c in extra.configurations.iter() { 15 | println!("{:?}", c); 16 | } 17 | }; 18 | } 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /examples/filter_devices.rs: -------------------------------------------------------------------------------- 1 | /// This example shows how to use the Filter to filter out devices that match a certain criteria 2 | /// 3 | /// See [`Filter`] docs for more information 4 | use cyme::profiler::{self, Filter}; 5 | use cyme::usb::BaseClass; 6 | 7 | fn main() -> Result<(), String> { 8 | // get all system devices 9 | let mut sp_usb = profiler::get_spusb() 10 | .map_err(|e| format!("Failed to gather system USB data from libusb, Error({})", e))?; 11 | 12 | // if one does want the tree, use the utility 13 | let filter = Filter { 14 | class: Some(BaseClass::Hid), 15 | ..Default::default() 16 | }; 17 | 18 | // will retain only the buses that have devices that match the filter - parent devices such as hubs with a HID device will be retained 19 | filter.retain_buses(&mut sp_usb.buses); 20 | sp_usb 21 | .buses 22 | .retain(|b| b.devices.as_ref().is_some_and(|d| d.is_empty())); 23 | 24 | // if one does not care about the tree, flatten the devices and do manually 25 | // let hid_devices = sp_usb.flatten_devices().iter().filter(|d| d.class == Some(BaseClass::HID)); 26 | 27 | if sp_usb.buses.is_empty() { 28 | println!("No HID devices found"); 29 | } else { 30 | println!("Found HID devices"); 31 | } 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/print_devices.rs: -------------------------------------------------------------------------------- 1 | use cyme::display; 2 | use cyme::profiler; 3 | 4 | fn main() -> Result<(), String> { 5 | // get all system devices - use get_spusb_with_extra for verbose info 6 | let sp_usb = profiler::get_spusb() 7 | .map_err(|e| format!("Failed to gather system USB data from libusb, Error({})", e))?; 8 | 9 | // flatten since we don't care tree/buses 10 | let devices = sp_usb.flattened_devices(); 11 | 12 | // print with default [`display::PrintSettings`] 13 | display::DisplayWriter::default() 14 | .print_flattened_devices(&devices, &display::PrintSettings::default()); 15 | 16 | // alternatively iterate over devices and do something with them 17 | for device in devices { 18 | if let (Some(0x05ac), Some(_)) = (device.vendor_id, device.product_id) { 19 | println!("Found Apple device: {}", device); 20 | } 21 | } 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/walk_sp_data.rs: -------------------------------------------------------------------------------- 1 | use cyme::profiler::{self, Device}; 2 | 3 | fn recursive_map_devices(device: &Device) { 4 | // the alternate format will print with colour 5 | println!("Device: {:#}", device); 6 | if let Some(v) = device.devices.as_ref() { 7 | for d in v { 8 | recursive_map_devices(d) 9 | } 10 | }; 11 | } 12 | 13 | fn main() -> Result<(), String> { 14 | // get all system devices 15 | let sp_usb = profiler::get_spusb() 16 | .map_err(|e| format!("Failed to gather system USB data from libusb, Error({})", e))?; 17 | 18 | // SPUSBDataType contains buses... 19 | for bus in sp_usb.buses { 20 | // which may contain devices... 21 | if let Some(devices) = bus.devices { 22 | // to walk all the devices, since each device can have devices attached, call a recursive function 23 | for device in devices { 24 | recursive_map_devices(&device); 25 | } 26 | } 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /scripts/release_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | VERSION="$(cargo metadata --no-deps --format-version=1 | jq -r '.packages[0].version')" 5 | DATE="$(date +%Y-%m-%d)" 6 | 7 | echo "Preparing release version $VERSION" 8 | 9 | # Check if the version is already in the changelog 10 | if git rev-parse -q --verify "v$VERSION" >/dev/null 2>&1; then 11 | echo "Error: git tag v$VERSION already exists! Aborting." 12 | exit 1 13 | fi 14 | 15 | # Change to version and date if Unreleased 16 | if ! grep -qE "^## \\[$VERSION\\]" CHANGELOG.md; then 17 | if grep -qE "^## \\[Unreleased\\]" CHANGELOG.md; then 18 | echo "Renaming [Unreleased] to [$VERSION] - $DATE" 19 | sed -i "s/^## \\[Unreleased\\]/## [$VERSION] - $DATE/" CHANGELOG.md 20 | else 21 | echo "Error: No '## [Unreleased]' or '## [$VERSION]' heading found in CHANGELOG.md." 22 | exit 1 23 | fi 24 | fi 25 | 26 | # Extract the changes text for this version 27 | CHANGELOG_CONTENT="$( 28 | awk "/^## \\[$VERSION\\]/ {found=1; next} /^## \\[/ {found=0} found" CHANGELOG.md 29 | )" 30 | 31 | if [ -z "$(echo "$CHANGELOG_CONTENT" | sed 's/^[[:space:]]*\$//')" ]; then 32 | echo "Error: No content found for version $VERSION in CHANGELOG.md!" 33 | exit 1 34 | fi 35 | 36 | echo "Changelog content for version $VERSION:" 37 | echo "$CHANGELOG_CONTENT" 38 | 39 | # Abort if dirty 40 | if ! git diff-index --quiet HEAD --; then 41 | echo "Error: Working directory is dirty! Please commit or stash your changes before proceeding." 42 | exit 1 43 | fi 44 | 45 | # Ensure on main 46 | if ! git rev-parse --abbrev-ref HEAD | grep -q "main"; then 47 | echo "Error: Not on main branch! Please switch to the main branch before proceeding." 48 | exit 1 49 | fi 50 | 51 | echo "Creating signed git tag v$VERSION" 52 | echo "$CHANGELOG_CONTENT" | git tag -a "v$VERSION" -F - 53 | 54 | echo "Tag v$VERSION created." 55 | 56 | # Check to continue 57 | read -r -p "Tag v$VERSION created locally. Push tag to origin? [y/N] " answer 58 | case "$answer" in 59 | [Yy]* ) 60 | echo "Pushing tag v$VERSION to origin..." 61 | # Ensure the tagged commit is pushed to the remote 62 | git push origin 63 | git push origin "v$VERSION" 64 | ;; 65 | * ) 66 | echo "Skipping tag push." 67 | exit 0 68 | ;; 69 | esac 70 | 71 | read -r -p "Create GitHub release with 'gh release create v$VERSION --notes-from-tag'? [y/N] " answer 72 | case "$answer" in 73 | [Yy]* ) 74 | echo "Creating GitHub release from tag..." 75 | gh release create "v$VERSION" --notes-from-tag --verify-tag --title "v$VERSION" 76 | ;; 77 | * ) 78 | echo "Skipping GitHub release creation." 79 | ;; 80 | esac 81 | -------------------------------------------------------------------------------- /src/colour.rs: -------------------------------------------------------------------------------- 1 | //! Colouring of cyme output 2 | use colored::*; 3 | use serde::ser::SerializeSeq; 4 | use serde::{Deserialize, Deserializer, Serialize}; 5 | use std::fmt; 6 | 7 | /// Colours [`crate::display::Block`] fields based on loose typing of field type 8 | /// 9 | /// Considered using HashMap with Colouring Enum like IconTheme but this seemed to suit better, it is less flexible though... 10 | #[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] 11 | #[serde(deny_unknown_fields)] 12 | pub struct ColourTheme { 13 | /// Colour to use for name from descriptor 14 | #[serde( 15 | default, 16 | serialize_with = "color_serializer", 17 | deserialize_with = "deserialize_option_color_from_string" 18 | )] 19 | pub name: Option, 20 | /// Colour to use for serial from descriptor 21 | #[serde( 22 | default, 23 | serialize_with = "color_serializer", 24 | deserialize_with = "deserialize_option_color_from_string" 25 | )] 26 | pub serial: Option, 27 | /// Colour to use for manufacturer from descriptor 28 | #[serde( 29 | default, 30 | serialize_with = "color_serializer", 31 | deserialize_with = "deserialize_option_color_from_string" 32 | )] 33 | pub manufacturer: Option, 34 | /// Colour to use for driver from udev 35 | #[serde( 36 | default, 37 | serialize_with = "color_serializer", 38 | deserialize_with = "deserialize_option_color_from_string" 39 | )] 40 | pub driver: Option, 41 | /// Colour to use for general String data 42 | #[serde( 43 | default, 44 | serialize_with = "color_serializer", 45 | deserialize_with = "deserialize_option_color_from_string" 46 | )] 47 | pub string: Option, 48 | /// Colour to use for icons 49 | #[serde( 50 | default, 51 | serialize_with = "color_serializer", 52 | deserialize_with = "deserialize_option_color_from_string" 53 | )] 54 | pub icon: Option, 55 | /// Colour to use for location data 56 | #[serde( 57 | default, 58 | serialize_with = "color_serializer", 59 | deserialize_with = "deserialize_option_color_from_string" 60 | )] 61 | pub location: Option, 62 | /// Colour to use for path data 63 | #[serde( 64 | default, 65 | serialize_with = "color_serializer", 66 | deserialize_with = "deserialize_option_color_from_string" 67 | )] 68 | pub path: Option, 69 | /// Colour to use for general number values 70 | #[serde( 71 | default, 72 | serialize_with = "color_serializer", 73 | deserialize_with = "deserialize_option_color_from_string" 74 | )] 75 | pub number: Option, 76 | /// Colour to use for speed 77 | #[serde( 78 | default, 79 | serialize_with = "color_serializer", 80 | deserialize_with = "deserialize_option_color_from_string" 81 | )] 82 | pub speed: Option, 83 | /// Colour to use for Vendor ID 84 | #[serde( 85 | default, 86 | serialize_with = "color_serializer", 87 | deserialize_with = "deserialize_option_color_from_string" 88 | )] 89 | pub vid: Option, 90 | /// Colour to use for Product ID 91 | #[serde( 92 | default, 93 | serialize_with = "color_serializer", 94 | deserialize_with = "deserialize_option_color_from_string" 95 | )] 96 | pub pid: Option, 97 | /// Colour to use for generic BaseClass 98 | #[serde( 99 | default, 100 | serialize_with = "color_serializer", 101 | deserialize_with = "deserialize_option_color_from_string" 102 | )] 103 | pub class_code: Option, 104 | /// Colour to use for SubCodes 105 | #[serde( 106 | default, 107 | serialize_with = "color_serializer", 108 | deserialize_with = "deserialize_option_color_from_string" 109 | )] 110 | pub sub_code: Option, 111 | /// Colour to use for protocol 112 | #[serde( 113 | default, 114 | serialize_with = "color_serializer", 115 | deserialize_with = "deserialize_option_color_from_string" 116 | )] 117 | pub protocol: Option, 118 | /// Colour to use for info/enum type 119 | #[serde( 120 | default, 121 | serialize_with = "color_serializer", 122 | deserialize_with = "deserialize_option_color_from_string" 123 | )] 124 | pub attributes: Option, 125 | /// Colour to use for power information 126 | #[serde( 127 | default, 128 | serialize_with = "color_serializer", 129 | deserialize_with = "deserialize_option_color_from_string" 130 | )] 131 | pub power: Option, 132 | /// Tree colour 133 | #[serde( 134 | default, 135 | serialize_with = "color_serializer", 136 | deserialize_with = "deserialize_option_color_from_string" 137 | )] 138 | pub tree: Option, 139 | /// Colour at prepended before printing `Bus` 140 | #[serde( 141 | default, 142 | serialize_with = "color_serializer", 143 | deserialize_with = "deserialize_option_color_from_string" 144 | )] 145 | pub tree_bus_start: Option, 146 | /// Colour printed at end of tree before printing `Device` 147 | #[serde( 148 | default, 149 | serialize_with = "color_serializer", 150 | deserialize_with = "deserialize_option_color_from_string" 151 | )] 152 | pub tree_bus_terminator: Option, 153 | /// Colour printed at end of tree before printing configuration 154 | #[serde( 155 | default, 156 | serialize_with = "color_serializer", 157 | deserialize_with = "deserialize_option_color_from_string" 158 | )] 159 | pub tree_configuration_terminator: Option, 160 | /// Colour printed at end of tree before printing interface 161 | #[serde( 162 | default, 163 | serialize_with = "color_serializer", 164 | deserialize_with = "deserialize_option_color_from_string" 165 | )] 166 | pub tree_interface_terminator: Option, 167 | /// Colour for endpoint in before print 168 | #[serde( 169 | default, 170 | serialize_with = "color_serializer", 171 | deserialize_with = "deserialize_option_color_from_string" 172 | )] 173 | pub tree_endpoint_in: Option, 174 | /// Colour for endpoint out before print 175 | #[serde( 176 | default, 177 | serialize_with = "color_serializer", 178 | deserialize_with = "deserialize_option_color_from_string" 179 | )] 180 | pub tree_endpoint_out: Option, 181 | } 182 | 183 | fn deserialize_option_color_from_string<'de, D>(deserializer: D) -> Result, D::Error> 184 | where 185 | D: Deserializer<'de>, 186 | { 187 | #[derive(Deserialize)] 188 | #[serde(untagged)] 189 | enum ColorOrNull<'a> { 190 | Str(&'a str), 191 | #[serde(deserialize_with = "deserialize_color")] 192 | FromStr(Color), 193 | Null, 194 | } 195 | 196 | match ColorOrNull::deserialize(deserializer)? { 197 | ColorOrNull::Str(s) => match s { 198 | "" => Ok(None), 199 | _ => Ok(Some(Color::from(s))), 200 | }, 201 | ColorOrNull::FromStr(i) => Ok(Some(i)), 202 | ColorOrNull::Null => Ok(None), 203 | } 204 | } 205 | 206 | // Custom color deserialize, adapted from: https://github.com/Peltoche/lsd/blob/master/src/theme/color.rs 207 | fn deserialize_color<'de, D>(deserializer: D) -> Result 208 | where 209 | D: serde::de::Deserializer<'de>, 210 | { 211 | struct ColorVisitor; 212 | impl<'de> serde::de::Visitor<'de> for ColorVisitor { 213 | type Value = Color; 214 | 215 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 216 | formatter.write_str("colour string or `3 u8 RGB array`") 217 | } 218 | 219 | fn visit_str(self, value: &str) -> Result 220 | where 221 | E: serde::de::Error, 222 | { 223 | Ok(Color::from(value)) 224 | } 225 | 226 | fn visit_seq(self, mut seq: M) -> Result 227 | where 228 | M: serde::de::SeqAccess<'de>, 229 | { 230 | let mut values = Vec::new(); 231 | if let Some(size) = seq.size_hint() { 232 | if size != 3 { 233 | return Err(serde::de::Error::invalid_length( 234 | size, 235 | &"a list of size 3(RGB)", 236 | )); 237 | } 238 | } 239 | loop { 240 | match seq.next_element::() { 241 | Ok(Some(x)) => { 242 | values.push(x); 243 | } 244 | Ok(None) => break, 245 | Err(e) => { 246 | return Err(e); 247 | } 248 | } 249 | } 250 | // recheck as size_hint sometimes not working 251 | if values.len() != 3 { 252 | return Err(serde::de::Error::invalid_length( 253 | values.len(), 254 | &"A u8 list of size 3: [R, G, B]", 255 | )); 256 | } 257 | Ok(Color::TrueColor { 258 | r: values[0], 259 | g: values[1], 260 | b: values[2], 261 | }) 262 | } 263 | } 264 | 265 | deserializer.deserialize_any(ColorVisitor) 266 | } 267 | 268 | fn color_to_string(color: Color) -> String { 269 | match color { 270 | Color::Black => "black".into(), 271 | Color::Red => "red".into(), 272 | Color::Green => "green".into(), 273 | Color::Yellow => "yellow".into(), 274 | Color::Blue => "blue".into(), 275 | Color::Magenta => "magenta".into(), 276 | Color::Cyan => "cyan".into(), 277 | Color::White => "white".into(), 278 | Color::BrightBlack => "bright black".into(), 279 | Color::BrightRed => "bright red".into(), 280 | Color::BrightGreen => "bright green".into(), 281 | Color::BrightYellow => "bright yellow".into(), 282 | Color::BrightBlue => "bright blue".into(), 283 | Color::BrightMagenta => "bright magenta".into(), 284 | Color::BrightCyan => "bright cyan".into(), 285 | Color::BrightWhite => "bright white".into(), 286 | Color::TrueColor { r, g, b } => format!("[{}, {}, {}]", r, g, b), 287 | } 288 | } 289 | 290 | /// Have to make this because external crate does not impl Display 291 | fn color_serializer(color: &Option, s: S) -> Result 292 | where 293 | S: serde::ser::Serializer, 294 | { 295 | match color { 296 | Some(c) => match c { 297 | Color::TrueColor { r, g, b } => { 298 | let mut seq = s.serialize_seq(Some(3))?; 299 | seq.serialize_element(r)?; 300 | seq.serialize_element(g)?; 301 | seq.serialize_element(b)?; 302 | seq.end() 303 | } 304 | _ => s.serialize_str(&color_to_string(*c)), 305 | }, 306 | None => s.serialize_none(), 307 | } 308 | } 309 | 310 | impl Default for ColourTheme { 311 | fn default() -> Self { 312 | ColourTheme::new() 313 | } 314 | } 315 | 316 | impl ColourTheme { 317 | /// New theme with defaults 318 | pub fn new() -> Self { 319 | ColourTheme { 320 | name: Some(Color::BrightBlue), 321 | serial: Some(Color::Green), 322 | manufacturer: Some(Color::Blue), 323 | driver: Some(Color::BrightMagenta), 324 | string: Some(Color::Blue), 325 | icon: None, 326 | location: Some(Color::Magenta), 327 | path: Some(Color::BrightCyan), 328 | number: Some(Color::Cyan), 329 | speed: Some(Color::Magenta), 330 | vid: Some(Color::BrightYellow), 331 | pid: Some(Color::Yellow), 332 | class_code: Some(Color::BrightYellow), 333 | sub_code: Some(Color::Yellow), 334 | protocol: Some(Color::Yellow), 335 | attributes: Some(Color::Magenta), 336 | power: Some(Color::Red), 337 | tree: Some(Color::BrightBlack), 338 | tree_bus_start: Some(Color::BrightBlack), 339 | tree_bus_terminator: Some(Color::BrightBlack), 340 | tree_configuration_terminator: Some(Color::BrightBlack), 341 | tree_interface_terminator: Some(Color::BrightBlack), 342 | tree_endpoint_in: Some(Color::Yellow), 343 | tree_endpoint_out: Some(Color::Magenta), 344 | } 345 | } 346 | } 347 | 348 | #[cfg(test)] 349 | mod tests { 350 | use super::*; 351 | 352 | #[test] 353 | fn test_serialize_color_theme() { 354 | let ct: ColourTheme = ColourTheme::new(); 355 | println!("{}", serde_json::to_string_pretty(&ct).unwrap()); 356 | } 357 | 358 | #[test] 359 | fn test_deserialize_color_theme() { 360 | let ct: ColourTheme = serde_json::from_str(r#"{"name": "blue"}"#).unwrap(); 361 | assert_eq!(ct.name, Some(Color::Blue)); 362 | } 363 | 364 | #[test] 365 | fn test_serialize_deserialize_color_theme() { 366 | let ct: ColourTheme = ColourTheme::new(); 367 | let ser = serde_json::to_string_pretty(&ct).unwrap(); 368 | let ctrt: ColourTheme = serde_json::from_str(&ser).unwrap(); 369 | assert_eq!(ct, ctrt); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Config for cyme binary 2 | use serde::{Deserialize, Serialize}; 3 | use std::fs::File; 4 | use std::io::BufReader; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use crate::colour; 8 | use crate::display; 9 | use crate::display::Block; 10 | use crate::error::{Error, ErrorKind, Result}; 11 | use crate::icon; 12 | 13 | const CONF_DIR: &str = "cyme"; 14 | const CONF_NAME: &str = "cyme.json"; 15 | 16 | /// Allows user supplied icons to replace or add to `DEFAULT_ICONS` and `DEFAULT_TREE` 17 | #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] 18 | #[serde(rename_all = "kebab-case", deny_unknown_fields, default)] 19 | pub struct Config { 20 | #[serde(skip)] 21 | filepath: Option, 22 | /// User supplied [`crate::icon::IconTheme`] - will merge with default 23 | pub icons: icon::IconTheme, 24 | /// User supplied [`crate::colour::ColourTheme`] - overrides default 25 | pub colours: colour::ColourTheme, 26 | /// Default [`crate::display::DeviceBlocks`] to use for displaying devices 27 | pub blocks: Option>, 28 | /// Default [`crate::display::BusBlocks`] to use for displaying buses 29 | pub bus_blocks: Option>, 30 | /// Default [`crate::display::ConfigurationBlocks`] to use for device configurations 31 | pub config_blocks: Option>, 32 | /// Default [`crate::display::InterfaceBlocks`] to use for device interfaces 33 | pub interface_blocks: Option>, 34 | /// Default [`crate::display::EndpointBlocks`] to use for device endpoints 35 | pub endpoint_blocks: Option>, 36 | /// Whether to hide device serial numbers by default 37 | pub mask_serials: Option, 38 | /// Max variable string length to display before truncating - descriptors and classes for example 39 | pub max_variable_string_len: Option, 40 | /// Disable auto generation of max_variable_string_len based on terminal width 41 | pub no_auto_width: bool, 42 | // non-Options copied from Args 43 | /// Attempt to maintain compatibility with lsusb output 44 | pub lsusb: bool, 45 | /// Dump USB device hierarchy as a tree 46 | pub tree: bool, 47 | /// Verbosity level: 1 prints device configurations; 2 prints interfaces; 3 prints interface endpoints; 4 prints everything and all blocks 48 | pub verbose: u8, 49 | /// Print more blocks by default at each verbosity 50 | pub more: bool, 51 | /// Hide empty buses when printing tree; those with no devices. 52 | pub hide_buses: bool, 53 | /// Hide empty hubs when printing tree; those with no devices. When listing will hide hubs regardless of whether empty of not 54 | pub hide_hubs: bool, 55 | /// Show root hubs when listing; Linux only 56 | pub list_root_hubs: bool, 57 | /// Show base16 values as base10 decimal instead 58 | pub decimal: bool, 59 | /// Disable padding to align blocks 60 | pub no_padding: bool, 61 | /// Disable color 62 | pub no_color: bool, 63 | /// Disables icons and utf-8 characters 64 | pub ascii: bool, 65 | /// Disables all [`display::Block`] icons 66 | pub no_icons: bool, 67 | /// Show block headings 68 | pub headings: bool, 69 | /// Force nusb/libusb profiler on macOS rather than using/combining system_profiler output 70 | pub force_libusb: bool, 71 | /// Print non-critical errors (normally due to permissions) during USB profiler to stderr 72 | pub print_non_critical_profiler_stderr: bool, 73 | } 74 | 75 | impl Config { 76 | /// New based on defaults 77 | pub fn new() -> Self { 78 | Default::default() 79 | } 80 | 81 | /// From system config if exists else default 82 | #[cfg(not(debug_assertions))] 83 | pub fn sys() -> Result { 84 | if let Some(p) = Self::config_file_path() { 85 | let path = p.join(CONF_NAME); 86 | log::info!("Looking for system config {:?}", &path); 87 | return match Self::from_file(&path) { 88 | Ok(c) => { 89 | log::info!("Loaded system config {:?}", c); 90 | Ok(c) 91 | } 92 | Err(e) => { 93 | // if parsing error, print issue but use default 94 | // IO error (unable to read) will raise as error 95 | if e.kind() == ErrorKind::Parsing { 96 | log::warn!("{}", e); 97 | Err(e) 98 | } else { 99 | Ok(Self::new()) 100 | } 101 | } 102 | }; 103 | } else { 104 | Ok(Self::new()) 105 | } 106 | } 107 | 108 | /// Use default if running in debug since the integration tests use this 109 | #[cfg(debug_assertions)] 110 | pub fn sys() -> Result { 111 | log::warn!("Running in debug, not checking for system config"); 112 | Ok(Self::new()) 113 | } 114 | 115 | /// Get example [`Config`] 116 | pub fn example() -> Self { 117 | Config { 118 | icons: icon::example_theme(), 119 | blocks: Some(display::DeviceBlocks::example_blocks()), 120 | bus_blocks: Some(display::BusBlocks::example_blocks()), 121 | config_blocks: Some(display::ConfigurationBlocks::example_blocks()), 122 | interface_blocks: Some(display::InterfaceBlocks::example_blocks()), 123 | endpoint_blocks: Some(display::EndpointBlocks::example_blocks()), 124 | ..Default::default() 125 | } 126 | } 127 | 128 | /// Attempt to read from .json format confg at `file_path` 129 | pub fn from_file>(file_path: P) -> Result { 130 | let f = File::open(&file_path)?; 131 | let mut config: Self = serde_json::from_reader(BufReader::new(f)).map_err(|e| { 132 | Error::new( 133 | ErrorKind::Parsing, 134 | &format!( 135 | "Failed to parse config at {:?}; Error({})", 136 | file_path.as_ref(), 137 | e 138 | ), 139 | ) 140 | })?; 141 | // set the file path we loaded from for saving 142 | config.filepath = Some(file_path.as_ref().to_path_buf()); 143 | Ok(config) 144 | } 145 | 146 | /// This provides the path for a configuration file, specific to OS 147 | /// return None if error like PermissionDenied 148 | pub fn config_file_path() -> Option { 149 | dirs::config_dir().map(|x| x.join(CONF_DIR)) 150 | } 151 | 152 | /// Get the file path for the config 153 | pub fn filepath(&self) -> Option<&Path> { 154 | self.filepath.as_deref() 155 | } 156 | 157 | /// Save the current config to a file 158 | pub fn save_file>(&self, path: P) -> Result<()> { 159 | log::info!("Saving config to {:?}", path.as_ref().display()); 160 | // create parent folders 161 | if let Some(parent) = path.as_ref().parent() { 162 | log::debug!("Creating parent folders for {:?}", parent.display()); 163 | std::fs::create_dir_all(parent)?; 164 | } 165 | let f = File::create(&path)?; 166 | serde_json::to_writer_pretty(f, self).map_err(|e| { 167 | Error::new( 168 | ErrorKind::Io, 169 | &format!("Failed to save config: Error({})", e), 170 | ) 171 | }) 172 | } 173 | 174 | /// Save the current config to the file it was loaded from or default location if None 175 | pub fn save(&self) -> Result<()> { 176 | if let Some(p) = self.filepath() { 177 | self.save_file(p) 178 | } else if let Some(p) = Self::config_file_path() { 179 | self.save_file(p.join(CONF_NAME)) 180 | } else { 181 | Err(Error::new( 182 | ErrorKind::Io, 183 | "Unable to determine config file path", 184 | )) 185 | } 186 | } 187 | 188 | /// Merge the settings from a [`display::PrintSettings`] into the config 189 | /// 190 | /// Dynamic settings and those loaded from config such as [`icon::IconTheme`] and [`color::ColourTheme`] are not merged 191 | pub fn merge_print_settings(&mut self, settings: &display::PrintSettings) { 192 | self.blocks = settings.device_blocks.clone(); 193 | self.bus_blocks = settings.bus_blocks.clone(); 194 | self.config_blocks = settings.config_blocks.clone(); 195 | self.interface_blocks = settings.interface_blocks.clone(); 196 | self.endpoint_blocks = settings.endpoint_blocks.clone(); 197 | self.more = settings.more; 198 | self.decimal = settings.decimal; 199 | self.mask_serials = settings.mask_serials.clone(); 200 | self.no_padding = settings.no_padding; 201 | self.headings = settings.headings; 202 | self.tree = settings.tree; 203 | self.max_variable_string_len = settings.max_variable_string_len; 204 | self.no_auto_width = !settings.auto_width; 205 | self.no_icons = matches!(settings.icon_when, display::IconWhen::Never); 206 | self.verbose = settings.verbosity; 207 | } 208 | 209 | /// Returns a [`display::PrintSettings`] based on the config 210 | pub fn print_settings(&self) -> display::PrintSettings { 211 | let colours = if self.no_color { 212 | None 213 | } else { 214 | Some(self.colours.clone()) 215 | }; 216 | let icons = if self.no_icons { 217 | None 218 | } else { 219 | Some(self.icons.clone()) 220 | }; 221 | display::PrintSettings { 222 | device_blocks: self.blocks.clone(), 223 | bus_blocks: self.bus_blocks.clone(), 224 | config_blocks: self.config_blocks.clone(), 225 | interface_blocks: self.interface_blocks.clone(), 226 | endpoint_blocks: self.endpoint_blocks.clone(), 227 | more: self.more, 228 | decimal: self.decimal, 229 | mask_serials: self.mask_serials.clone(), 230 | no_padding: self.no_padding, 231 | headings: self.headings, 232 | tree: self.tree, 233 | max_variable_string_len: self.max_variable_string_len, 234 | auto_width: !self.no_auto_width, 235 | icon_when: if self.no_icons { 236 | display::IconWhen::Never 237 | } else { 238 | display::IconWhen::Auto 239 | }, 240 | icons, 241 | colours, 242 | verbosity: self.verbose, 243 | ..Default::default() 244 | } 245 | } 246 | } 247 | 248 | #[cfg(test)] 249 | mod tests { 250 | use super::*; 251 | 252 | #[test] 253 | #[cfg(feature = "regex_icon")] 254 | fn test_deserialize_example_file() { 255 | let path = PathBuf::from("./doc").join("cyme_example_config.json"); 256 | assert!(Config::from_file(path).is_ok()); 257 | } 258 | 259 | #[test] 260 | fn test_deserialize_config_no_theme() { 261 | let path = PathBuf::from("./tests/data").join("config_no_theme.json"); 262 | assert!(Config::from_file(path).is_ok()); 263 | } 264 | 265 | #[test] 266 | fn test_deserialize_config_missing_args() { 267 | let path = PathBuf::from("./tests/data").join("config_missing_args.json"); 268 | assert!(Config::from_file(path).is_ok()); 269 | } 270 | 271 | #[test] 272 | fn test_save_config() { 273 | // save to temp file 274 | let path = PathBuf::from("./tests/data").join("config_save.json"); 275 | let c = Config::new(); 276 | assert!(c.save_file(&path).is_ok()); 277 | assert!(Config::from_file(path).is_ok()); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error type used within crate with From for commonly used crate errors 2 | use std::error; 3 | use std::{fmt, io}; 4 | 5 | /// Result type used within crate 6 | pub type Result = std::result::Result; 7 | 8 | /// Contained with [`ErrorKind`] to provide more context 9 | #[derive(Debug, PartialEq, Clone)] 10 | pub struct ErrorArg 11 | where 12 | E: fmt::Debug, 13 | G: fmt::Debug, 14 | { 15 | expected: E, 16 | got: G, 17 | } 18 | 19 | impl fmt::Display for ErrorArg { 20 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 21 | write!(f, "Expected: {}, Got: {}", self.expected, self.got) 22 | } 23 | } 24 | 25 | impl ErrorArg 26 | where 27 | E: fmt::Debug, 28 | G: fmt::Debug, 29 | { 30 | /// New ErrorArg 31 | pub fn new(expected: E, got: G) -> ErrorArg { 32 | ErrorArg { expected, got } 33 | } 34 | 35 | /// The expected value 36 | pub fn expected(&self) -> &E { 37 | &self.expected 38 | } 39 | 40 | /// The actual value 41 | pub fn got(&self) -> &G { 42 | &self.got 43 | } 44 | } 45 | 46 | #[derive(Debug, PartialEq, Clone)] 47 | /// Kind of error produced 48 | pub enum ErrorKind { 49 | /// Error running macOS `system_profiler` command 50 | SystemProfiler, 51 | /// Unsupported system for command being run; system_profiler not on macOS for example, libusb feature not installed 52 | Unsupported, 53 | /// Unable to find USB device on bus 54 | NotFound, 55 | /// Unable to open device to query device descriptors - check permissions 56 | Opening, 57 | /// Error parsing a string into a value - used for u32 to json deserialization 58 | Parsing, 59 | /// Error decoding an encoded value into a type 60 | Decoding, 61 | /// Error parsing config file 62 | Config, 63 | /// [`std::io::Error`] probably not found when reading file to parse 64 | Io, 65 | /// libusb error 66 | LibUSB, 67 | /// nusb error 68 | Nusb, 69 | /// Stall in control request transfer 70 | TransferStall, 71 | /// Error calling udev 72 | Udev, 73 | /// Invalid arg for method or cli 74 | InvalidArg, 75 | /// Error calling IOKit 76 | IoKit, 77 | /// Error From other crate without enum variant 78 | Other(&'static str), 79 | /// Invalid device descriptor 80 | InvalidDescriptor, 81 | /// Invalid device descriptor length 82 | DescriptorLength(ErrorArg), 83 | /// Invalid device used in context 84 | InvalidDevice, 85 | /// Invalid USB path 86 | InvalidPath, 87 | } 88 | 89 | #[derive(Debug, PartialEq)] 90 | /// Cyme error which impl [`std::error`] 91 | pub struct Error { 92 | /// The [`ErrorKind`] 93 | pub kind: ErrorKind, 94 | /// String description 95 | pub message: String, 96 | } 97 | 98 | impl Error { 99 | /// New error helper 100 | pub fn new(kind: ErrorKind, message: &str) -> Error { 101 | Error { 102 | kind, 103 | message: message.to_string(), 104 | } 105 | } 106 | 107 | /// New error helper for descriptor length 108 | pub fn new_descriptor_len(name: &str, expected: usize, got: usize) -> Error { 109 | let error_arg = ErrorArg::new(expected, got); 110 | Error { 111 | kind: ErrorKind::DescriptorLength(error_arg), 112 | message: format!( 113 | "Invalid descriptor length for {}. Expected: {}, Got {}", 114 | name, expected, got 115 | ), 116 | } 117 | } 118 | 119 | /// The [`ErrorKind`] 120 | pub fn kind(&self) -> ErrorKind { 121 | self.kind.to_owned() 122 | } 123 | 124 | /// The description 125 | pub fn message(&self) -> &String { 126 | &self.message 127 | } 128 | } 129 | 130 | impl error::Error for Error {} 131 | 132 | impl fmt::Display for Error { 133 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 134 | if f.alternate() { 135 | write!(f, "{}", self.message) 136 | } else { 137 | write!(f, "{:?} Error: {}", self.kind, self.message) 138 | } 139 | } 140 | } 141 | 142 | impl From for Error { 143 | fn from(error: io::Error) -> Self { 144 | Error { 145 | kind: ErrorKind::Io, 146 | message: error.to_string(), 147 | } 148 | } 149 | } 150 | 151 | impl From for Error { 152 | fn from(error: serde_json::Error) -> Self { 153 | Error { 154 | kind: ErrorKind::Parsing, 155 | message: error.to_string(), 156 | } 157 | } 158 | } 159 | 160 | impl From for Error { 161 | fn from(error: std::string::FromUtf8Error) -> Self { 162 | Error { 163 | kind: ErrorKind::Other("FromUtf8Error"), 164 | message: error.to_string(), 165 | } 166 | } 167 | } 168 | 169 | impl From for io::Error { 170 | fn from(val: Error) -> Self { 171 | io::Error::other(val.message) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! List system USB buses and devices; a modern `lsusb` that attempts to maintain compatibility with, but also add new features. 2 | //! 3 | //! # Examples 4 | //! 5 | //! Profile USB devices on cross-platform systems: 6 | //! 7 | //! ```no_run 8 | //! use cyme::profiler; 9 | //! let sp_usb = profiler::get_spusb().unwrap(); 10 | //! ``` 11 | //! 12 | //! Profile USB devices with all extra descriptor data (requires opening devices) on cross-platform systems: 13 | //! 14 | //! ```no_run 15 | //! use cyme::profiler; 16 | //! let sp_usb = profiler::get_spusb_with_extra().unwrap(); 17 | //! ``` 18 | //! 19 | //! It's often useful to then flatten this into a list of devices ([`profiler::Device`]): 20 | //! 21 | //! ```no_run 22 | //! # use cyme::profiler; 23 | //! # let sp_usb = profiler::get_spusb().unwrap(); 24 | //! // flatten since we don't care tree/buses 25 | //! let devices = sp_usb.flattened_devices(); 26 | //! 27 | //! for device in devices { 28 | //! format!("{}", device); 29 | //! } 30 | //! ``` 31 | //! 32 | //! One can then print with the cyme display module: 33 | //! 34 | //! ```no_run 35 | //! # use cyme::profiler; 36 | //! # let sp_usb = profiler::get_spusb().unwrap(); 37 | //! # let devices = sp_usb.flattened_devices(); 38 | //! use cyme::display; 39 | //! // print with default [`display::PrintSettings`] 40 | //! display::DisplayWriter::default().print_flattened_devices(&devices, &display::PrintSettings::default()); 41 | //! ``` 42 | //! 43 | //! The [`profiler::SystemProfile`] struct contains system [`profiler::Bus`]s, which contain [`profiler::Device`]s as a USB tree. 44 | #![allow(dead_code)] 45 | #![warn(missing_docs)] 46 | 47 | pub mod colour; 48 | pub mod config; 49 | pub mod display; 50 | pub mod error; 51 | pub mod icon; 52 | pub mod lsusb; 53 | pub mod profiler; 54 | pub mod types; 55 | #[cfg(all(target_os = "linux", feature = "udev"))] 56 | pub mod udev; 57 | #[cfg(all(all(target_os = "linux", feature = "udevlib"), not(feature = "udev")))] 58 | #[path = "udev_ffi.rs"] 59 | pub mod udev; 60 | pub mod usb; 61 | 62 | // run any Rust code as doctest 63 | #[doc = include_str!("../README.md")] 64 | #[cfg(doctest)] 65 | pub struct ReadmeDoctests; 66 | -------------------------------------------------------------------------------- /src/lsusb/names.rs: -------------------------------------------------------------------------------- 1 | //! Port of names.c in usbutils that provides name lookups for USB data using udev, falling back to USB IDs repository. 2 | //! 3 | //! lsusb uses udev and the bundled hwdb (based on USB IDs) for name lookups. To attempt parity with lsusb, this module uses udev_hwdb if the feature is enabled, otherwise it will fall back to the USB IDs repository. Whilst they both get data from the same source, the bundled udev hwdb might be different due to release version/customisations. 4 | //! 5 | //! The function names match those found in the lsusb source code. 6 | #[allow(unused_imports)] 7 | use crate::error::{Error, ErrorKind}; 8 | use usb_ids::{self, FromId}; 9 | 10 | /// Get name of vendor from [`usb_ids::Vendor`] or `hwdb_get` if feature is enabled 11 | /// 12 | /// ``` 13 | /// use cyme::lsusb::names; 14 | /// assert_eq!(names::vendor(0x1d6b), Some("Linux Foundation".to_owned())); 15 | /// ``` 16 | pub fn vendor(vid: u16) -> Option { 17 | hwdb_get(&format!("usb:v{:04X}*", vid), "ID_VENDOR_FROM_DATABASE") 18 | .unwrap_or_else(|_| usb_ids::Vendor::from_id(vid).map(|v| v.name().to_owned())) 19 | } 20 | 21 | /// Get name of product from [`usb_ids::Device`] or `hwdb_get` if feature is enabled 22 | /// 23 | /// ``` 24 | /// use cyme::lsusb::names; 25 | /// assert_eq!(names::product(0x1d6b, 0x0003), Some("3.0 root hub".to_owned())); 26 | /// ``` 27 | pub fn product(vid: u16, pid: u16) -> Option { 28 | hwdb_get( 29 | &format!("usb:v{:04X}p{:04X}*", vid, pid), 30 | "ID_MODEL_FROM_DATABASE", 31 | ) 32 | .unwrap_or_else(|_| usb_ids::Device::from_vid_pid(vid, pid).map(|v| v.name().to_owned())) 33 | } 34 | 35 | /// Get name of class from [`usb_ids::Class`] or `hwdb_get` if feature is enabled 36 | /// 37 | /// ``` 38 | /// use cyme::lsusb::names; 39 | /// assert_eq!(names::class(0x03), Some("Human Interface Device".to_owned())); 40 | /// ``` 41 | pub fn class(id: u8) -> Option { 42 | hwdb_get( 43 | &format!("usb:v*p*d*dc{:02X}*", id), 44 | "ID_USB_CLASS_FROM_DATABASE", 45 | ) 46 | .unwrap_or_else(|_| usb_ids::Class::from_id(id).map(|v| v.name().to_owned())) 47 | } 48 | 49 | /// Get name of sub class from [`usb_ids::SubClass`] or `hwdb_get` if feature is enabled 50 | /// 51 | /// ``` 52 | /// use cyme::lsusb::names; 53 | /// assert_eq!(names::subclass(0x02, 0x02), Some("Abstract (modem)".to_owned())); 54 | /// ``` 55 | pub fn subclass(cid: u8, scid: u8) -> Option { 56 | hwdb_get( 57 | &format!("usb:v*p*d*dc{:02X}dsc{:02X}*", cid, scid), 58 | "ID_USB_SUBCLASS_FROM_DATABASE", 59 | ) 60 | .unwrap_or_else(|_| usb_ids::SubClass::from_cid_scid(cid, scid).map(|v| v.name().to_owned())) 61 | } 62 | 63 | /// Get name of protocol from [`usb_ids::Protocol`] or `hwdb_get` if feature is enabled 64 | /// 65 | /// ``` 66 | /// use cyme::lsusb::names; 67 | /// assert_eq!(names::protocol(0x02, 0x02, 0x05), Some("AT-commands (3G)".to_owned())); 68 | /// ``` 69 | pub fn protocol(cid: u8, scid: u8, pid: u8) -> Option { 70 | hwdb_get( 71 | &format!("usb:v*p*d*dc{:02X}dsc{:02X}dp{:02X}*", cid, scid, pid), 72 | "ID_USB_PROTOCOL_FROM_DATABASE", 73 | ) 74 | .unwrap_or_else(|_| { 75 | usb_ids::Protocol::from_cid_scid_pid(cid, scid, pid).map(|v| v.name().to_owned()) 76 | }) 77 | } 78 | 79 | /// Get HID descriptor type name from [`usb_ids::Hid`] 80 | pub fn hid(id: u8) -> Option { 81 | usb_ids::Hid::from_id(id).map(|v| v.name().to_owned()) 82 | } 83 | 84 | /// Get HID report tag name from [`usb_ids::HidItemType`] 85 | pub fn report_tag(id: u8) -> Option { 86 | usb_ids::HidItemType::from_id(id).map(|v| v.name().to_owned()) 87 | } 88 | 89 | /// Get name of [`usb_ids::HidUsagePage`] from id 90 | pub fn huts(id: u8) -> Option { 91 | usb_ids::HidUsagePage::from_id(id).map(|v| v.name().to_owned()) 92 | } 93 | 94 | /// Get name of [`usb_ids::HidUsage`] from page id and usage id 95 | pub fn hutus(page_id: u8, id: u16) -> Option { 96 | usb_ids::HidUsage::from_pageid_uid(page_id, id).map(|v| v.name().to_owned()) 97 | } 98 | 99 | /// Get name of [`usb_ids::Language`] from id 100 | pub fn langid(id: u16) -> Option { 101 | usb_ids::Language::from_id(id).map(|v| v.name().to_owned()) 102 | } 103 | 104 | /// Get name of [`usb_ids::Phy`] from id 105 | pub fn physdes(id: u8) -> Option { 106 | usb_ids::Phy::from_id(id).map(|v| v.name().to_owned()) 107 | } 108 | 109 | /// Get name of [`usb_ids::Bias`] from id 110 | pub fn bias(id: u8) -> Option { 111 | usb_ids::Bias::from_id(id).map(|v| v.name().to_owned()) 112 | } 113 | 114 | /// Get name of [`usb_ids::HidCountryCode`] from id 115 | pub fn countrycode(id: u8) -> Option { 116 | usb_ids::HidCountryCode::from_id(id).map(|v| v.name().to_owned()) 117 | } 118 | 119 | /// Get name of [`usb_ids::VideoTerminal`] from id 120 | pub fn videoterminal(id: u16) -> Option { 121 | usb_ids::VideoTerminal::from_id(id).map(|v| v.name().to_owned()) 122 | } 123 | 124 | /// Wrapper around [`crate::udev::hwdb_get`] so that it can be 'used' without feature 125 | /// 126 | /// Returns `Err` not `None` if feature is not enabled so that with unwrap_or hwdb can still return `None` if no match in db 127 | #[allow(unused_variables)] 128 | fn hwdb_get(modalias: &str, key: &'static str) -> Result, Error> { 129 | #[cfg(all(target_os = "linux", feature = "udev_hwdb"))] 130 | return crate::udev::hwdb::get(modalias, key); 131 | 132 | #[cfg(not(all(target_os = "linux", feature = "udev_hwdb")))] 133 | return Err(Error::new( 134 | ErrorKind::Unsupported, 135 | "hwdb_get requires 'udev_hwdb' feature", 136 | )); 137 | } 138 | -------------------------------------------------------------------------------- /src/profiler/macos.rs: -------------------------------------------------------------------------------- 1 | //! macOS specific code for USB device profiling. 2 | //! 3 | //! Includes parser for macOS `system_profiler` command -json output with SPUSBDataType. Merged with libusb or nusb for extra data. Also includes IOKit functions for obtaining host controller data - helper code taken from [nusb](https://github.com/kevinmehall/nusb). 4 | //! 5 | //! `system_profiler`: Bus and Device structs are used as deserializers for serde. The JSON output with the -json flag is not really JSON; all values are String regardless of contained data so it requires some extra work. Additionally, some values differ slightly from the non json output such as the speed - it is a description rather than numerical. 6 | use super::*; 7 | use std::process::Command; 8 | 9 | use core_foundation::{ 10 | base::{CFType, TCFType}, 11 | data::CFData, 12 | string::CFString, 13 | ConcreteCFType, 14 | }; 15 | use io_kit_sys::{ 16 | kIOMasterPortDefault, kIORegistryIterateParents, kIORegistryIterateRecursively, 17 | keys::kIOServicePlane, ret::kIOReturnSuccess, IOIteratorNext, IOObjectRelease, 18 | IORegistryEntryGetRegistryEntryID, IORegistryEntrySearchCFProperty, 19 | IOServiceGetMatchingServices, IOServiceNameMatching, 20 | }; 21 | 22 | pub(crate) struct IoObject(u32); 23 | 24 | impl IoObject { 25 | // Safety: `handle` must be an IOObject handle. Ownership is transferred. 26 | pub unsafe fn new(handle: u32) -> IoObject { 27 | IoObject(handle) 28 | } 29 | pub fn get(&self) -> u32 { 30 | self.0 31 | } 32 | } 33 | 34 | impl Drop for IoObject { 35 | fn drop(&mut self) { 36 | unsafe { 37 | IOObjectRelease(self.0); 38 | } 39 | } 40 | } 41 | 42 | pub(crate) struct IoService(IoObject); 43 | 44 | impl IoService { 45 | // Safety: `handle` must be an IOService handle. Ownership is transferred. 46 | pub unsafe fn new(handle: u32) -> IoService { 47 | IoService(IoObject(handle)) 48 | } 49 | pub fn get(&self) -> u32 { 50 | self.0 .0 51 | } 52 | } 53 | 54 | pub(crate) struct IoServiceIterator(IoObject); 55 | 56 | impl IoServiceIterator { 57 | // Safety: `handle` must be an IoIterator of IoService. Ownership is transferred. 58 | pub unsafe fn new(handle: u32) -> IoServiceIterator { 59 | IoServiceIterator(IoObject::new(handle)) 60 | } 61 | } 62 | 63 | impl Iterator for IoServiceIterator { 64 | type Item = IoService; 65 | 66 | fn next(&mut self) -> Option { 67 | unsafe { 68 | let handle = IOIteratorNext(self.0.get()); 69 | if handle != 0 { 70 | Some(IoService::new(handle)) 71 | } else { 72 | None 73 | } 74 | } 75 | } 76 | } 77 | 78 | pub(crate) struct HostControllerInfo { 79 | pub(crate) name: String, 80 | pub(crate) class_name: String, 81 | pub(crate) io_name: String, 82 | pub(crate) registry_id: u64, 83 | pub(crate) vendor_id: u16, 84 | pub(crate) device_id: u16, 85 | pub(crate) revision_id: u16, 86 | pub(crate) class_code: u32, 87 | pub(crate) subsystem_vendor_id: Option, 88 | pub(crate) subsystem_id: Option, 89 | } 90 | 91 | impl std::fmt::Debug for HostControllerInfo { 92 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 93 | f.debug_struct("PciControllerInfo") 94 | .field("name", &self.name) 95 | .field("class_name", &self.class_name) 96 | .field("io_name", &self.io_name) 97 | .field("registry_id", &format!("{:08x}", self.registry_id)) 98 | .field("vendor_id", &format!("{:04x}", self.vendor_id)) 99 | .field("device_id", &format!("{:04x}", self.device_id)) 100 | .field("revision_id", &format!("{:04x}", self.revision_id)) 101 | .field("class_code", &format!("{:08x}", self.class_code)) 102 | .field("subsystem_vendor_id", &self.subsystem_vendor_id) 103 | .field("subsystem_id", &self.subsystem_id) 104 | .finish() 105 | } 106 | } 107 | 108 | pub(crate) fn get_registry_id(device: &IoService) -> Option { 109 | unsafe { 110 | let mut out = 0; 111 | let r = IORegistryEntryGetRegistryEntryID(device.get(), &mut out); 112 | 113 | if r == kIOReturnSuccess { 114 | Some(out) 115 | } else { 116 | // not sure this can actually fail. 117 | log::debug!("IORegistryEntryGetRegistryEntryID failed with {r}"); 118 | None 119 | } 120 | } 121 | } 122 | 123 | fn get_property(device: &IoService, property: &'static str) -> Option { 124 | unsafe { 125 | let cf_property = CFString::from_static_string(property); 126 | 127 | let raw = IORegistryEntrySearchCFProperty( 128 | device.get(), 129 | kIOServicePlane as *mut i8, 130 | cf_property.as_CFTypeRef() as *const _, 131 | std::ptr::null(), 132 | kIORegistryIterateRecursively | kIORegistryIterateParents, 133 | ); 134 | 135 | if raw.is_null() { 136 | log::debug!("Device does not have property `{property}`"); 137 | return None; 138 | } 139 | 140 | let res = CFType::wrap_under_create_rule(raw).downcast_into(); 141 | 142 | if res.is_none() { 143 | log::debug!("Failed to convert device property `{property}`"); 144 | } 145 | 146 | res 147 | } 148 | } 149 | 150 | fn get_string_property(device: &IoService, property: &'static str) -> Option { 151 | get_property::(device, property).map(|s| s.to_string()) 152 | } 153 | 154 | fn get_byte_array_property(device: &IoService, property: &'static str) -> Option> { 155 | let d = get_property::(device, property)?; 156 | Some(d.bytes().to_vec()) 157 | } 158 | 159 | fn get_ascii_array_property(device: &IoService, property: &'static str) -> Option { 160 | let d = get_property::(device, property)?; 161 | Some( 162 | d.bytes() 163 | .iter() 164 | .map(|b| *b as char) 165 | .filter(|c| *c != '\0') 166 | .collect(), 167 | ) 168 | } 169 | 170 | pub(crate) fn probe_controller(device: IoService) -> Option { 171 | let registry_id = get_registry_id(&device)?; 172 | log::debug!("Probing controller {registry_id:08x}"); 173 | 174 | // name is a CFData of ASCII characters 175 | let name = get_ascii_array_property(&device, "name")?; 176 | 177 | let class_name = get_string_property(&device, "IOClass")?; 178 | let io_name = get_string_property(&device, "IOName")?; 179 | 180 | let vendor_id = 181 | get_byte_array_property(&device, "vendor-id").map(|v| u16::from_le_bytes([v[0], v[1]]))?; 182 | let device_id = 183 | get_byte_array_property(&device, "device-id").map(|v| u16::from_le_bytes([v[0], v[1]]))?; 184 | let revision_id = get_byte_array_property(&device, "revision-id") 185 | .map(|v| u16::from_le_bytes([v[0], v[1]]))?; 186 | let class_code = get_byte_array_property(&device, "class-code") 187 | .map(|v| u32::from_le_bytes([v[0], v[1], v[2], v[3]]))?; 188 | let subsystem_vendor_id = get_byte_array_property(&device, "subsystem-vendor-id") 189 | .map(|v| u16::from_le_bytes([v[0], v[1]])); 190 | let subsystem_id = 191 | get_byte_array_property(&device, "subsystem-id").map(|v| u16::from_le_bytes([v[0], v[1]])); 192 | 193 | Some(HostControllerInfo { 194 | name, 195 | class_name, 196 | io_name, 197 | registry_id, 198 | vendor_id, 199 | device_id, 200 | revision_id, 201 | class_code, 202 | subsystem_vendor_id, 203 | subsystem_id, 204 | }) 205 | } 206 | 207 | pub(crate) fn get_controller(name: &str) -> Result { 208 | unsafe { 209 | let dictionary = IOServiceNameMatching(name.as_ptr() as *const i8); 210 | if dictionary.is_null() { 211 | return Err(Error::new(ErrorKind::IoKit, "IOServiceMatching failed")); 212 | } 213 | 214 | let mut iterator = 0; 215 | let r = IOServiceGetMatchingServices(kIOMasterPortDefault, dictionary, &mut iterator); 216 | if r != kIOReturnSuccess { 217 | return Err(Error::new(ErrorKind::IoKit, &r.to_string())); 218 | } 219 | 220 | IoServiceIterator::new(iterator) 221 | .next() 222 | .and_then(probe_controller) 223 | .ok_or(Error::new( 224 | ErrorKind::IoKit, 225 | &format!("No controller found for {}", name), 226 | )) 227 | } 228 | } 229 | 230 | /// Runs the system_profiler command for SPUSBDataType and parses the json stdout into a [`SystemProfile`]. 231 | /// 232 | /// Ok result not contain [`usb::DeviceExtra`] because system_profiler does not provide this. Use `get_spusb_with_extra` to combine with libusb output for [`Device`]s with `extra` 233 | pub fn get_spusb() -> Result { 234 | let output = Command::new("system_profiler") 235 | .args(["-timeout", "5", "-json", "SPUSBDataType"]) 236 | .output()?; 237 | 238 | if output.status.success() { 239 | serde_json::from_str(String::from_utf8(output.stdout)?.as_str()) 240 | .map_err(|e| { 241 | Error::new( 242 | ErrorKind::Parsing, 243 | &format!( 244 | "Failed to parse 'system_profiler -json SPUSBDataType'; Error({})", 245 | e 246 | ), 247 | ) 248 | // map to get pci.ids host controller data 249 | }) 250 | .map(|mut sp: SystemProfile| { 251 | for bus in sp.buses.iter_mut() { 252 | bus.fill_host_controller_from_ids(); 253 | } 254 | sp 255 | }) 256 | } else { 257 | log::error!( 258 | "system_profiler returned non-zero stderr: {:?}, stdout: {:?}", 259 | String::from_utf8(output.stderr)?, 260 | String::from_utf8(output.stdout)? 261 | ); 262 | Err(Error::new( 263 | ErrorKind::SystemProfiler, 264 | "system_profiler returned non-zero, use '--force-libusb' to bypass", 265 | )) 266 | } 267 | } 268 | 269 | /// Runs `get_spusb` and then adds in data obtained from libusb. Requires 'libusb' feature. 270 | /// 271 | /// `system_profiler` captures Apple buses (essentially root_hubs) that are not captured by libusb (but are captured by nusb); this method merges the two to so the bus information is kept. 272 | pub fn get_spusb_with_extra() -> Result { 273 | #[cfg(all(feature = "libusb", not(feature = "nusb")))] 274 | { 275 | get_spusb().and_then(|mut spusb| { 276 | crate::profiler::libusb::fill_spusb(&mut spusb)?; 277 | Ok(spusb) 278 | }) 279 | } 280 | 281 | #[cfg(feature = "nusb")] 282 | { 283 | get_spusb().and_then(|mut spusb| { 284 | crate::profiler::nusb::fill_spusb(&mut spusb)?; 285 | Ok(spusb) 286 | }) 287 | } 288 | 289 | #[cfg(all(not(feature = "libusb"), not(feature = "nusb")))] 290 | { 291 | Err(Error::new( 292 | ErrorKind::Unsupported, 293 | "nusb or libusb feature is required to do this, install with `cargo install --features nusb/libusb`", 294 | )) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/profiler/watch.rs: -------------------------------------------------------------------------------- 1 | //! Leverages the usb HotplugEvent to create a stream of system USB devices 2 | //! 3 | //! See the watch cli for a usage example. 4 | use super::nusb::NusbProfiler; 5 | use super::{Device, DeviceEvent, SystemProfile}; 6 | use crate::error::Error; 7 | use ::nusb::hotplug::HotplugEvent; 8 | use ::nusb::watch_devices; 9 | use chrono::Local; 10 | use futures_lite::stream::Stream; 11 | use std::pin::Pin; 12 | use std::sync::{Arc, Mutex}; 13 | use std::task::{Context, Poll}; 14 | 15 | /// Builder for [`SystemProfileStream`] 16 | #[derive(Default)] 17 | pub struct SystemProfileStreamBuilder { 18 | spusb: Option, 19 | verbose: bool, 20 | } 21 | 22 | impl SystemProfileStreamBuilder { 23 | /// Create a new [`SystemProfileStreamBuilder`] 24 | pub fn new() -> Self { 25 | Self { 26 | spusb: None, 27 | verbose: true, 28 | } 29 | } 30 | 31 | /// Set the verbosity of the stream 32 | /// 33 | /// When set to true, the stream will include full device descriptors for verbose printing 34 | pub fn is_verbose(mut self, verbose: bool) -> Self { 35 | self.verbose = verbose; 36 | self 37 | } 38 | 39 | /// Set the initial [`SystemProfile`] for the stream 40 | pub fn with_spusb(mut self, spusb: SystemProfile) -> Self { 41 | self.spusb = Some(spusb); 42 | self 43 | } 44 | 45 | /// Build the [`SystemProfileStream`] 46 | pub fn build(self) -> Result { 47 | let spusb = if let Some(spusb) = self.spusb { 48 | Arc::new(Mutex::new(spusb)) 49 | } else if self.verbose { 50 | Arc::new(Mutex::new(super::get_spusb_with_extra()?)) 51 | } else { 52 | Arc::new(Mutex::new(super::get_spusb()?)) 53 | }; 54 | let mut new = SystemProfileStream::new(spusb)?; 55 | new.verbose = self.verbose; 56 | Ok(new) 57 | } 58 | } 59 | 60 | /// A stream that yields an updated [`SystemProfile`] when a USB device is connected or disconnected 61 | pub struct SystemProfileStream { 62 | spusb: Arc>, 63 | watch_stream: Pin + Send>>, 64 | verbose: bool, 65 | } 66 | 67 | impl SystemProfileStream { 68 | /// Create a new [`SystemProfileStream`] with a initial [`SystemProfile`] 69 | pub fn new(spusb: Arc>) -> Result { 70 | let watch_stream = Box::pin(watch_devices()?); 71 | Ok(Self { 72 | spusb, 73 | watch_stream, 74 | verbose: true, 75 | }) 76 | } 77 | 78 | /// Get the [`SystemProfile`] from the stream 79 | pub fn get_profile(&self) -> Arc> { 80 | Arc::clone(&self.spusb) 81 | } 82 | 83 | /// Re-profile the system USB devices 84 | /// 85 | /// Last events will be lost 86 | pub fn reprofile(&self) -> Arc> { 87 | if self.verbose { 88 | Arc::new(Mutex::new(super::get_spusb_with_extra().unwrap())) 89 | } else { 90 | Arc::new(Mutex::new(super::get_spusb().unwrap())) 91 | } 92 | } 93 | } 94 | 95 | impl Stream for SystemProfileStream { 96 | type Item = Arc>; 97 | 98 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 99 | let extra = self.verbose; 100 | let this = self.get_mut(); 101 | let mut profiler = NusbProfiler::new(); 102 | 103 | match Pin::new(&mut this.watch_stream).poll_next(cx) { 104 | Poll::Ready(Some(event)) => { 105 | let mut spusb = this.spusb.lock().unwrap(); 106 | 107 | match event { 108 | HotplugEvent::Connected(device) => { 109 | let mut cyme_device: Device = 110 | profiler.build_spdevice(&device, extra).unwrap(); 111 | cyme_device.last_event = Some(DeviceEvent::Connected(Local::now())); 112 | // Windows bus number is a string ID so we need to find the bus based on this and assign the bus number created during cyme profiling 113 | #[cfg(target_os = "windows")] 114 | { 115 | if let Some(existing_bus) = 116 | spusb.buses.iter().find(|b| b.id == device.bus_id()) 117 | { 118 | log::debug!( 119 | "Win found bus for connected device ({}): {}", 120 | cyme_device, 121 | existing_bus.id 122 | ); 123 | if let Some(existing_number) = existing_bus.get_bus_number() { 124 | log::debug!( 125 | "Assigning bus number {} to device {}", 126 | existing_number, 127 | cyme_device 128 | ); 129 | cyme_device.location_id.bus = existing_number; 130 | } 131 | } else { 132 | log::error!( 133 | "Win no bus found for connected device, seeking bus_id: {}", 134 | device.bus_id() 135 | ); 136 | } 137 | } 138 | 139 | spusb.insert(cyme_device); 140 | } 141 | HotplugEvent::Disconnected(id) => { 142 | if let Some(device) = spusb.get_id_mut(&id) { 143 | device.last_event = Some(DeviceEvent::Disconnected(Local::now())); 144 | } 145 | } 146 | } 147 | Poll::Ready(Some(Arc::clone(&this.spusb))) 148 | } 149 | Poll::Ready(None) => Poll::Ready(None), 150 | Poll::Pending => Poll::Pending, 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | //! Types used in crate non-specific to a module 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | use serde::de::{self, MapAccess, SeqAccess, Visitor}; 6 | use serde::{Deserialize, Deserializer, Serialize}; 7 | 8 | use crate::error::{self, Error, ErrorKind}; 9 | 10 | /// A numerical `value` converted from a String, which includes a `unit` and `description` 11 | /// 12 | /// Serialized string is of format "\[value\] \[unit\]" where u32 of f32 is supported 13 | /// 14 | /// ``` 15 | /// use std::str::FromStr; 16 | /// use cyme::types::NumericalUnit; 17 | /// 18 | /// let s: &'static str = "100.0 W"; 19 | /// let nu = NumericalUnit::from_str(s).unwrap(); 20 | /// assert_eq!(nu, NumericalUnit{ value: 100.0, unit: "W".into(), description: None }); 21 | /// 22 | /// let s: &'static str = "59 mA"; 23 | /// let nu = NumericalUnit::from_str(s).unwrap(); 24 | /// assert_eq!(nu, NumericalUnit{ value: 59, unit: "mA".into(), description: None }); 25 | /// ``` 26 | #[derive(Debug, Default, Clone, PartialEq, Serialize)] 27 | pub struct NumericalUnit { 28 | /// Numerical value 29 | pub value: T, 30 | /// SI unit for the numerical value 31 | pub unit: String, 32 | /// Description of numerical value 33 | pub description: Option, 34 | } 35 | 36 | impl fmt::Display for NumericalUnit { 37 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 38 | if let Some(width) = f.width() { 39 | let actual_width = width - self.unit.len() - 1; 40 | write!(f, "{:actual_width$} {:}", self.value, self.unit) 41 | } else { 42 | write!(f, "{:} {:}", self.value, self.unit) 43 | } 44 | } 45 | } 46 | 47 | impl fmt::Display for NumericalUnit { 48 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 49 | // If we received a precision, we use it. 50 | if let Some(width) = f.width() { 51 | let actual_width = width - self.unit.len() - 1; 52 | write!( 53 | f, 54 | "{1:actual_width$.*} {2}", 55 | f.precision().unwrap_or(2), 56 | self.value, 57 | self.unit 58 | ) 59 | } else { 60 | write!( 61 | f, 62 | "{1:.*} {2}", 63 | f.precision().unwrap_or(2), 64 | self.value, 65 | self.unit 66 | ) 67 | } 68 | } 69 | } 70 | 71 | impl FromStr for NumericalUnit { 72 | type Err = Error; 73 | 74 | fn from_str(s: &str) -> error::Result { 75 | let value_split: Vec<&str> = s.trim().split(' ').collect(); 76 | if value_split.len() >= 2 { 77 | Ok(NumericalUnit { 78 | value: value_split[0] 79 | .trim() 80 | .parse::() 81 | .map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))?, 82 | unit: value_split[1].trim().to_string(), 83 | description: None, 84 | }) 85 | } else { 86 | Err(Error::new( 87 | ErrorKind::Decoding, 88 | "string split does not contain [u32] [unit]", 89 | )) 90 | } 91 | } 92 | } 93 | 94 | impl FromStr for NumericalUnit { 95 | type Err = Error; 96 | 97 | fn from_str(s: &str) -> error::Result { 98 | let value_split: Vec<&str> = s.trim().split(' ').collect(); 99 | if value_split.len() >= 2 { 100 | Ok(NumericalUnit { 101 | value: value_split[0] 102 | .trim() 103 | .parse::() 104 | .map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))?, 105 | unit: value_split[1].trim().to_string(), 106 | description: None, 107 | }) 108 | } else { 109 | Err(Error::new( 110 | ErrorKind::Decoding, 111 | "string split does not contain [f32] [unit]", 112 | )) 113 | } 114 | } 115 | } 116 | 117 | impl<'de> Deserialize<'de> for NumericalUnit { 118 | fn deserialize(deserializer: D) -> Result 119 | where 120 | D: Deserializer<'de>, 121 | { 122 | #[derive(Deserialize)] 123 | #[serde(field_identifier, rename_all = "lowercase")] 124 | #[serde(untagged)] 125 | enum Field { 126 | Value, 127 | Unit, 128 | Description, 129 | } 130 | 131 | struct DeviceNumericalUnitU32Visitor; 132 | 133 | impl<'de> Visitor<'de> for DeviceNumericalUnitU32Visitor { 134 | type Value = NumericalUnit; 135 | 136 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 137 | formatter.write_str("a string with format '[int] [unit]'") 138 | } 139 | 140 | fn visit_seq(self, mut seq: V) -> Result, V::Error> 141 | where 142 | V: SeqAccess<'de>, 143 | { 144 | let value = seq 145 | .next_element()? 146 | .ok_or_else(|| de::Error::invalid_length(0, &self))?; 147 | let unit = seq 148 | .next_element()? 149 | .ok_or_else(|| de::Error::invalid_length(1, &self))?; 150 | let description = seq 151 | .next_element() 152 | .ok() 153 | .ok_or_else(|| de::Error::invalid_length(2, &self))?; 154 | Ok(NumericalUnit:: { 155 | value, 156 | unit, 157 | description, 158 | }) 159 | } 160 | 161 | fn visit_map(self, mut map: V) -> Result, V::Error> 162 | where 163 | V: MapAccess<'de>, 164 | { 165 | let mut value = None; 166 | let mut unit = None; 167 | let mut description = None; 168 | while let Some(key) = map.next_key()? { 169 | match key { 170 | Field::Value => { 171 | if value.is_some() { 172 | return Err(de::Error::duplicate_field("value")); 173 | } 174 | value = Some(map.next_value()?); 175 | } 176 | Field::Unit => { 177 | if unit.is_some() { 178 | return Err(de::Error::duplicate_field("unit")); 179 | } 180 | unit = Some(map.next_value()?); 181 | } 182 | Field::Description => { 183 | if description.is_some() { 184 | return Err(de::Error::duplicate_field("description")); 185 | } 186 | description = map.next_value().ok(); 187 | } 188 | } 189 | } 190 | let value = value.ok_or_else(|| de::Error::missing_field("value"))?; 191 | let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?; 192 | Ok(NumericalUnit:: { 193 | value, 194 | unit, 195 | description, 196 | }) 197 | } 198 | 199 | fn visit_string(self, value: String) -> Result 200 | where 201 | E: de::Error, 202 | { 203 | NumericalUnit::from_str(value.as_str()).map_err(E::custom) 204 | } 205 | 206 | fn visit_str(self, value: &str) -> Result 207 | where 208 | E: de::Error, 209 | { 210 | NumericalUnit::from_str(value).map_err(E::custom) 211 | } 212 | } 213 | 214 | deserializer.deserialize_any(DeviceNumericalUnitU32Visitor) 215 | } 216 | } 217 | 218 | impl<'de> Deserialize<'de> for NumericalUnit { 219 | fn deserialize(deserializer: D) -> Result 220 | where 221 | D: Deserializer<'de>, 222 | { 223 | #[derive(Deserialize)] 224 | #[serde(field_identifier, rename_all = "lowercase")] 225 | #[serde(untagged)] 226 | enum Field { 227 | Value, 228 | Unit, 229 | #[serde(deserialize_with = "deserialize_description")] 230 | Description, 231 | } 232 | 233 | struct DeviceNumericalUnitF32Visitor; 234 | 235 | impl<'de> Visitor<'de> for DeviceNumericalUnitF32Visitor { 236 | type Value = NumericalUnit; 237 | 238 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 239 | formatter.write_str("a string with format '[float] [unit]'") 240 | } 241 | 242 | fn visit_seq(self, mut seq: V) -> Result, V::Error> 243 | where 244 | V: SeqAccess<'de>, 245 | { 246 | let value = seq 247 | .next_element()? 248 | .ok_or_else(|| de::Error::invalid_length(0, &self))?; 249 | let unit = seq 250 | .next_element()? 251 | .ok_or_else(|| de::Error::invalid_length(1, &self))?; 252 | let description = seq 253 | .next_element() 254 | .ok() 255 | .ok_or_else(|| de::Error::invalid_length(2, &self))?; 256 | Ok(NumericalUnit:: { 257 | value, 258 | unit, 259 | description, 260 | }) 261 | } 262 | 263 | fn visit_map(self, mut map: V) -> Result, V::Error> 264 | where 265 | V: MapAccess<'de>, 266 | { 267 | let mut value = None; 268 | let mut unit = None; 269 | let mut description = None; 270 | while let Some(key) = map.next_key()? { 271 | match key { 272 | Field::Value => { 273 | if value.is_some() { 274 | return Err(de::Error::duplicate_field("value")); 275 | } 276 | value = Some(map.next_value()?); 277 | } 278 | Field::Unit => { 279 | if unit.is_some() { 280 | return Err(de::Error::duplicate_field("unit")); 281 | } 282 | unit = Some(map.next_value()?); 283 | } 284 | Field::Description => { 285 | if description.is_some() { 286 | return Err(de::Error::duplicate_field("description")); 287 | } 288 | description = map.next_value().ok(); 289 | } 290 | } 291 | } 292 | let value = value.ok_or_else(|| de::Error::missing_field("value"))?; 293 | let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?; 294 | Ok(NumericalUnit:: { 295 | value, 296 | unit, 297 | description, 298 | }) 299 | } 300 | 301 | fn visit_string(self, value: String) -> Result 302 | where 303 | E: de::Error, 304 | { 305 | NumericalUnit::from_str(value.as_str()).map_err(E::custom) 306 | } 307 | 308 | fn visit_str(self, value: &str) -> Result 309 | where 310 | E: de::Error, 311 | { 312 | NumericalUnit::from_str(value).map_err(E::custom) 313 | } 314 | } 315 | 316 | deserializer.deserialize_any(DeviceNumericalUnitF32Visitor) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/udev.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to get device information using udev - only supported on Linux. Requires 'udev' feature. 2 | use udevrs::{udev_new, UdevDevice, UdevHwdb}; 3 | 4 | use crate::error::{Error, ErrorKind}; 5 | 6 | /// Contains data returned by [`get_udev_info()`]. 7 | #[derive(Debug, Clone, PartialEq, Default)] 8 | pub struct UdevInfo { 9 | /// The driver name for the device 10 | pub driver: Option, 11 | /// The syspath for the device 12 | pub syspath: Option, 13 | } 14 | 15 | fn get_device(port_path: &str) -> Result { 16 | UdevDevice::new_from_subsystem_sysname(udev_new(), "usb", port_path).map_err(|e| { 17 | log::error!( 18 | "Failed to get udev info for device at {}: Error({})", 19 | port_path, 20 | e 21 | ); 22 | Error::new( 23 | ErrorKind::Udev, 24 | &format!( 25 | "Failed to get udev info for device at {}: Error({})", 26 | port_path, e 27 | ), 28 | ) 29 | }) 30 | } 31 | 32 | /// Lookup the driver and syspath for a device given the `port_path`. Returns [`UdevInfo`] containing both. 33 | /// 34 | /// ```no_run 35 | /// use cyme::udev::get_udev_info; 36 | /// 37 | /// let udevi = get_udev_info("1-0:1.0").unwrap(); 38 | /// assert_eq!(udevi.driver, Some("hub".into())); 39 | /// assert_eq!(udevi.syspath.unwrap().contains("usb1/1-0:1.0"), true); 40 | /// ``` 41 | pub fn get_udev_info(port_path: &str) -> Result { 42 | let mut device = get_device(port_path)?; 43 | 44 | Ok({ 45 | UdevInfo { 46 | driver: device.get_driver().map(|s| s.trim().to_string()), 47 | syspath: Some(device.syspath().trim().to_string()), 48 | } 49 | }) 50 | } 51 | 52 | /// Lookup the driver name for a device given the `port_path`. 53 | /// 54 | /// ```no_run 55 | /// use cyme::udev::get_udev_driver_name; 56 | /// let driver = get_udev_driver_name("1-0:1.0").unwrap(); 57 | /// assert_eq!(driver, Some("hub".into())); 58 | /// ``` 59 | pub fn get_udev_driver_name(port_path: &str) -> Result, Error> { 60 | let mut device = get_device(port_path)?; 61 | 62 | Ok(device.get_driver().map(|s| s.trim().to_string())) 63 | } 64 | 65 | /// Lookup the syspath for a device given the `port_path`. 66 | /// 67 | /// ```no_run 68 | /// use cyme::udev::get_udev_syspath; 69 | /// let syspath = get_udev_syspath("1-0:1.0").unwrap(); 70 | /// assert_eq!(syspath.unwrap().contains("usb1/1-0:1.0"), true); 71 | /// ``` 72 | pub fn get_udev_syspath(port_path: &str) -> Result, Error> { 73 | let device = get_device(port_path)?; 74 | 75 | Ok(Some(device.syspath().trim().to_string())) 76 | } 77 | 78 | /// Lookup a udev attribute given the `port_path` and `attribute`. 79 | /// 80 | /// This only works on Linux and not all devices have all attributes. 81 | /// These attributes are generally readable by all users. 82 | /// 83 | /// NOTE: In general you should read from sysfs directly as it does not 84 | /// depend on the udev feature. See `get_sysfs_string()` in profiler.rs 85 | /// 86 | /// ```no_run 87 | /// use cyme::udev::get_udev_attribute; 88 | /// 89 | /// let interface_class = get_udev_attribute("1-0:1.0", "bInterfaceClass").unwrap(); 90 | /// assert_eq!(interface_class, Some("09".into())); 91 | /// ``` 92 | pub fn get_udev_attribute + std::fmt::Display + Into>( 93 | port_path: &str, 94 | attribute: T, 95 | ) -> Result, Error> { 96 | let mut device = get_device(port_path)?; 97 | 98 | Ok(device 99 | .get_sysattr_value(&attribute.into()) 100 | .map(|s| s.trim().to_string())) 101 | } 102 | 103 | /// Utilities to get device information using udev hwdb - only supported on Linux. Requires 'udev' feature. 104 | pub mod hwdb { 105 | use super::*; 106 | 107 | /// Lookup an entry in the udev hwdb given the `modalias` and `key`. 108 | /// 109 | /// Should act like https://github.com/gregkh/usbutils/blob/master/names.c#L115 110 | /// 111 | /// ``` 112 | /// use cyme::udev; 113 | /// 114 | /// let modalias = "usb:v1D6Bp0001"; 115 | /// let vendor = udev::hwdb::get(&modalias, "ID_VENDOR_FROM_DATABASE").unwrap(); 116 | /// 117 | /// assert_eq!(vendor, Some("Linux Foundation".into())); 118 | /// 119 | /// let modalias = "usb:v1366p0101"; 120 | /// let vendor = udev::hwdb::get(&modalias, "ID_MODEL_FROM_DATABASE").unwrap(); 121 | /// 122 | /// assert_eq!(vendor, Some("J-Link PLUS".into())); 123 | /// ``` 124 | pub fn get(modalias: &str, key: &'static str) -> Result, Error> { 125 | let mut hwdb = UdevHwdb::new(udev_new()).map_err(|e| { 126 | log::error!("Failed to get hwdb: Error({})", e); 127 | Error::new( 128 | ErrorKind::Udev, 129 | &format!("Failed to get hwdb: Error({})", e), 130 | ) 131 | })?; 132 | 133 | Ok(udevrs::udev_hwdb_query_one(&mut hwdb, modalias, key).map(|s| s.trim().to_string())) 134 | } 135 | } 136 | 137 | #[cfg(test)] 138 | mod tests { 139 | use super::*; 140 | 141 | /// Tests can obtain driver and syspath for root_hub on bus 1 - only do if we have USB 142 | #[cfg_attr(not(feature = "usb_test"), ignore)] 143 | #[test] 144 | fn test_udev_info() { 145 | let udevi = get_udev_info("1-0:1.0").unwrap(); 146 | assert_eq!(udevi.driver, Some("hub".into())); 147 | assert!(udevi.syspath.unwrap().contains("1-0:1.0")); 148 | } 149 | 150 | /// Tests can lookup bInterfaceClass of the root hub, which is always 09 151 | #[cfg_attr(not(feature = "usb_test"), ignore)] 152 | #[test] 153 | fn test_udev_attribute() { 154 | let interface_class = get_udev_attribute("1-0:1.0", "bInterfaceClass").unwrap(); 155 | assert_eq!(interface_class, Some("09".into())); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/udev_ffi.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to get device information using udev libudev FFI - only supported on Linux. Requires 'udev_ffi' feature. 2 | use std::path::Path; 3 | use udevlib; 4 | 5 | use crate::error::{Error, ErrorKind}; 6 | 7 | /// Contains data returned by [`get_udev_info()`]. 8 | #[derive(Debug, Clone, PartialEq, Default)] 9 | pub struct UdevInfo { 10 | /// The driver name for the device 11 | pub driver: Option, 12 | /// The syspath for the device 13 | pub syspath: Option, 14 | } 15 | 16 | fn get_device(port_path: &str) -> Result { 17 | udevlib::Device::from_subsystem_sysname(String::from("usb"), port_path.to_string()).map_err( 18 | |e| { 19 | log::error!( 20 | "Failed to get udev info for device at {}: Error({})", 21 | port_path, 22 | e 23 | ); 24 | Error::new( 25 | ErrorKind::Udev, 26 | &format!( 27 | "Failed to get udev info for device at {}: Error({})", 28 | port_path, e 29 | ), 30 | ) 31 | }, 32 | ) 33 | } 34 | 35 | /// Lookup the driver and syspath for a device given the `port_path`. Returns [`UdevInfo`] containing both. 36 | /// 37 | /// ```no_run 38 | /// use cyme::udev::get_udev_info; 39 | /// 40 | /// let udevi = get_udev_info("1-0:1.0").unwrap(); 41 | /// assert_eq!(udevi.driver, Some("hub".into())); 42 | /// assert_eq!(udevi.syspath.unwrap().contains("usb1/1-0:1.0"), true); 43 | /// ``` 44 | pub fn get_udev_info(port_path: &str) -> Result { 45 | let device = get_device(port_path)?; 46 | 47 | Ok({ 48 | UdevInfo { 49 | driver: device 50 | .driver() 51 | .map(|s| s.to_str().unwrap_or("").to_string()), 52 | syspath: device.syspath().to_str().map(|s| s.to_string()), 53 | } 54 | }) 55 | } 56 | 57 | /// Lookup the driver name for a device given the `port_path`. 58 | /// 59 | /// ```no_run 60 | /// use cyme::udev::get_udev_driver_name; 61 | /// let driver = get_udev_driver_name("1-0:1.0").unwrap(); 62 | /// assert_eq!(driver, Some("hub".into())); 63 | /// ``` 64 | pub fn get_udev_driver_name(port_path: &str) -> Result, Error> { 65 | let device = get_device(port_path)?; 66 | 67 | Ok(device 68 | .driver() 69 | .map(|s| s.to_str().unwrap_or("").to_string())) 70 | } 71 | 72 | /// Lookup the syspath for a device given the `port_path`. 73 | /// 74 | /// ```no_run 75 | /// use cyme::udev::get_udev_syspath; 76 | /// let syspath = get_udev_syspath("1-0:1.0").unwrap(); 77 | /// assert_eq!(syspath.unwrap().contains("usb1/1-0:1.0"), true); 78 | /// ``` 79 | pub fn get_udev_syspath(port_path: &str) -> Result, Error> { 80 | let device = get_device(port_path)?; 81 | 82 | Ok(device.syspath().to_str().map(|s| s.to_string())) 83 | } 84 | 85 | /// Lookup a udev attribute given the `port_path` and `attribute`. 86 | /// 87 | /// This only works on Linux and not all devices have all attributes. 88 | /// These attributes are generally readable by all users. 89 | /// 90 | /// NOTE: In general you should read from sysfs directly as it does not 91 | /// depend on the udev feature. See `get_sysfs_string()` in lsusb.rs 92 | /// 93 | /// ```no_run 94 | /// use cyme::udev::get_udev_attribute; 95 | /// 96 | /// let interface_class = get_udev_attribute("1-0:1.0", "bInterfaceClass").unwrap(); 97 | /// assert_eq!(interface_class, Some("09".into())); 98 | /// ``` 99 | pub fn get_udev_attribute + std::fmt::Display>( 100 | port_path: &str, 101 | attribute: T, 102 | ) -> Result, Error> { 103 | let path: String = format!("/sys/bus/usb/devices/{}", port_path); 104 | let device = udevlib::Device::from_syspath(Path::new(&path)).map_err(|e| { 105 | Error::new( 106 | ErrorKind::Udev, 107 | &format!( 108 | "Failed to get udev attribute {} for device at {}: Error({})", 109 | attribute, path, e 110 | ), 111 | ) 112 | })?; 113 | 114 | Ok(device 115 | .attribute_value(attribute) 116 | .map(|s| s.to_str().unwrap_or("").to_string())) 117 | } 118 | 119 | /// udev hwdb lookup functions 120 | /// 121 | /// Protected by the `udev_hwdb` feature because 'libudev-sys' excludes hwdb ffi bindings if native udev does not support hwdb 122 | #[cfg(feature = "udev_hwdb")] 123 | pub mod hwdb { 124 | use super::*; 125 | /// Lookup an entry in the udev hwdb given the `modalias` and `key`. 126 | /// 127 | /// Should act like https://github.com/gregkh/usbutils/blob/master/names.c#L115 128 | /// 129 | /// ``` 130 | /// use cyme::udev; 131 | /// 132 | /// let modalias = "usb:v1D6Bp0001"; 133 | /// let vendor = udev::hwdb::get(&modalias, "ID_VENDOR_FROM_DATABASE").unwrap(); 134 | /// 135 | /// assert_eq!(vendor, Some("Linux Foundation".into())); 136 | /// 137 | /// let modalias = "usb:v*p*d*dc03dsc01dp01*"; 138 | /// let vendor = udev::hwdb::get(&modalias, "ID_USB_PROTOCOL_FROM_DATABASE").unwrap(); 139 | /// 140 | /// assert_eq!(vendor, Some("Keyboard".into())); 141 | /// ``` 142 | pub fn get(modalias: &str, key: &'static str) -> Result, Error> { 143 | let hwdb = udevlib::Hwdb::new().map_err(|e| { 144 | Error::new( 145 | ErrorKind::Udev, 146 | &format!("Failed to get hwdb: Error({})", e), 147 | ) 148 | })?; 149 | 150 | Ok(hwdb 151 | .query_one(&modalias.to_string(), &key.to_string()) 152 | .map(|s| s.to_str().unwrap_or("").to_string())) 153 | } 154 | } 155 | 156 | #[cfg(test)] 157 | mod tests { 158 | use super::*; 159 | 160 | /// Tests can obtain driver and syspath for root_hub on bus 1 - only do if we have USB 161 | #[cfg_attr(not(feature = "usb_test"), ignore)] 162 | #[test] 163 | fn test_udev_info() { 164 | let udevi = get_udev_info("1-0:1.0").unwrap(); 165 | assert_eq!(udevi.driver, Some("hub".into())); 166 | assert!(udevi.syspath.unwrap().contains("usb1/1-0:1.0")); 167 | } 168 | 169 | /// Tests can lookup bInterfaceClass of the root hub, which is always 09 170 | #[cfg_attr(not(feature = "usb_test"), ignore)] 171 | #[test] 172 | fn test_udev_attribute() { 173 | let interface_class = get_udev_attribute("1-0:1.0", "bInterfaceClass").unwrap(); 174 | assert_eq!(interface_class, Some("09".into())); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Runs tests using actual binary, adapted from 'fd' method: https://github.com/sharkdp/fd/blob/master/tests/testenv/mod.rs 2 | #![allow(dead_code)] 3 | use serde_json::json; 4 | use std::env; 5 | use std::fs::File; 6 | use std::io::{BufReader, Read}; 7 | use std::path::PathBuf; 8 | use std::process; 9 | // #[cfg(windows)] 10 | // use std::os::windows; 11 | 12 | // if changing content of DeviceX structs, update the tests data with `--from-json TEST_DUMP --json TEST_ARGS > file.json` 13 | /// Dump from the `system_profiler` command on macOS 14 | pub const SYSTEM_PROFILER_DUMP_PATH: &str = "./tests/data/system_profiler_dump.json"; 15 | /// Dump using macOS system_profiler so no [`DeviceExtra`] 16 | pub const CYME_SP_TREE_DUMP: &str = "./tests/data/cyme_sp_macos_tree.json"; 17 | /// Dump using macOS system_profiler and libusb merge so with [`DeviceExtra`] 18 | pub const CYME_LIBUSB_MERGE_MACOS_TREE_DUMP: &str = 19 | "./tests/data/cyme_libusb_merge_macos_tree.json"; 20 | /// Dump using macOS force libusb merge so with [`DeviceExtra`] but not Apple internal buses 21 | pub const CYME_LIBUSB_MACOS_TREE_DUMP: &str = "./tests/data/cyme_libusb_macos_tree.json"; 22 | /// Dump using Linux with libusb so with [`DeviceExtra`] 23 | pub const CYME_LIBUSB_LINUX_TREE_DUMP: &str = "./tests/data/cyme_libusb_linux_tree.json"; 24 | /// Output of lsusb --tree 25 | pub const LSUSB_TREE_OUTPUT: &str = "./tests/data/lsusb_tree.txt"; 26 | /// Output of lsusb --tree -vvv 27 | pub const LSUSB_TREE_OUTPUT_VERBOSE: &str = "./tests/data/lsusb_tree_verbose.txt"; 28 | /// Output of lsusb 29 | pub const LSUSB_OUTPUT: &str = "./tests/data/lsusb_list.txt"; 30 | /// Output of lsusb --verbose 31 | pub const LSUSB_OUTPUT_VERBOSE: &str = "./tests/data/lsusb_verbose.txt"; 32 | 33 | pub fn read_dump(file_name: &str) -> BufReader { 34 | let f = File::open(file_name).expect("Unable to open json dump file"); 35 | BufReader::new(f) 36 | } 37 | 38 | pub fn read_dump_to_string(file_name: &str) -> String { 39 | let mut ret = String::new(); 40 | let mut br = read_dump(file_name); 41 | br.read_to_string(&mut ret) 42 | .unwrap_or_else(|_| panic!("Failed to read {}", file_name)); 43 | ret 44 | } 45 | 46 | pub fn sp_data_from_system_profiler() -> cyme::profiler::SystemProfile { 47 | let mut br = read_dump(SYSTEM_PROFILER_DUMP_PATH); 48 | let mut data = String::new(); 49 | br.read_to_string(&mut data).expect("Unable to read string"); 50 | 51 | serde_json::from_str::(&data).unwrap() 52 | } 53 | 54 | pub fn sp_data_from_libusb_linux() -> cyme::profiler::SystemProfile { 55 | let mut br = read_dump(CYME_LIBUSB_LINUX_TREE_DUMP); 56 | let mut data = String::new(); 57 | br.read_to_string(&mut data).expect("Unable to read string"); 58 | 59 | serde_json::from_str::(&data).unwrap() 60 | } 61 | 62 | /// Environment for the integration tests. 63 | pub struct TestEnv { 64 | /// Path to the *cyme* executable. 65 | cyme_exe: PathBuf, 66 | /// Normalize each line by sorting the whitespace-separated words 67 | normalize_line: bool, 68 | /// Strip whitespace at start 69 | strip_start: bool, 70 | } 71 | 72 | /// Find the *cyme* executable. 73 | fn find_cyme_exe() -> PathBuf { 74 | // Tests exe is in target/debug/deps, the *cyme* exe is in target/debug 75 | let root = env::current_exe() 76 | .expect("tests executable") 77 | .parent() 78 | .expect("tests executable directory") 79 | .parent() 80 | .expect("cyme executable directory") 81 | .to_path_buf(); 82 | 83 | let exe_name = if cfg!(windows) { "cyme.exe" } else { "cyme" }; 84 | 85 | root.join(exe_name) 86 | } 87 | 88 | /// Format an error message for when *cyme* did not exit successfully. 89 | fn format_exit_error(args: &[&str], output: &process::Output) -> String { 90 | format!( 91 | "`cyme {}` did not exit successfully.\nstdout:\n---\n{}---\nstderr:\n---\n{}---", 92 | args.join(" "), 93 | String::from_utf8_lossy(&output.stdout), 94 | String::from_utf8_lossy(&output.stderr) 95 | ) 96 | } 97 | 98 | /// Format an error message for when the output of *cyme* did not match the expected output. 99 | fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String { 100 | // Generate diff text. 101 | let diff_text = diff::lines(expected, actual) 102 | .into_iter() 103 | .map(|diff| match diff { 104 | diff::Result::Left(l) => format!("-{}", l), 105 | diff::Result::Both(l, _) => format!(" {}", l), 106 | diff::Result::Right(r) => format!("+{}", r), 107 | }) 108 | .collect::>() 109 | .join("\n"); 110 | 111 | format!( 112 | concat!( 113 | "`cyme {}` did not produce the expected output.\n", 114 | "Showing diff between expected and actual:\n{}\n" 115 | ), 116 | args.join(" "), 117 | diff_text 118 | ) 119 | } 120 | 121 | /// Normalize the output for comparison. 122 | fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String { 123 | // Split into lines and normalize separators. 124 | let mut lines = s 125 | .replace('\0', "NULL\n") 126 | .lines() 127 | .map(|line| { 128 | let line = if trim_start { line.trim_start() } else { line }; 129 | if normalize_line { 130 | let mut words: Vec<_> = line.split_whitespace().collect(); 131 | words.sort_unstable(); 132 | return words.join(" "); 133 | } 134 | line.to_string() 135 | }) 136 | .collect::>(); 137 | 138 | lines.sort(); 139 | lines.join("\n") 140 | } 141 | 142 | /// Trim whitespace from the beginning of each line. 143 | fn trim_lines(s: &str) -> String { 144 | s.lines() 145 | .map(|line| line.trim_start()) 146 | .fold(String::new(), |mut str, line| { 147 | str.push_str(line); 148 | str.push('\n'); 149 | str 150 | }) 151 | } 152 | 153 | impl TestEnv { 154 | pub fn new() -> TestEnv { 155 | let cyme_exe = find_cyme_exe(); 156 | 157 | TestEnv { 158 | cyme_exe, 159 | normalize_line: false, 160 | strip_start: false, 161 | } 162 | } 163 | 164 | pub fn normalize_line(self, normalize: bool, strip_start: bool) -> TestEnv { 165 | TestEnv { 166 | cyme_exe: self.cyme_exe, 167 | normalize_line: normalize, 168 | strip_start, 169 | } 170 | } 171 | 172 | /// Get the path of the cyme executable. 173 | #[cfg_attr(windows, allow(unused))] 174 | pub fn test_exe(&self) -> &PathBuf { 175 | &self.cyme_exe 176 | } 177 | 178 | /// Assert that calling *cyme* in the specified path under the root working directory, 179 | /// and with the specified arguments produces the expected output. 180 | pub fn assert_success_and_get_output( 181 | &self, 182 | dump_file: Option<&str>, 183 | args: &[&str], 184 | ) -> process::Output { 185 | // Setup *cyme* command. 186 | let mut cmd = process::Command::new(&self.cyme_exe); 187 | if let Some(dump) = dump_file { 188 | cmd.arg("--from-json").arg(dump).args(args); 189 | } else { 190 | cmd.arg("--json").args(args); 191 | } 192 | 193 | // Run *cyme*. 194 | let output = cmd.output().expect("cyme output"); 195 | 196 | // Check for exit status. 197 | if !output.status.success() { 198 | panic!("{}", format_exit_error(args, &output)); 199 | } 200 | 201 | output 202 | } 203 | 204 | pub fn assert_success_and_get_normalized_output( 205 | &self, 206 | dump_file: Option<&str>, 207 | args: &[&str], 208 | ) -> String { 209 | let output = self.assert_success_and_get_output(dump_file, args); 210 | normalize_output( 211 | &String::from_utf8_lossy(&output.stdout), 212 | self.strip_start, 213 | self.normalize_line, 214 | ) 215 | } 216 | 217 | /// Assert that calling *cyme* with the specified arguments produces the expected output. 218 | pub fn assert_output( 219 | &self, 220 | dump_file: Option<&str>, 221 | args: &[&str], 222 | expected: &str, 223 | contains: bool, 224 | ) { 225 | // Don't touch if doing contains 226 | let (expected, actual) = if contains { 227 | let output = self.assert_success_and_get_output(dump_file, args); 228 | ( 229 | expected.to_string(), 230 | String::from_utf8_lossy(&output.stdout).to_string(), 231 | ) 232 | // Normalize both expected and actual output. 233 | } else { 234 | ( 235 | normalize_output(expected, self.strip_start, self.normalize_line), 236 | self.assert_success_and_get_normalized_output(dump_file, args), 237 | ) 238 | }; 239 | 240 | // Compare actual output to expected output. 241 | if contains { 242 | if !actual.contains(&expected) { 243 | panic!("{}", format_output_error(args, &expected, &actual)); 244 | } 245 | } else if expected != actual { 246 | panic!("{}", format_output_error(args, &expected, &actual)); 247 | } 248 | } 249 | 250 | pub fn assert_output_json(&self, dump_file: Option<&str>, args: &[&str], expected: &str) { 251 | // Normalize both expected and actual output. 252 | let output = self.assert_success_and_get_output(dump_file, args); 253 | let actual = String::from_utf8_lossy(&output.stdout).to_string(); 254 | 255 | // Compare actual output to expected output. 256 | assert_json_diff::assert_json_include!(actual: json!(actual), expected: json!(expected)); 257 | } 258 | 259 | /// Parses output back to [`cyme::profiler::SystemProfile`] and checks device with `port_path` exists in it 260 | pub fn assert_output_contains_port_path( 261 | &self, 262 | dump_file: Option<&str>, 263 | args: &[&str], 264 | port_path: &str, 265 | ) { 266 | // Normalize both expected and actual output. 267 | let output = self.assert_success_and_get_output(dump_file, args); 268 | let actual = String::from_utf8_lossy(&output.stdout).to_string(); 269 | let spdata_out = serde_json::from_str::(&actual).unwrap(); 270 | let port_path = cyme::usb::PortPath::try_from(port_path).unwrap(); 271 | 272 | assert!(spdata_out.get_node(&port_path).is_some()); 273 | } 274 | 275 | /// Similar to assert_output, but able to handle non-utf8 output 276 | #[cfg(all(unix, not(target_os = "macos")))] 277 | pub fn assert_output_raw(&self, dump_file: Option<&str>, args: &[&str], expected: &[u8]) { 278 | let output = self.assert_success_and_get_output(dump_file, args); 279 | 280 | assert_eq!(expected, &output.stdout[..]); 281 | } 282 | 283 | /// Assert that calling *cyme* with the specified arguments produces the expected error, 284 | /// and does not succeed. 285 | pub fn assert_failure_with_error( 286 | &self, 287 | dump_file: Option<&str>, 288 | args: &[&str], 289 | expected: &str, 290 | ) { 291 | let status = self.assert_error(dump_file, args, Some(expected)); 292 | if status.success() { 293 | panic!("error '{}' did not occur.", expected); 294 | } 295 | } 296 | 297 | /// Assert that calling *cyme* with the specified arguments does not succeed. 298 | pub fn assert_failure(&self, dump_file: Option<&str>, args: &[&str]) { 299 | let status = self.assert_error(dump_file, args, None); 300 | if status.success() { 301 | panic!("Failure did not occur as expected."); 302 | } 303 | } 304 | 305 | fn assert_error( 306 | &self, 307 | dump_file: Option<&str>, 308 | args: &[&str], 309 | expected: Option<&str>, 310 | ) -> process::ExitStatus { 311 | // Setup *cyme* command. 312 | let mut cmd = process::Command::new(&self.cyme_exe); 313 | if let Some(dump) = dump_file { 314 | cmd.arg("--from-json").arg(dump).args(args); 315 | } else { 316 | cmd.arg("--json").args(args); 317 | } 318 | 319 | // Run *cyme*. 320 | let output = cmd.output().expect("cyme output"); 321 | 322 | if let Some(expected) = expected { 323 | // Normalize both expected and actual output. 324 | let expected_error = trim_lines(expected); 325 | let actual_err = trim_lines(&String::from_utf8_lossy(&output.stderr)); 326 | 327 | // Compare actual output to expected output. 328 | if !actual_err.trim_start().starts_with(&expected_error) { 329 | panic!( 330 | "{}", 331 | format_output_error(args, &expected_error, &actual_err) 332 | ); 333 | } 334 | } 335 | 336 | output.status 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /tests/data/config_missing_args.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | "bus-number", 4 | "device-number", 5 | "icon", 6 | "vendor-id", 7 | "product-id", 8 | "name", 9 | "serial", 10 | "speed" 11 | ], 12 | "bus-blocks": [ 13 | "name", 14 | "host-controller" 15 | ], 16 | "config-blocks": [ 17 | "number", 18 | "name", 19 | "icon-attributes", 20 | "max-power" 21 | ], 22 | "interface-blocks": [ 23 | "port-path", 24 | "icon", 25 | "alt-setting", 26 | "class-code", 27 | "sub-class", 28 | "protocol", 29 | "name" 30 | ], 31 | "endpoint-blocks": [ 32 | "number", 33 | "direction", 34 | "transfer-type", 35 | "sync-type", 36 | "usage-type", 37 | "max-packet-size" 38 | ], 39 | "tree": false, 40 | "more": false 41 | } 42 | -------------------------------------------------------------------------------- /tests/data/config_no_theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": [ 3 | "bus-number", 4 | "device-number", 5 | "icon", 6 | "vendor-id", 7 | "product-id", 8 | "name", 9 | "serial", 10 | "speed" 11 | ], 12 | "bus-blocks": [ 13 | "name", 14 | "host-controller" 15 | ], 16 | "config-blocks": [ 17 | "number", 18 | "name", 19 | "icon-attributes", 20 | "max-power" 21 | ], 22 | "interface-blocks": [ 23 | "port-path", 24 | "icon", 25 | "alt-setting", 26 | "class-code", 27 | "sub-class", 28 | "protocol", 29 | "name" 30 | ], 31 | "endpoint-blocks": [ 32 | "number", 33 | "direction", 34 | "transfer-type", 35 | "sync-type", 36 | "usage-type", 37 | "max-packet-size" 38 | ], 39 | "max-variable-string-len": null, 40 | "no-auto-width": false, 41 | "mask-serials": null, 42 | "lsusb": false, 43 | "tree": false, 44 | "verbose": 0, 45 | "more": false, 46 | "hide-buses": false, 47 | "hide-hubs": false, 48 | "decimal": false, 49 | "no-padding": false, 50 | "ascii": false, 51 | "headings": false, 52 | "force-libusb": false 53 | } 54 | -------------------------------------------------------------------------------- /tests/data/cyme_sp_tree_json_dump.json: -------------------------------------------------------------------------------- 1 | { 2 | "buses": [ 3 | { 4 | "name": "Unknown", 5 | "host_controller": "Unknown", 6 | "usb_bus_number": 0, 7 | "devices": [ 8 | { 9 | "name": "4-Port USB 3.0 Hub", 10 | "vendor_id": 3034, 11 | "product_id": 1041, 12 | "location_id": { 13 | "bus": 0, 14 | "tree_positions": [ 15 | 1 16 | ], 17 | "number": 1 18 | }, 19 | "serial_num": "", 20 | "manufacturer": "Generic", 21 | "bcd_device": 1.17, 22 | "bcd_usb": 3.0, 23 | "device_speed": "5.0 Gb/s", 24 | "devices": [ 25 | { 26 | "name": "4-Port USB 3.0 Hub", 27 | "vendor_id": 3034, 28 | "product_id": 1041, 29 | "location_id": { 30 | "bus": 0, 31 | "tree_positions": [ 32 | 1, 33 | 1 34 | ], 35 | "number": 2 36 | }, 37 | "serial_num": "", 38 | "manufacturer": "Generic", 39 | "bcd_device": 1.17, 40 | "bcd_usb": 3.0, 41 | "device_speed": "5.0 Gb/s", 42 | "class": "hub", 43 | "sub_class": 0, 44 | "protocol": 3 45 | } 46 | ], 47 | "class": "hub", 48 | "sub_class": 0, 49 | "protocol": 3 50 | } 51 | ] 52 | }, 53 | { 54 | "name": "Unknown", 55 | "host_controller": "Unknown", 56 | "usb_bus_number": 2, 57 | "devices": [ 58 | { 59 | "name": "USB3.1 Hub", 60 | "vendor_id": 1086, 61 | "product_id": 39520, 62 | "location_id": { 63 | "bus": 2, 64 | "tree_positions": [ 65 | 3 66 | ], 67 | "number": 2 68 | }, 69 | "serial_num": "", 70 | "manufacturer": "LG Electronics Inc.", 71 | "bcd_device": 52.5, 72 | "bcd_usb": 3.1, 73 | "device_speed": "5.0 Gb/s", 74 | "devices": [ 75 | { 76 | "name": "USB3.0 Hub", 77 | "vendor_id": 8457, 78 | "product_id": 2071, 79 | "location_id": { 80 | "bus": 2, 81 | "tree_positions": [ 82 | 3, 83 | 3 84 | ], 85 | "number": 8 86 | }, 87 | "serial_num": "", 88 | "manufacturer": "VIA Labs, Inc.", 89 | "bcd_device": 4.53, 90 | "bcd_usb": 3.1, 91 | "device_speed": "5.0 Gb/s", 92 | "class": "hub", 93 | "sub_class": 0, 94 | "protocol": 3 95 | }, 96 | { 97 | "name": "Extreme SSD", 98 | "vendor_id": 1921, 99 | "product_id": 21900, 100 | "location_id": { 101 | "bus": 2, 102 | "tree_positions": [ 103 | 3, 104 | 2 105 | ], 106 | "number": 6 107 | }, 108 | "serial_num": "323030323247343032303739", 109 | "manufacturer": "SanDisk", 110 | "bcd_device": 10.12, 111 | "bcd_usb": 3.1, 112 | "device_speed": "5.0 Gb/s", 113 | "class": "use-interface-descriptor", 114 | "sub_class": 0, 115 | "protocol": 0 116 | }, 117 | { 118 | "name": "Belkin USB-C LAN", 119 | "vendor_id": 3034, 120 | "product_id": 33107, 121 | "location_id": { 122 | "bus": 2, 123 | "tree_positions": [ 124 | 3, 125 | 1 126 | ], 127 | "number": 5 128 | }, 129 | "serial_num": "B2C1A1000000", 130 | "manufacturer": "Belkin", 131 | "bcd_device": 30.0, 132 | "bcd_usb": 3.0, 133 | "device_speed": "5.0 Gb/s", 134 | "class": "use-interface-descriptor", 135 | "sub_class": 0, 136 | "protocol": 0 137 | }, 138 | { 139 | "name": "", 140 | "vendor_id": 1086, 141 | "product_id": 39537, 142 | "location_id": { 143 | "bus": 2, 144 | "tree_positions": [ 145 | 3, 146 | 4 147 | ], 148 | "number": 4 149 | }, 150 | "serial_num": "", 151 | "manufacturer": "LG Electronics USA, Inc.", 152 | "bcd_device": 1.0, 153 | "bcd_usb": 3.0, 154 | "device_speed": "5.0 Gb/s", 155 | "devices": [ 156 | { 157 | "name": "LG UltraFine Display Camera", 158 | "vendor_id": 1086, 159 | "product_id": 39528, 160 | "location_id": { 161 | "bus": 2, 162 | "tree_positions": [ 163 | 3, 164 | 4, 165 | 3 166 | ], 167 | "number": 9 168 | }, 169 | "serial_num": "", 170 | "manufacturer": "LG Electronlcs Inc.", 171 | "bcd_device": 2.23, 172 | "bcd_usb": 3.0, 173 | "device_speed": "5.0 Gb/s", 174 | "class": "miscellaneous", 175 | "sub_class": 2, 176 | "protocol": 1 177 | } 178 | ], 179 | "class": "hub", 180 | "sub_class": 0, 181 | "protocol": 3 182 | } 183 | ], 184 | "class": "hub", 185 | "sub_class": 0, 186 | "protocol": 3 187 | }, 188 | { 189 | "name": "USB2.1 Hub", 190 | "vendor_id": 1086, 191 | "product_id": 39521, 192 | "location_id": { 193 | "bus": 2, 194 | "tree_positions": [ 195 | 2 196 | ], 197 | "number": 1 198 | }, 199 | "serial_num": "", 200 | "manufacturer": "LG Electronics Inc.", 201 | "bcd_device": 52.5, 202 | "bcd_usb": 2.1, 203 | "device_speed": "480.0 Mb/s", 204 | "devices": [ 205 | { 206 | "name": "USB2.0 Hub", 207 | "vendor_id": 8457, 208 | "product_id": 10263, 209 | "location_id": { 210 | "bus": 2, 211 | "tree_positions": [ 212 | 2, 213 | 3 214 | ], 215 | "number": 7 216 | }, 217 | "serial_num": "", 218 | "manufacturer": "VIA Labs, Inc.", 219 | "bcd_device": 4.53, 220 | "bcd_usb": 2.1, 221 | "device_speed": "480.0 Mb/s", 222 | "devices": [ 223 | { 224 | "name": "USB Billboard Device", 225 | "vendor_id": 8457, 226 | "product_id": 34839, 227 | "location_id": { 228 | "bus": 2, 229 | "tree_positions": [ 230 | 2, 231 | 3, 232 | 5 233 | ], 234 | "number": 12 235 | }, 236 | "serial_num": "0000000000000001", 237 | "manufacturer": "VIA Labs, Inc.", 238 | "bcd_device": 0.01, 239 | "bcd_usb": 2.01, 240 | "device_speed": "480.0 Mb/s", 241 | "class": "application-specific-interface", 242 | "sub_class": 0, 243 | "protocol": 0 244 | } 245 | ], 246 | "class": "hub", 247 | "sub_class": 0, 248 | "protocol": 2 249 | }, 250 | { 251 | "name": "", 252 | "vendor_id": 1086, 253 | "product_id": 39539, 254 | "location_id": { 255 | "bus": 2, 256 | "tree_positions": [ 257 | 2, 258 | 4 259 | ], 260 | "number": 3 261 | }, 262 | "serial_num": "AA0A2871A7D0", 263 | "manufacturer": "LG Electronics USA, Inc.", 264 | "bcd_device": 1.0, 265 | "bcd_usb": 2.1, 266 | "device_speed": "480.0 Mb/s", 267 | "devices": [ 268 | { 269 | "name": "LG UltraFine Display Audio", 270 | "vendor_id": 1086, 271 | "product_id": 39526, 272 | "location_id": { 273 | "bus": 2, 274 | "tree_positions": [ 275 | 2, 276 | 4, 277 | 1 278 | ], 279 | "number": 11 280 | }, 281 | "serial_num": "", 282 | "manufacturer": "LG Electronics Inc.", 283 | "bcd_device": 0.03, 284 | "bcd_usb": 2.0, 285 | "device_speed": "480.0 Mb/s", 286 | "class": "use-interface-descriptor", 287 | "sub_class": 0, 288 | "protocol": 0 289 | }, 290 | { 291 | "name": "LG UltraFine Display Controls", 292 | "vendor_id": 1086, 293 | "product_id": 39536, 294 | "location_id": { 295 | "bus": 2, 296 | "tree_positions": [ 297 | 2, 298 | 4, 299 | 2 300 | ], 301 | "number": 10 302 | }, 303 | "serial_num": "", 304 | "manufacturer": "LG Electronics Inc.", 305 | "bcd_device": 4.13, 306 | "bcd_usb": 2.0, 307 | "device_speed": "12.0 Mb/s", 308 | "class": "use-interface-descriptor", 309 | "sub_class": 0, 310 | "protocol": 0 311 | } 312 | ], 313 | "class": "hub", 314 | "sub_class": 0, 315 | "protocol": 2 316 | } 317 | ], 318 | "class": "hub", 319 | "sub_class": 0, 320 | "protocol": 2 321 | } 322 | ] 323 | }, 324 | { 325 | "name": "Unknown", 326 | "host_controller": "Unknown", 327 | "usb_bus_number": 20, 328 | "devices": [ 329 | { 330 | "name": "4-Port USB 2.0 Hub", 331 | "vendor_id": 3034, 332 | "product_id": 21521, 333 | "location_id": { 334 | "bus": 20, 335 | "tree_positions": [ 336 | 3 337 | ], 338 | "number": 1 339 | }, 340 | "serial_num": "", 341 | "manufacturer": "Generic", 342 | "bcd_device": 1.17, 343 | "bcd_usb": 2.1, 344 | "device_speed": "480.0 Mb/s", 345 | "devices": [ 346 | { 347 | "name": "4-Port USB 2.0 Hub", 348 | "vendor_id": 3034, 349 | "product_id": 21521, 350 | "location_id": { 351 | "bus": 20, 352 | "tree_positions": [ 353 | 3, 354 | 1 355 | ], 356 | "number": 2 357 | }, 358 | "serial_num": "", 359 | "manufacturer": "Generic", 360 | "bcd_device": 1.17, 361 | "bcd_usb": 2.1, 362 | "device_speed": "480.0 Mb/s", 363 | "class": "hub", 364 | "sub_class": 0, 365 | "protocol": 2 366 | } 367 | ], 368 | "class": "hub", 369 | "sub_class": 0, 370 | "protocol": 2 371 | } 372 | ] 373 | } 374 | ] 375 | } 376 | -------------------------------------------------------------------------------- /tests/data/lsusb_list.txt: -------------------------------------------------------------------------------- 1 | Bus 001 Device 003: ID 203a:fffa PARALLELS Virtual Printer (/Users/john/Parallels/Arch.pvm/parallel.txt) 2 | Bus 001 Device 002: ID 203a:fffc PARALLELS Virtual Mouse 3 | Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 4 | Bus 002 Device 022: ID 203a:fffe PARALLELS Virtual USB1.1 HUB 5 | Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 6 | Bus 002 Device 023: ID 1366:1050 SEGGER J-Link 7 | Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 8 | Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 9 | Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub 10 | -------------------------------------------------------------------------------- /tests/data/lsusb_tree.txt: -------------------------------------------------------------------------------- 1 | /: Bus 001.Port 001: Dev 001, Class=root_hub, Driver=hub, 480M 2 | |__ Port 002: Dev 002, If 0, Class=Human Interface Device, Driver=usbhid, 480M 3 | |__ Port 002: Dev 002, If 1, Class=Human Interface Device, Driver=usbhid, 480M 4 | |__ Port 006: Dev 003, If 0, Class=Printer, Driver=usblp, 480M 5 | /: Bus 002.Port 001: Dev 001, Class=root_hub, Driver=hub, 12M 6 | |__ Port 002: Dev 022, If 0, Class=Hub, Driver=hub, 12M 7 | |__ Port 001: Dev 023, If 0, Class=Communications, Driver=cdc_acm, 12M 8 | |__ Port 001: Dev 023, If 1, Class=CDC Data, Driver=cdc_acm, 12M 9 | |__ Port 001: Dev 023, If 2, Class=Communications, Driver=cdc_acm, 12M 10 | |__ Port 001: Dev 023, If 3, Class=CDC Data, Driver=cdc_acm, 12M 11 | |__ Port 001: Dev 023, If 4, Class=Vendor Specific Class, Driver=[none], 12M 12 | |__ Port 008: Dev 024, If 0, Class=Communications, Driver=cdc_acm, 12M 13 | |__ Port 008: Dev 024, If 1, Class=CDC Data, Driver=cdc_acm, 12M 14 | |__ Port 008: Dev 024, If 2, Class=Communications, Driver=cdc_acm, 12M 15 | |__ Port 008: Dev 024, If 3, Class=CDC Data, Driver=cdc_acm, 12M 16 | |__ Port 008: Dev 024, If 4, Class=Application Specific Interface, Driver=[none], 12M 17 | |__ Port 008: Dev 024, If 5, Class=Vendor Specific Class, Driver=[none], 12M 18 | /: Bus 003.Port 001: Dev 001, Class=root_hub, Driver=hub, 480M 19 | /: Bus 004.Port 001: Dev 001, Class=root_hub, Driver=hub, 10000M 20 | -------------------------------------------------------------------------------- /tests/data/lsusb_tree_verbose.txt: -------------------------------------------------------------------------------- 1 | /: Bus 001.Port 001: Dev 001, Class=root_hub, Driver=hub, 480M 2 | ID 1d6b:0002 Linux Foundation 2.0 root hub 3 | /sys/bus/usb/devices/usb1 /dev/bus/usb/001/001 4 | |__ Port 002: Dev 002, If 0, Class=Human Interface Device, Driver=usbhid, 480M 5 | ID 203a:fffc PARALLELS [unknown] 6 | /sys/bus/usb/devices/1-2 /dev/bus/usb/001/002 7 | Manufactuer=Virtual Mouse Product=Parallels Serial=PW3.0 8 | |__ Port 002: Dev 002, If 1, Class=Human Interface Device, Driver=usbhid, 480M 9 | ID 203a:fffc PARALLELS [unknown] 10 | /sys/bus/usb/devices/1-2 /dev/bus/usb/001/002 11 | Manufactuer=Virtual Mouse Product=Parallels Serial=PW3.0 12 | |__ Port 006: Dev 003, If 0, Class=Printer, Driver=usblp, 480M 13 | ID 203a:fffa PARALLELS [unknown] 14 | /sys/bus/usb/devices/1-6 /dev/bus/usb/001/003 15 | Manufactuer=Virtual Printer (/Users/john/Parallels/Arch.pvm/parallel.txt) Product=Parallels Serial=SN0000 16 | /: Bus 002.Port 001: Dev 001, Class=root_hub, Driver=hub, 12M 17 | ID 1d6b:0001 Linux Foundation 1.1 root hub 18 | /sys/bus/usb/devices/usb2 /dev/bus/usb/002/001 19 | |__ Port 002: Dev 022, If 0, Class=Hub, Driver=hub, 12M 20 | ID 203a:fffe PARALLELS [unknown] 21 | /sys/bus/usb/devices/2-2 /dev/bus/usb/002/022 22 | Manufactuer=Virtual USB1.1 HUB Product=Parallels Serial=PW3.0 23 | |__ Port 001: Dev 023, If 0, Class=Communications, Driver=cdc_acm, 12M 24 | ID 1366:1050 SEGGER [unknown] 25 | /sys/bus/usb/devices/2-2.1 /dev/bus/usb/002/023 26 | Manufactuer=J-Link Product=SEGGER Serial=001050027328 27 | |__ Port 001: Dev 023, If 1, Class=CDC Data, Driver=cdc_acm, 12M 28 | ID 1366:1050 SEGGER [unknown] 29 | /sys/bus/usb/devices/2-2.1 /dev/bus/usb/002/023 30 | Manufactuer=J-Link Product=SEGGER Serial=001050027328 31 | |__ Port 001: Dev 023, If 2, Class=Communications, Driver=cdc_acm, 12M 32 | ID 1366:1050 SEGGER [unknown] 33 | /sys/bus/usb/devices/2-2.1 /dev/bus/usb/002/023 34 | Manufactuer=J-Link Product=SEGGER Serial=001050027328 35 | |__ Port 001: Dev 023, If 3, Class=CDC Data, Driver=cdc_acm, 12M 36 | ID 1366:1050 SEGGER [unknown] 37 | /sys/bus/usb/devices/2-2.1 /dev/bus/usb/002/023 38 | Manufactuer=J-Link Product=SEGGER Serial=001050027328 39 | |__ Port 001: Dev 023, If 4, Class=Vendor Specific Class, Driver=[none], 12M 40 | ID 1366:1050 SEGGER [unknown] 41 | /sys/bus/usb/devices/2-2.1 /dev/bus/usb/002/023 42 | Manufactuer=J-Link Product=SEGGER Serial=001050027328 43 | |__ Port 008: Dev 024, If 0, Class=Communications, Driver=cdc_acm, 12M 44 | ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 45 | /sys/bus/usb/devices/2-2.8 /dev/bus/usb/002/024 46 | Manufactuer=Black Magic Probe v1.8.2 Product=Black Magic Debug Serial=97B6A11D 47 | |__ Port 008: Dev 024, If 1, Class=CDC Data, Driver=cdc_acm, 12M 48 | ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 49 | /sys/bus/usb/devices/2-2.8 /dev/bus/usb/002/024 50 | Manufactuer=Black Magic Probe v1.8.2 Product=Black Magic Debug Serial=97B6A11D 51 | |__ Port 008: Dev 024, If 2, Class=Communications, Driver=cdc_acm, 12M 52 | ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 53 | /sys/bus/usb/devices/2-2.8 /dev/bus/usb/002/024 54 | Manufactuer=Black Magic Probe v1.8.2 Product=Black Magic Debug Serial=97B6A11D 55 | |__ Port 008: Dev 024, If 3, Class=CDC Data, Driver=cdc_acm, 12M 56 | ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 57 | /sys/bus/usb/devices/2-2.8 /dev/bus/usb/002/024 58 | Manufactuer=Black Magic Probe v1.8.2 Product=Black Magic Debug Serial=97B6A11D 59 | |__ Port 008: Dev 024, If 4, Class=Application Specific Interface, Driver=[none], 12M 60 | ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 61 | /sys/bus/usb/devices/2-2.8 /dev/bus/usb/002/024 62 | Manufactuer=Black Magic Probe v1.8.2 Product=Black Magic Debug Serial=97B6A11D 63 | |__ Port 008: Dev 024, If 5, Class=Vendor Specific Class, Driver=[none], 12M 64 | ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 65 | /sys/bus/usb/devices/2-2.8 /dev/bus/usb/002/024 66 | Manufactuer=Black Magic Probe v1.8.2 Product=Black Magic Debug Serial=97B6A11D 67 | /: Bus 003.Port 001: Dev 001, Class=root_hub, Driver=hub, 480M 68 | ID 1d6b:0002 Linux Foundation 2.0 root hub 69 | /sys/bus/usb/devices/usb3 /dev/bus/usb/003/001 70 | /: Bus 004.Port 001: Dev 001, Class=root_hub, Driver=hub, 10000M 71 | ID 1d6b:0003 Linux Foundation 3.0 root hub 72 | /sys/bus/usb/devices/usb4 /dev/bus/usb/004/001 73 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | //! These test cyme CLI by reading from json but also output as json so that we can check without worrying about formatting 2 | //! 3 | //! It is slightly the dog wagging the tail but is as integration as it gets! Could improve by adding some tests for actual format like --block, --padding args etc 4 | mod common; 5 | 6 | #[test] 7 | fn test_run() { 8 | let te = common::TestEnv::new(); 9 | 10 | // just run and check it doesn't exit with error without --from-json arg 11 | te.assert_success_and_get_output(None, &[]); 12 | } 13 | 14 | #[test] 15 | #[cfg(target_os = "macos")] 16 | fn test_run_force_libusb() { 17 | let te = common::TestEnv::new(); 18 | 19 | // just run and check it doesn't exit with error without --from-json arg 20 | te.assert_success_and_get_output(None, &["--force-libusb"]); 21 | } 22 | 23 | #[test] 24 | fn test_list() { 25 | let te = common::TestEnv::new(); 26 | 27 | let mut comp_sp = common::sp_data_from_libusb_linux(); 28 | comp_sp.into_flattened(); 29 | let devices = comp_sp.flattened_devices(); 30 | let comp = serde_json::to_string_pretty(&devices).unwrap(); 31 | 32 | // TODO not sure why assert_output_json doesn't work, might help to have module which shows diff 33 | te.assert_output( 34 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 35 | &["--json"], 36 | &comp, 37 | false, 38 | ); 39 | } 40 | 41 | #[test] 42 | fn test_list_filtering() { 43 | let te = common::TestEnv::new(); 44 | 45 | let mut comp_sp = common::sp_data_from_libusb_linux(); 46 | let filter = cyme::profiler::Filter { 47 | name: Some("Black Magic".into()), 48 | no_exclude_root_hub: true, 49 | ..Default::default() 50 | }; 51 | comp_sp.into_flattened(); 52 | let mut devices = comp_sp.flattened_devices(); 53 | filter.retain_flattened_devices_ref(&mut devices); 54 | let comp = serde_json::to_string_pretty(&devices).unwrap(); 55 | 56 | te.assert_output( 57 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 58 | &["--json", "--filter-name", "Black Magic"], 59 | &comp, 60 | false, 61 | ); 62 | 63 | te.assert_output( 64 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 65 | &["--json", "--vidpid", "1d50"], 66 | &comp, 67 | false, 68 | ); 69 | 70 | te.assert_output( 71 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 72 | &["--json", "--vidpid", "1d50:6018"], 73 | &comp, 74 | false, 75 | ); 76 | 77 | te.assert_failure( 78 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 79 | &["--json", "--vidpid", "1d50:unhappy"], 80 | ); 81 | 82 | te.assert_output( 83 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 84 | &["--json", "--filter-serial", "97B6A11D"], 85 | &comp, 86 | false, 87 | ); 88 | 89 | let mut comp_sp = common::sp_data_from_libusb_linux(); 90 | let mut filter = cyme::profiler::Filter { 91 | bus: Some(2), 92 | no_exclude_root_hub: true, 93 | ..Default::default() 94 | }; 95 | comp_sp.into_flattened(); 96 | let mut devices = comp_sp.flattened_devices(); 97 | filter.retain_flattened_devices_ref(&mut devices); 98 | let comp = serde_json::to_string_pretty(&devices).unwrap(); 99 | 100 | te.assert_output( 101 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 102 | &["--json", "--show", "2:"], 103 | &comp, 104 | false, 105 | ); 106 | 107 | te.assert_failure( 108 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 109 | &["--json", "--show", "f"], 110 | ); 111 | 112 | filter.number = Some(23); 113 | filter.retain_flattened_devices_ref(&mut devices); 114 | let comp = serde_json::to_string_pretty(&devices).unwrap(); 115 | 116 | te.assert_output( 117 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 118 | &["--json", "--show", "2:23"], 119 | &comp, 120 | false, 121 | ); 122 | 123 | te.assert_failure( 124 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 125 | &["--json", "--show", "blah"], 126 | ); 127 | } 128 | 129 | #[test] 130 | // windows line ending messes this up 131 | #[cfg(not(target_os = "windows"))] 132 | fn test_tree() { 133 | let te = common::TestEnv::new(); 134 | 135 | let comp = common::read_dump_to_string(common::CYME_LIBUSB_LINUX_TREE_DUMP); 136 | 137 | te.assert_output_json( 138 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 139 | &["--json", "--tree"], 140 | &comp, 141 | ); 142 | } 143 | 144 | #[test] 145 | fn test_tree_filtering() { 146 | let te = common::TestEnv::new(); 147 | 148 | let mut comp_sp = common::sp_data_from_libusb_linux(); 149 | let filter = cyme::profiler::Filter { 150 | name: Some("Black Magic".into()), 151 | ..Default::default() 152 | }; 153 | filter.retain_buses(&mut comp_sp.buses); 154 | let comp = serde_json::to_string_pretty(&comp_sp).unwrap(); 155 | 156 | te.assert_output( 157 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 158 | &["--json", "--tree", "--vidpid", "1d50"], 159 | &comp, 160 | false, 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /tests/integration_test_lsusb_display.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | /// Tests lsusb with no args compatibility mode 4 | #[test] 5 | fn test_lsusb_list() { 6 | let te = common::TestEnv::new(); 7 | 8 | let comp = common::read_dump_to_string(common::LSUSB_OUTPUT); 9 | 10 | te.assert_output( 11 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 12 | &["--lsusb"], 13 | comp.as_str(), 14 | false, 15 | ); 16 | } 17 | 18 | /// Tests lsusb --tree compatibility mode 19 | #[test] 20 | fn test_lsusb_tree() { 21 | let te = common::TestEnv::new(); 22 | 23 | let comp = common::read_dump_to_string(common::LSUSB_TREE_OUTPUT); 24 | 25 | te.assert_output( 26 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 27 | &["--lsusb", "--tree"], 28 | comp.as_str(), 29 | false, 30 | ); 31 | } 32 | 33 | /// Tests lsusb --tree fully verbose compatibility mode 34 | #[test] 35 | fn test_lsusb_tree_verbose() { 36 | let te = common::TestEnv::new(); 37 | 38 | let comp = common::read_dump_to_string(common::LSUSB_TREE_OUTPUT_VERBOSE); 39 | 40 | te.assert_output( 41 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 42 | &["--lsusb", "--tree", "-vvv"], 43 | comp.as_str(), 44 | false, 45 | ); 46 | } 47 | 48 | /// Tests lsusb -d vidpid filter 49 | #[test] 50 | fn test_lsusb_vidpid() { 51 | let te = common::TestEnv::new(); 52 | 53 | te.assert_output( 54 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 55 | &["--lsusb", "--vidpid", "1d50"], 56 | "Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application)", 57 | false, 58 | ); 59 | te.assert_output( 60 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 61 | &["--lsusb", "--vidpid", "1d50:"], 62 | "Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application)", 63 | false, 64 | ); 65 | te.assert_output( 66 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 67 | &["--lsusb", "--vidpid", "1d50:6018"], 68 | "Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application)", 69 | false, 70 | ); 71 | te.assert_output( 72 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 73 | &["--lsusb", "--vidpid", "1d6b:"], 74 | r#"Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 75 | Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 76 | Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 77 | Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub"#, 78 | true, 79 | ); 80 | te.assert_failure( 81 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 82 | &["--lsusb", "--vidpid", "dfgdfg"], 83 | ); 84 | } 85 | 86 | /// Tests lsusb -s bus:devno filter 87 | #[test] 88 | fn test_lsusb_show() { 89 | let te = common::TestEnv::new(); 90 | 91 | te.assert_output( 92 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 93 | &["--lsusb", "--show", "24"], 94 | "Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application)", 95 | false, 96 | ); 97 | te.assert_output( 98 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 99 | &["--lsusb", "--show", "2:24"], 100 | "Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application)", 101 | false, 102 | ); 103 | te.assert_output( 104 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 105 | &["--lsusb", "--show", "2:"], 106 | r#"Bus 002 Device 022: ID 203a:fffe PARALLELS Virtual USB1.1 HUB 107 | Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application) 108 | Bus 002 Device 023: ID 1366:1050 SEGGER J-Link 109 | Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub"#, 110 | false, 111 | ); 112 | te.assert_failure( 113 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 114 | &["--lsusb", "--show", "d"], 115 | ); 116 | } 117 | 118 | /// Only tests contains first line...full verbose is not exactly the same but too difficult to match! 119 | #[test] 120 | fn test_lsusb_device() { 121 | let te = common::TestEnv::new(); 122 | 123 | te.assert_output( 124 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 125 | &["--lsusb", "--device", "/dev/bus/usb/002/024"], 126 | "Bus 002 Device 024: ID 1d50:6018 OpenMoko, Inc. Black Magic Debug Probe (Application)", 127 | true, 128 | ); 129 | te.assert_output( 130 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 131 | &["--lsusb", "--device", "/dev/bus/usb/002/001"], 132 | "Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub", 133 | true, 134 | ); 135 | te.assert_failure( 136 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 137 | &["--lsusb", "--device", "/dev/blah/002/001"], 138 | ); 139 | te.assert_failure( 140 | Some(common::CYME_LIBUSB_LINUX_TREE_DUMP), 141 | &["--lsusb", "--device", "/dev/bus/usb/002"], 142 | ); 143 | } 144 | --------------------------------------------------------------------------------