pax_global_header00006660000000000000000000000064151736442240014522gustar00rootroot0000000000000052 comment=75be11e70f8dcdd44e6390031f2b87d01fab85af tbtools-0.8.0+git20260510.75be11e/000077500000000000000000000000001517364422400156645ustar00rootroot00000000000000tbtools-0.8.0+git20260510.75be11e/.gitignore000066400000000000000000000000101517364422400176430ustar00rootroot00000000000000/target tbtools-0.8.0+git20260510.75be11e/CODE_OF_CONDUCT.md000066400000000000000000000124761517364422400204750ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at CommunityCodeOfConduct AT intel DOT com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq tbtools-0.8.0+git20260510.75be11e/CONTRIBUTING.md000066400000000000000000000043271517364422400201230ustar00rootroot00000000000000# Contributing ### License tbtools is licensed under the terms in MIT. By contributing to the project, you agree to the license and copyright terms therein and release your contribution under these terms. ### Sign your work Please use the sign-off line at the end of the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: Signed-off-by: Joe Smith Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. tbtools-0.8.0+git20260510.75be11e/Cargo.lock000066400000000000000000001021051517364422400175700ustar00rootroot00000000000000# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.61.2", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "castaway" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compact_str" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ "castaway", "cfg-if", "itoa", "rustversion", "ryu", "static_assertions", ] [[package]] name = "crossbeam-channel" version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags", "crossterm_winapi", "mio", "parking_lot", "rustix", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "csv" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", "serde_core", ] [[package]] name = "csv-core" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] [[package]] name = "cursive" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "386d5a36020bb856e9a34ecb8a4e6c9bd6b0262d1857bae4db7bc7e2fdaa532e" dependencies = [ "ahash", "cfg-if", "crossbeam-channel", "crossterm", "cursive_core", "lazy_static", "libc", "log", "signal-hook", "unicode-segmentation", "unicode-width", ] [[package]] name = "cursive-macros" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac7ac0eb0cede3dfdfebf4d5f22354e05a730b79c25fd03481fc69fcfba0a73e" dependencies = [ "proc-macro2", ] [[package]] name = "cursive_core" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321ec774d27fafc66e812034d0025f8858bd7d9095304ff8fc200e0b9f9cc257" dependencies = [ "ahash", "compact_str", "crossbeam-channel", "cursive-macros", "enum-map", "enumset", "lazy_static", "log", "num", "parking_lot", "serde_json", "time", "unicode-segmentation", "unicode-width", "xi-unicode", ] [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "syn", ] [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", "syn", ] [[package]] name = "deranged" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "enum-map" version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" dependencies = [ "enum-map-derive", ] [[package]] name = "enum-map-derive" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "enumset" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" dependencies = [ "darling", "proc-macro2", "quote", "syn", ] [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "include_dir" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" dependencies = [ "glob", "include_dir_macros", ] [[package]] name = "include_dir_macros" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ "proc-macro2", "quote", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.181" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libudev-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" dependencies = [ "libc", "pkg-config", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi", "windows-sys 0.61.2", ] [[package]] name = "nix" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-complex", "num-integer", "num-iter", "num-rational", "num-traits", ] [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "signal-hook" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ "errno", "libc", ] [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tbtools" version = "0.8.0" dependencies = [ "ansi_term", "clap", "csv", "cursive", "include_dir", "lazy_static", "nix", "num-traits", "rand", "regex", "serde", "serde_json", "udev", "uuid", ] [[package]] name = "time" version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", ] [[package]] name = "udev" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" dependencies = [ "io-lifetimes", "libc", "libudev-sys", "pkg-config", ] [[package]] name = "unicode-ident" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "xi-unicode" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" [[package]] name = "zerocopy" version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" tbtools-0.8.0+git20260510.75be11e/Cargo.toml000066400000000000000000000010721517364422400176140ustar00rootroot00000000000000[package] name = "tbtools" version = "0.8.0" edition = "2024" [dependencies] ansi_term = "0.12" clap = { version = "4.0", features = ["derive"] } csv = "1.3.0" cursive = { version = "0.21.1", default-features = false, features = ["crossterm-backend"] } include_dir = { version = "0.7.3", features = ["glob"] } lazy_static = "1.4.0" nix = { version = "0.31.2", features = ["ioctl", "mount", "poll", "time", "user"] } num-traits = "0.2.15" rand = "0.9.3" regex = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.108" udev = "0.9.0" uuid = "1.7.0" tbtools-0.8.0+git20260510.75be11e/LICENSE000066400000000000000000000020511517364422400166670ustar00rootroot00000000000000Copyright 2024 Intel Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tbtools-0.8.0+git20260510.75be11e/Makefile000066400000000000000000000034461517364422400173330ustar00rootroot00000000000000# Thunderbolt/USB4 debug tools # Copyright (C) 2023, Intel Corporation CARGO = cargo INSTALL = install LN = ln RM = rm RMDIR = rmdir # Release build, uncomment for debug build #CFLAGS = #IFLAGS = --debug CFLAGS = -r IFLAGS = # For buildroot, override $PREFIX if using something else BR_HOME ?= $(HOME)/devel/buildroot PREFIX ?= $(BR_HOME)/output/target/usr TOOLS = tbadapters tbauth tbdump tbget tblist tbmargin tbmonitor tbset tbtunnels tbtrace SCRIPTS = nvm-version.sh pcie-downstream-mapping.sh reset-port.sh tb-bandwidth.sh build: $(CARGO) build $(CFLAGS) run: $(CARGO) run $(CFLAGS) install-scripts: $(INSTALL) -d $(PREFIX)/share/tbtools/scripts $(foreach script, $(SCRIPTS), $(INSTALL) -m 0755 scripts/$(script) $(PREFIX)/share/tbtools/scripts/$(script);) uninstall-scripts: $(foreach script, $(SCRIPTS), $(RM) -f $(PREFIX)/share/tbtools/scripts/$(script);) -$(RMDIR) $(PREFIX)/share/tbtools/scripts install-completion: $(INSTALL) -d $(PREFIX)/share/bash-completion/completions $(INSTALL) -m 0644 scripts/tbtools-completion.bash $(PREFIX)/share/bash-completion/completions $(foreach tool, $(TOOLS), $(LN) -sf tbtools-completion.bash $(PREFIX)/share/bash-completion/completions/$(tool);) uninstall-completion: $(foreach tool, $(TOOLS), $(RM) -f $(PREFIX)/share/bash-completion/completions/$(tool);) $(RM) -f $(PREFIX)/share/bash-completion/completions/tbtools-completion.bash install-binaries: $(CARGO) install $(IFLAGS) --path . --root $(PREFIX) # Create convenient lstb symlink as well $(LN) -sf tblist $(PREFIX)/bin/lstb uninstall-binaries: $(CARGO) uninstall --root $(PREFIX) $(RM) -f $(PREFIX)/bin/lstb install: install-binaries install-completion install-scripts uninstall: uninstall-scripts uninstall-completion uninstall-binaries clean: $(CARGO) clean .PHONY: build run install clean tbtools-0.8.0+git20260510.75be11e/README.md000066400000000000000000000251071517364422400171500ustar00rootroot00000000000000# Thunderbolt/USB4 debugging tools This is a collection of tools for Linux Thunderbolt/USB4 development, debugging and validation but may be useful to others as well. This consists of a library (`tbtools`) that can be used as standalone for a custom tools, and a basic set of debugging tools built on top of this library. Following tools are currently available: `tblist` : Lists all the Thunderbolt/USB4 devices, including retimers. `tbauth` : Authorizes and De-authorizes (if supported) devices. `tbadapters` : Displays all adapters of given router. `tbdump` : Reads registers from various config spaces. `tbget` : Read specified registers from various config spaces. This is similar to `tbdump` but more suitable for scripting. `tbset` : Write various config spaces. In order to write registers the kernel must have `CONFIG_USB4_DEBUGFS_WRITE=y` set. `tbmargin` : Run receiver lane margining on given USB4 port. This requires that the kernel is build with `CONFIG_USB4_DEBUGFS_MARGINING=y` set. `tbmonitor` : Command line monitor for events in the Thunderbolt/USB4 domains. `tbpd` : Type-C port power delivery control. Currently only supports ChromeOS style through their embedded controller. To use this on Chromebooks you need to enable `CONFIG_CROS_EC_CHARDEV` in the kernel config. `tbtrace` : Helper utility for tracing the control channel traffic. This requires that the kernel driver has tracepoint support. If you need to enable tracing already from boot, you can pass `trace_event=thunderbolt:*` in the kernel command line. All this also requires that you have `CONFIG_TRACING=y` in your kernel config. `tbtunnels` : Utility that dumps all tunnels in the domain. `tbman` : Live device manager tool. This can be used to view the device topology in more visual manner. This too requires `debugfs` access but mounts it automatically (which implies that the user needs to have `root` privileges). ![tbman in action](src/bin/tbman/tbman.png) ## Building The tools are written in Rust so you need to have Rust build tools installed. Check from [Rust home page](https://www.rust-lang.org/tools/install) how to install it if not already installed. You can the build the tools by running ``` $ cargo build ``` If you don't need all the tools you can build just the binaries you want by passing `--bin` option to cargo: ``` $ cargo build --bin tblist --bin tbdump ``` ## Installing You can also install the tools to the default Cargo directory by running ``` $ cargo install --path . ``` Selected binaries can be installed using `--bin` option similar than with `cargo build` above. ## Uninstalling To uninstall the binaries, run ``` $ cargo uninstall ``` ## Buildroot You can enable tbtools in [buildroot](https://github.com/andy-shev/buildroot) by setting `BR2_PACKAGE_TBTOOLS=y` (or from nconfig select `Target Packages` → `Debugging, profiling and benchmark` → `tbtools`). ## Addressing All the tools that take device (router, host, retimer) address use similar addressing than the kernel. For example a second depth router (or host) as in the below kernel device name: ``` 0-301 ^ ^ | | | +---- route == 0x301 +------- domain == 0 ``` Can be addressed in the tools such as `tbauth` like: ``` # tbauth -d 0 -r 301 ``` And a retimer such as this: ``` 0-0:3.1 ^ ^ ^ ^ | | | | | | | +- index == 1 | | +--- adapter == 3 | +----- route == 0x0 +------- domain == 0 ``` Can be addressed in a tool that accepts retimers such as `tbmargin` using following command line: ``` # tbmargin -d 0 -r 0 -a 3 -i 1 ``` Any adapter can be addressed in the same way. For example host router (kernel name `0-0`) lane adapter `1` can be addressed like below (command dumps 10 first double words of the adapter config space): ``` # tbdump -d 0 -r 0 -a 1 -N 10 0 ``` ## Named registers The library currently knows the names of registers in USB4 spec and these are available in tools such as `tbdump`, `tbget` and `tbset`. For instance reading `PORT_CS_18` register of downstream port of the host router can also be done like this: ``` # tbdump -r 0 -N 1 -a 3 PORT_CS_18 -vv 0x00a2 0x00000410 0b00000000 00000000 00000100 00010000 PORT_CS_18 [00:07] 0x10 Cable USB4 Version [08:08] 0x0 Bonding Enabled (BE) [09:09] 0x0 TBT3-Compatible Mode (TCM) [10:10] 0x1 CLx Protocol Support (CPS) [11:11] 0x0 RS-FEC Enabled (Gen 2) (RE2) [12:12] 0x0 RS-FEC Enabled (Gen 3) (RE3) [13:13] 0x0 Router Detected (RD) [16:16] 0x0 Wake on Connect Status [17:17] 0x0 Wake on Disconnect Status [18:18] 0x0 Wake on USB4 Wake Status [19:19] 0x0 Wake on Inter-Domain Status [20:20] 0x0 Cable Gen 3 Support (CG3) [21:21] 0x0 Cable Gen 4 Support (CG4) [22:22] 0x0 Cable Asymmetric Support (CSA) [23:23] 0x0 Cable CLx Support (CSC) [24:24] 0x0 AsymmetricTransitionInProgress (TIP) ``` For a accessing single fields, `tbget` and `tbset` can be used instead. They accept the long names and the short ones. The below commands read the same field in `PORT_CS_18` register: ``` # tbget -r 0 -a 1 "PORT_CS_18.Cable Asymmetric Support" 0x0 # tbget -r 0 -a 1 PORT_CS_18.CSA 0x0 ``` For write side, `tbset` allows to write register fields using their names, for example: ``` # tbset -r 0 -a 1 PORT_CS_19.DPR=1 ``` The currently known registers can be seen in [registers.json](src/data/registers.json). ## Vendor defined registers The USB4 specification supports VSC (Vendor Specific Capability) and VSEC (Vendor Specific Extended Capability) registers. Some of these have generic placeholder name in the spec and the tool but vendors can extend this if needed. The library supports optional `src/data/VENDOR-registers.json` that can be used to amend (if generic spec name exists) or add (if generic name does not exist) the register metadata. The format is exactly the same as in `registers.json` but there is an additional field `vendor_id` that is an array of decimal vendor IDs (this is matched with the `ROUTER_CS_0.Vendor ID` field), such as in below example: ``` {"offset": 0, "vendor_id": [32902, 32903], "cap_id": 5, "vs_cap_id": 1, "name": "VENDOR_REG_0"}, {"offset": 1, "vendor_id": [32902, 32903], "cap_id": 5, "vs_cap_id": 1, "name": "VENDOR_REG_1"}, ... ``` ## Scripting Some of the tools, such as `tblist` and `tbadapters` accept `-S` option. This turns the output into [CSV](https://datatracker.ietf.org/doc/html/rfc4180) format that should be more suitable for parsing in scripts. For example: ``` # tblist -S domain,route,adapter,index,vendor,device,vendor_name,device_name,type 0,0,,,7eb2,8087,Intel,Gen14,Router 0,1,,,1234,8087,Intel Thunderbolt generic vendor name,Intel Thunderbolt generic model name,Router 1,0,,,7eb2,8087,Intel,Gen14,Router ``` The field ordering is guaranteed not to change but it is possible that more fields will be added in the future at the end. ## Bash command line completion Bash command line completion is built on top of [bash-completion](https://github.com/scop/bash-completion/) package. Once enabled it will try to complete the device address (`domain`, `route`, and `adapter`) and also known register names. The [Makefile](Makefile) has `install-completion` target that installs the completion script and creates symlinks for each tool accordingly. ## Debug logs In addition to these the kernel driver logs useful information when debug logging is enabled. You can do this either by passing `thunderbolt.dyndbg=+p` in the kernel command line, or runtime by: ``` # echo -n 'module thunderbolt =p' > /sys/kernel/debug/dynamic_debug/control ``` The logs can be read by running `dmesg` command. The kernel needs to have `CONFIG_DYNAMIC_DEBUG=y` set. ## Tracing Recent addition to the kernel driver allows tracing of the transport layer control packets. There is a tool `tbtrace` that can be used to control this. First you enable tracing: ``` # tbtrace enable ``` Then do whatever you want to trace. Once you are done you can stop the tracing: ``` # tbtrace disable ``` You can then dump the tracing buffer in parsed format. If you run against live system you can ask more details such as registers being accessed and their contents. Note it is recommended that you don't have tracing enabled at this point if you use `-vv` because this causes the tool itself to perform lots of register accesses that fill up the trace buffer as well. ``` # tbtrace dump -vv [ 186.463000] tb_tx Read Request Domain 0 Route 0 Adapter 9 0x0000 0x00000000 0b00000000 00000000 00000000 00000000 .... Route String High 0x0001 0x00000000 0b00000000 00000000 00000000 00000000 .... Route String Low 0x0002 0x02482039 0b00000010 01001000 00100000 00111001 .H.9 [00:12] 0x39 Address [13:18] 0x1 Read Size [19:24] 0x9 Adapter Num [25:26] 0x1 Configuration Space (CS) → Adapter Configuration Space [27:28] 0x0 Sequence Number (SN) [ 186.463359] tb_rx Read Response Domain 0 Route 0 Adapter 9 0x0000 0x80000000 0b10000000 00000000 00000000 00000000 .... Route String High 0x0001 0x00000000 0b00000000 00000000 00000000 00000000 .... Route String Low 0x0002 0x02482039 0b00000010 01001000 00100000 00111001 .H.9 [00:12] 0x39 Address [13:18] 0x1 Read Size [19:24] 0x9 Adapter Num [25:26] 0x1 Configuration Space (CS) → Adapter Configuration Space [27:28] 0x0 Sequence Number (SN) 0x0003 0x2186043a 0b00100001 10000110 00000100 00111010 .... 0x0039 ADP_PCIE_CS_0 [00:07] 0x3a Next Capability Pointer [08:15] 0x4 Capability ID [16:16] 0x0 Link [17:17] 0x1 TX EI [18:18] 0x1 RX EI [19:19] 0x0 RST [25:28] 0x0 LTSSM [31:31] 0x0 Path Enable (PE) ... ``` `tbman` supports tracing too. You can press <F3> to enable and disable tracing and <F4> to view the current trace buffer. ## Development Please make sure following commands pass without warnings before submitting patches: ``` $ cargo fmt $ cargo test $ cargo clippy ``` ## Documentation Documentation of the library itself can be generated by running `cargo doc`. The documentation is accessible through `target/doc/tbtools/index.html`. tbtools-0.8.0+git20260510.75be11e/SECURITY.md000066400000000000000000000006261517364422400174610ustar00rootroot00000000000000# Security Policy Intel is committed to rapidly addressing security vulnerabilities affecting our customers and providing clear guidance on the solution, impact, severity and mitigation. ## Reporting a Vulnerability Please report any security vulnerabilities in this project utilizing the guidelines [here](https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html). tbtools-0.8.0+git20260510.75be11e/TODO000066400000000000000000000013661517364422400163620ustar00rootroot00000000000000tbtools ------- - tbmand - a "web server" version of tbman (REST API or so). Allows more complex graphics for device topology etc through HTML5 but still should work on minimal userspace (for example buildroot) - sideband register support - DMA test (dma_test.ko) support. Probably some sort of script that runs it automatically or so. - snapshot. E.g take snapshot of the debugfs files and then add allow specifying that for any of the tools. tbget/tbdump ----- - Move --query and --script to tbdump instead (update the bash completion to use tbdump as well). tbman ----- - full tunnel viewer + DP (bw management) stuff - NVM access - log viewer (for example show logs for this device) - lane margining - filtering to tracing tbtools-0.8.0+git20260510.75be11e/scripts/000077500000000000000000000000001517364422400173535ustar00rootroot00000000000000tbtools-0.8.0+git20260510.75be11e/scripts/alloc-bw.sh000077500000000000000000000010641517364422400214130ustar00rootroot00000000000000#!/bin/sh -e if [ $# -lt 2 ]; then echo "Usage: alloc-bw.sh AUX BANDWIDTH" echo echo "Allocates BANDWIDTH (in Mbit/s) through USB4 DPCD registers." exit 1 fi AUX=$1 GR=$(dd if=/dev/drm_dp_aux${AUX} bs=1 skip=$((0xe0022)) count=1 2> /dev/null | od -An -tx1 | sed 's/^[ \t]*//') case $GR in 00) GR=250 ;; 01) GR=500 ;; 02) GR=1000 ;; *) echo "Error: unsupported granularity: $GR" 1>&2 exit 1 ;; esac BW=$(($2 / $GR)) OBW=$(printf "\%o" $BW) printf "%b" "$OBW" | dd of=/dev/drm_dp_aux${AUX} bs=1 count=1 seek=$((0xe0031)) 2> /dev/null tbtools-0.8.0+git20260510.75be11e/scripts/disable-bw.sh000077500000000000000000000005011517364422400217170ustar00rootroot00000000000000#!/bin/sh if [ $# -ne 1 ]; then echo "Usage: disable-bw.sh AUX" echo echo "Disables USB4 bandwidth allocation mode from DPCD side." exit 1 fi AUX=$1 VAL=0x00 OVAL=$(printf "\%o" $VAL) printf "%b" "$OVAL" | dd of=/dev/drm_dp_aux${AUX} bs=1 count=1 seek=$((0xe0030)) 2> /dev/null echo "BW allocation mode disabled" tbtools-0.8.0+git20260510.75be11e/scripts/dp-suspend-resume.sh000077500000000000000000001077271517364422400233100ustar00rootroot00000000000000#!/bin/bash set -ue shopt -s extglob shopt -s expand_aliases CONFIG_FILE=.config TBTEST_DIR=$(dirname $0) . $TBTEST_DIR/utils.sh . $TBTEST_DIR/type-utils.sh . $TBTEST_DIR/tbtool-db.sh . $TBTEST_DIR/tools.sh . $TBTEST_DIR/trace.sh # enable_debug /tmp/tbtest-debug.log AUTORESUME_DELAY_SEC=2 # delay after a suspend until system is autoresumed TEST_CYCLE_DELAY_SEC=30 # delay after resume until the next suspend/resume cycle MAX_PING_DURATION_SEC=30 # maximum duration while waiting for a responsive network connection MAX_TEST_CMD_RETRY_ATTEMPTS=10 # max number test commands are retried, in case they need this MAX_REC_SCAN=300 FILTERED_DMESG_ERRORS="^done\.$" # error/warning messages filtered out declare -r ERR_STATS_CHANGED=243 declare -r TBT_DP_IN_ADAPTERS_CONF_NAME=TBT_DP_IN_ADAPTERS TBT_DP_IN_ADAPTERS=() # list of preconfigured (D:R:A [D:R:A ...]) enabled DP IN # adapters to test. If unset the adapters will be detected. [ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE" epoch_delta=0 # delta between epoch and time since script start declare -ra required_tools=( TBGET TBADAPTERS TBLIST DMESG AWK SED RM MKTEMP GREP RTCWAKE LSMOD MODPROBE RMMOD DD HEAD TAIL TEE WC CAT PRINTF WC DATE STTY STDBUF RM RMDIR KILL ) declare -ra optional_tools=( SUDO TPUT PING NMAP LOCALE ) modules="thunderbolt i915" declare -a dp_in_adapters trace_filter_msg_config=$TRACE_FILTER_EXCLUDE trace_filter_msg_regex="" declare -r FILTER_DEV_NONE=0 declare -r FILTER_DEV_TB=$(bit 0) declare -r FILTER_DEV_GFX=$(bit 1) declare -r FILTER_DEV_ALL=$((FILTER_DEV_TB | FILTER_DEV_GFX)) trace_filter_dev_mask=$FILTER_DEV_NONE trace_filter_dev_regex="" gfx_dev_regex="" tb_dev_regex="" test_started= dry_run=false adapters_got_detected=false skip_test=false reload_network=false wait_network_connection=false declare -r NMAP_STATUS_UP_REGEX="\bStatus: Up\b" ping_tool= ping_options= max_cycle_count=0 # Number of test cycles, until interrupted if 0 declare -r TEST_PHASE_PREPARE=0 declare -r TEST_PHASE_RESUMED=1 declare -r TEST_PHASE_SUSPENDING=2 declare -r TEST_PHASE_STOPPED=3 declare -A test_phase_label declare -r HAS_COMMANDS=$(bit 1) declare -r HAS_COUNTDOWN=$(bit 2) declare -r HAS_PROGRESS=$(bit 3) declare -Ar test_phase_flags=( [$TEST_PHASE_PREPARE]=$((HAS_COMMANDS | HAS_PROGRESS)) [$TEST_PHASE_RESUMED]=$((HAS_COMMANDS | HAS_COUNTDOWN)) [$TEST_PHASE_STOPPED]=$((HAS_COMMANDS | HAS_COUNTDOWN)) ) test_cycle=0 test_phase=$TEST_PHASE_PREPARE test_phase_prev= test_phase_start=0 test_phase_status_str="" test_phase_commands_str="" declare -r ES_LAST=0 declare -r ES_CURRENT=1 declare -r ES_TOTAL=2 declare -r ES_REPORTED=3 declare -a es_dmesg=(0 0 0 0) declare -a es_dmesg_flt=(0 0 0 0) declare -a es_dpme=(0 0 0 0) declare -a es_other=(0 0 0 0) declare -a es_dropped=(0 0 0 0) # record tracker for kmsg in a given stage of processing declare -r RT_SEEK=0 declare -r RT_FEED=1 declare -r RT_PARSE=2 # last seen kmsg cycle number/timestamp declare -a rec_tracker_seq=(-1 -1 -1) declare -a rec_tracker_timestamp=(-1 -1 -1) # timestamp at which the processing is considered done declare -a rec_tracker_target_timestamp=(-1 -1 -1) # last time the cycle number has changed declare -a rec_tracker_update_time=(-1 -1 -1) declare -r REC_TRACKER_POLL_DURATION=5 # error summing flags declare -r NO_DMESG_FLT=$(bit 0) declare -r NO_DROPPED=$(bit 1) cycle_delay_expires=0 cycle_delay_left=0 last_cycle_with_errors=0 declare -r CS_SEQ=0 declare -r CS_TIMESTAMP=1 declare -r CS_REC=2 declare -a current_cycle_read=(0 0 0) declare -a current_cycle_checked=(0 0 0) declare -a current_cycle_start=(-1 0 0) declare -r error_line_fmt=" %-26s %-6s %-6s" successive_sig_count=0 # Handle these sig gracefully, setting only a flag here and letting the # test exit/cleanup happen - by the main thread checking this flag # periodically or after a command got interrupted by this signal - in # the normal main process thread context. # # Successive signal interrupts of the same type will be handled # immediately, without waiting for the main process thread. graceful_sigs="INT TERM" abort_sigs="EXIT" all_sigs="$graceful_sigs $abort_sigs" declare -Ar sig_causes=( ["INT"]="interrupted" ["TERM"]="terminated" ["EXIT"]="aborted" ) declare -A non_descendant_pids=() init_test_phase_labels() { test_phase_label=( [$TEST_PHASE_PREPARE]="Prepare" [$TEST_PHASE_RESUMED]="Resumed" [$TEST_PHASE_SUSPENDING]="Suspending.." ) } grep_no_err() { local err $GREP "$@" err=$? [ $err -eq 1 ] && return 0 return $err } init_suspend_resume_tools() { init_tools required_tools false || return 1 init_tools optional_tools true || return 1 init_sudo || return 1 export -f grep_no_err export GREP } setup_server_ping_tool() { local ip_address local ip_host local ip_port local nmap_output ip_address=$(get_ssh_server_address) || return 1 ip_host=${ip_address%:*} ip_port=${ip_address#*:} if [ -n "${NMAP+x}" ]; then ping_tool=$NMAP ping_options="--host-timeout 1 -oG - $ip_host" nmap_output=$($ping_tool $ping_options 2>&1) if [[ $? -ne 0 ]] || [[ ! "$nmap_output" =~ $NMAP_STATUS_UP_REGEX ]]; then ping_tool="" ping_options="" fi fi [ -n "$ping_tool" ] && return 0 if [ -z "${PING+x}" ]; then pr_err "Neither nmap or ping is available" return 1 fi ping_tool=$PING ping_options="-c 1 -w 1 $ip_host" ping_output=$($ping_tool $ping_options 2>&1) if [ $? -ne 0 ]; then pr_err "Couldn't ping the $ip_host address:\n$ping_output" return 1 fi } setup_server_ping() { setup_server_ping_tool && return 0 err_exit "Couldn't setup a ping tool required for a responsive network connection" } parse_options() { local options local long_options local parsed options="c:swdh" long_options="cycle-count:,reload-network-module,skip-test,wait-network-connection,dry-run,help" # Using getopt to store the parsed options and arguments into $parsed parsed=$(getopt --options=$options --longoptions=$long_options --name "$(basename $0)" -- "$@") || return 1 # Checking for errors if [[ $? -ne 0 ]]; then exit 2 fi # Setting the parsed options and arguments to $@ eval set -- "$parsed" usg=\ "Usage: $(basename $0) [OPTIONS] OPTIONS: -c, --cycle-count Number of test cycles. By default test until interrupted. --reload-network-module rmmod/modprobe network module across suspend/resume. -s, --skip-test Perform only the initialization steps, skip the actual test. -w, --wait-network-connection Wait for responsive network connection after each test cycle. -d, --dry-run Don't perform tests, used for development. -h, --help Print this help. " while true; do case "$1" in -c|--cycle-count) valid_number "$2" || err_exit "Invalid test cycle count \"$2\"\n" [ $2 -gt 0 ] || err_exit "The test cycle count must be at least 1\n" max_cycle_count=$2 shift ;; -s|--skip-test) skip_test=true ;; --reload-network-module) reload_network=true ;; -w|--wait-network-connection) wait_network_connection=true ;; -d|--dry-run) dry_run=true ;; -h|--help) echo "$usg" exit ;; --) shift break ;; *) err_exit "Unexpected option \"$1\"\n" ;; esac shift done if [ $# -gt 0 ]; then err_exit "unexpected arguments \"$@\"\n$usg" fi if $skip_test && [ $max_cycle_count -ne 0 ]; then err_exit "Can't specify both --skip-test and --cycle-count\n" fi if $reload_network; then if [[ ! -v NETWORK_MODULE ]]; then err_exit "The NETWORK_MODULE variable must be set to reload the network module\n" fi if ! $LSMOD | grep "^\<$NETWORK_MODULE\>" > /dev/null ; then err_exit "The \"$NETWORK_MODULE\" network module is not loaded\n" fi fi if $wait_network_connection; then if $skip_test; then err_exit "Can't specify both --skip-test and --wait-network-connection\n" fi setup_server_ping fi } init_test_start() { local epoch=$("$DATE" +%s) local script_start=$SECONDS epoch_delta=$((epoch - script_start)) test_started=$epoch test_phase_start=$test_started } get_epoch_seconds() { echo "$((SECONDS + epoch_delta))" } get_dp_in_adapters() { local err local adps dp_in_adapters=() log "$(get_prefix Init): Looking for DP IN adapters ...\n" adps=$(get_configured_dp_in_adapters) err=$? if [ $err -eq 0 ]; then eval "dp_in_adapters=($adps)" return 0 fi [ $err -ne 2 ] && return 1 adps=$(find_enabled_dp_in_adapters) err=$? [ $err -ne 0 ] && return 1 eval "dp_in_adapters=($adps)" if [ ${#dp_in_adapters[@]} -eq 0 ]; then log "Couldn't find any enabled DP IN adapters to test\n" return 2 fi adapters_got_detected=true } init_dp_in_adapters() { local config_detect_str local adapter_desc $dry_run && return 0 get_dp_in_adapters || return 0 if $adapters_got_detected; then config_detect_str=detected else config_detect_str=configured fi log "$(get_prefix Init): Testing the following $config_detect_str DP IN adapters:\n" for adapter_desc in "${dp_in_adapters[@]}"; do local -A dr_adapter=() local dra dradapter_deserialize dr_adapter "$adapter_desc" dra=$(get_adapter_dra dr_adapter) log_cont " DRA:$dra Dev:${dr_adapter[$DR_DEV]} Pci-ID:${dr_adapter[$DR_PCI_ID]}\n" done } init_filter_regex() { local pattern while IFS= read -r pattern; do trace_filter_msg_regex+=${trace_filter_msg_regex:+|} trace_filter_msg_regex+=$pattern done < <(echo "$FILTERED_DMESG_ERRORS") } get_canonical_intel_vendor_id() { local vendor=$1 [ "$vendor" = "$INTEL_ALIAS_VENDOR_ID" ] && vendor=$INTEL_VENDOR_ID echo "$vendor" } check_pci_address_collision() { local -nr __addresses=$1 local -r address=$2 local -r vendor=$3 local -r device=$4 if [ -n "${__addresses["$address"]+x}" -a \ "${__addresses["$address"]-}" != "$vendor:$device" ]; then local ven_dev_str="$vendor:$device, ${__addresses["$address"]}" pr_err "Multiple PCI IDs for the same address $ven_dev_str\n" return 1 fi return 0 } get_tb_dev_addresses() { local -A addresses=() local address local dev_list local dev dev_list=$(get_tb_devices) || return 1 for dev in $dev_list; do local domain route pci_id while IFS=, read -r domain route pci_id; do local vendor device vendor=$(sanitize_pci_vendor_id "${pci_id#*:}") device=$(sanitize_pci_device_id "${pci_id%%:*}") address=$(get_pci_dev_address "$vendor" "$device") || continue if ! check_pci_address_collision addresses "$address" \ "$vendor" "$device"; then continue fi addresses["$address"]="$vendor:$device" done < <(echo "$dev") done for address in ${!addresses[@]}; do echo "$address-${addresses[$address]}" done return 0 } get_gfx_dev_addresses() { local -A addresses=() local address local dir for dir in $SYSFS_DRM_CLASS_DIR/card+([0-9]); do local vendor device vendor=$(sanitize_pci_vendor_id "$(cat $dir/device/vendor)") device=$(sanitize_pci_device_id "$(cat $dir/device/device)") address=$(get_pci_dev_address "$vendor" "$device") if ! check_pci_address_collision addresses "$address" \ "$vendor" "$device"; then continue fi addresses["$address"]="$vendor:$device" done for address in ${!addresses[@]}; do echo "$address-${addresses[$address]}" done } init_dev_regex() { local tb_addresses="" local gfx_addresses="" local address_desc for address_desc in $(get_tb_dev_addresses); do local address=${address_desc%%-*} tb_addresses+=" $address_desc" tb_dev_regex="${tb_dev_regex}${tb_dev_regex:+|}\+pci:$address" done for address_desc in $(get_gfx_dev_addresses); do local address=${address_desc%%-*} gfx_addresses+=" $address_desc" gfx_dev_regex="${gfx_dev_regex}${gfx_dev_regex:+|}\+pci:$address" done log "$(get_prefix Init): Using the following device dmesg filters:\n" log_cont " TBT devices:\n" for address_desc in $tb_addresses; do local address=${address_desc%%-*} local pci_ids=${address_desc#*-} log_cont " $address: $pci_ids\n" done log_cont " GFX devices:\n" for address_desc in $gfx_addresses; do local address=${address_desc%%-*} local pci_ids=${address_desc#*-} log_cont " $address: $pci_ids\n" done } get_dpme() { local -nr adapter=$1 local dpme local err if $dry_run; then echo 0x1 return 0 fi dpme=$(test_cmd $SUDO $TBGET \ -d "${adapter[$DR_DOMAIN_IDX]}" \ -r "${adapter[$DR_ROUTE]}" \ -a "${adapter[$ADP_ID]}" \ ADP_DP_CS_8.DPME) err=$? if [ "$dpme" != 0x0 -a "$dpme" != 0x1 ]; then log_err "Invalid DPME state: \"$dpme\"\n" err=1 fi echo "$dpme" return $err } check_dpme() { local dr_adapter_desc local err local ret=0 for dr_adapter_desc in "${dp_in_adapters[@]}"; do local -A dr_adapter=() local dpme local dra dradapter_deserialize dr_adapter "$dr_adapter_desc" dra=$(get_adapter_dra dr_adapter) dpme=$(get_dpme dr_adapter) err=$? [ $ret -eq 0 ] && ret=$err if [ $err -ne 0 ]; then log_err "Cannot get DPME for DP IN adapter at DRA $dra\n" [ $err -eq $ERR_INT ] && break dpme_current_cycle_errors=$((dpme_current_cycle_errors + 1)) dpme_total_errors=$((dpme_total_errors + 1)) continue fi if [ "$dpme" != "0x1" ]; then log_err "DPME for DP IN adapter at DRA $dra is not enabled\n" [ $ret -eq 0 ] && ret=1 dpme_current_cycle_errors=$((dpme_current_cycle_errors + 1)) dpme_total_errors=$((dpme_total_errors + 1)) fi done return $ret } calc_dropped_errors() { local dropped_recs=$1 es_dropped[ES_CURRENT]=$((dropped_recs - \ current_cycle_start[CS_REC])) [ ${es_dropped[ES_CURRENT]} -lt 0 ] && \ es_dropped[ES_CURRENT]=0 es_dropped[ES_TOTAL]=$dropped_recs } calc_error_stats() { local -n __es_calc=$1 local new_errors=$2 __es_calc[ES_CURRENT]=$((__es_calc[ES_CURRENT] + new_errors)) __es_calc[ES_LAST]=${__es_calc[ES_CURRENT]} __es_calc[ES_TOTAL]=$((__es_calc[ES_TOTAL] + new_errors)) } check_dmesg_errors() { local rec_info local -a ri local rec_counts local -a rc local dump_recs local last_rec local err rec_info=$(trace_get_record_info) || return $? read -a ri < <(echo "$rec_info") || cmd_err_ret last_rec=$((ri[RI_FIRST_REC] + ri[RI_NUM_RECS])) current_cycle_read[CS_SEQ]=${ri[RI_KMSG_SEQ]} current_cycle_read[CS_TIMESTAMP]=${ri[RI_KMSG_TIMESTAMP]} dump_recs=$((last_rec - current_cycle_checked[CS_REC])) dump_recs=$(min $dump_recs $MAX_REC_SCAN) last_rec=$((current_cycle_checked[CS_REC] + dump_recs)) calc_dropped_errors "${ri[RI_FIRST_REC]}" if [ $dump_recs -eq 0 ]; then if [ $last_rec -ne 0 ]; then current_cycle_checked[CS_SEQ]=${current_cycle_read[CS_SEQ]} current_cycle_checked[CS_TIMESTAMP]=${current_cycle_read[CS_TIMESTAMP]} fi return 0 fi err=0 rec_counts=$(trace_dump_recs "$TRACE_OP_COUNT" \ "$TRACE_FILTER_ONLY" \ "$trace_filter_msg_regex" \ "$trace_filter_dev_regex" \ "${current_cycle_checked[CS_REC]}" "$dump_recs" 0) || err=$? [ $err -ne 0 ] && return $err read -a rc < <(echo "$rec_counts") || return $? last_rec=$((rc[RC_FIRST_REC] + rc[RC_NUM_RECS])) calc_dropped_errors ${rc[RC_DROPPED_RECS]} calc_error_stats es_dmesg $((rc[RC_NUM_RECS] - rc[RC_MATCH_RECS])) calc_error_stats es_dmesg_flt ${rc[RC_MATCH_RECS]} current_cycle_checked[CS_REC]=$last_rec last_cycle_with_errors=$test_cycle if [ ${rc[RC_NUM_RECS]} -gt 0 ]; then current_cycle_checked[CS_SEQ]=${rc[RC_LAST_SEQ]} current_cycle_checked[CS_TIMESTAMP]=${rc[RC_LAST_TIMESTAMP]} fi if [ ${current_cycle_start[CS_SEQ]} -eq -1 ]; then current_cycle_start[CS_SEQ]=$((${rc[RC_LAST_SEQ]} - rc[RC_NUM_RECS])) fi last_cycle_start[CS_REC]=${current_cycle_start[CS_REC]} return 0 } check_and_reset_new_errors() { local -n __es_chk=$1 local old_reported=${__es_chk[ES_REPORTED]} __es_chk[ES_REPORTED]=${__es_chk[ES_CURRENT]} if [ $old_reported -eq ${__es_chk[ES_REPORTED]} ]; then return 0 fi return $ERR_STATS_CHANGED } check_all_errors() { check_dmesg_errors || return $? check_and_reset_new_errors es_dmesg || return $? check_and_reset_new_errors es_dmesg_flt || return $? check_and_reset_new_errors es_dpme || return $? check_and_reset_new_errors es_other || return $? return 0 } rec_tracker_is_complete() { local idx=$1 local now=$2 if [ ${rec_tracker_timestamp[idx]} -eq -1 ]; then return $ERR_STATS_CHANGED fi if [ ${rec_tracker_timestamp[idx]} -ge \ ${rec_tracker_target_timestamp[idx]} ]; then \ return 0 fi if [ $((now - rec_tracker_update_time[idx])) -ge \ $REC_TRACKER_POLL_DURATION ]; then return 0 fi return $ERR_STATS_CHANGED } rec_tracker_update() { local idx=$1 local now=$2 local new_seq=$3 local new_timestamp=$4 local ret=0 rec_tracker_is_complete $idx $now && return 0 if [ ${rec_tracker_target_timestamp[idx]} -eq -1 ]; then if [ $idx -eq 0 ]; then pr_err "Unexpected rec tracker state\n" exit 1 fi rec_tracker_target_timestamp[idx]=$(min \ ${rec_tracker_target_timestamp[idx - 1]} \ ${rec_tracker_timestamp[idx - 1]}) fi if [ ${rec_tracker_seq[idx]} -eq -1 -o \ ${rec_tracker_seq[idx]} -lt $new_seq ]; then rec_tracker_seq[idx]=$new_seq rec_tracker_timestamp[idx]=$new_timestamp rec_tracker_update_time[idx]=$now fi return $ERR_STATS_CHANGED } check_kmsg_dumper() { local now=$1 local dstats local -a ds dstats=$(trace_get_kmsg_dumper_info) || return $? read -a ds < <(echo "$dstats") || cmd_err_ret # Did the given stage start already? [ ${ds[DS_SEEK_SEQ]} -lt 0 -o \ ${ds[DS_FEED_SEQ]} -lt 0 ] && return $ERR_GENERIC rec_tracker_update $RT_SEEK $now \ ${ds[DS_SEEK_SEQ]} \ ${ds[DS_SEEK_TIMESTAMP]} || return $? rec_tracker_update $RT_FEED $now \ ${ds[DS_FEED_SEQ]} \ ${ds[DS_FEED_TIMESTAMP]} || return $? } check_kmsg_parser() { local now=$1 rec_tracker_update $RT_PARSE $now \ ${current_cycle_checked[CS_SEQ]} \ ${current_cycle_checked[CS_TIMESTAMP]} } # Check if dmesg scanning has settled check_dmesg_scan() { local now=$(get_epoch_seconds) [ $test_phase -ne $TEST_PHASE_PREPARE ] && return 0 if check_kmsg_dumper "$now" && check_kmsg_parser "$now"; then update_test_phase $TEST_PHASE_RESUMED || return $? return 0 fi return 0 } get_rec_tracker_progress() { local now=$(get_epoch_seconds) local delta range perc local range_end if ! rec_tracker_is_complete $RT_SEEK $now || [ ${current_cycle_start[CS_SEQ]} -eq -1 ]; then echo "$(rpad_space "$(repeat_char "." $((now % 5)))" 4)" return fi delta=$((current_cycle_checked[CS_SEQ] - \ current_cycle_start[CS_SEQ])) range_end=$(max ${rec_tracker_seq[RT_SEEK]} \ ${rec_tracker_seq[RT_FEED]}) range=$((range_end - current_cycle_start[CS_SEQ])) if [ $range -eq 0 ]; then perc=0 else perc=$((delta * 10000 / range)) fi perc=$(($(min $perc $perc) / 100)) echo "$(lpad_space "$perc" 3) %" } sum_errors() { local err_type=$1 local flags=$2 local errors=0 errors=$((errors + es_dmesg[err_type])) errors=$((errors + es_dpme[err_type])) errors=$((errors + es_other[err_type])) [ $((flags & NO_DMESG_FLT)) -eq 0 ] && \ errors=$((errors + es_dmesg_flt[err_type])) [ $((flags & NO_DROPPED)) -eq 0 ] && \ errors=$((errors + es_dropped[err_type])) echo "$errors" } phase_flags() { echo "${test_phase_flags[$test_phase]-0}" } phase_has_flag() { local flag=$1 [ $(($(phase_flags) & $flag)) -ne 0 ] } update_test_phase_str() { local status=$1 local commands=$2 update_back_if_changed "$test_phase_commands_str" "$commands" update_back_if_changed "$test_phase_status_str" "$status" test_phase_status_str="$status" test_phase_commands_str="$commands" log_cr log "${status}${commands}" } erase_test_phase_commands() { cursor_erase_str "$test_phase_commands_str" test_phase_commands_str="" } erase_test_phase() { erase_test_phase_commands cursor_erase_str "$test_phase_status_str" test_phase_status_str="" } get_prefix() { local label=$1 echo "${COLOR_GREY}$label +$(sec_to_time $((test_phase_start - test_started)))${COLOR_NONE}" } get_cycle_prefix() { get_prefix "Cycle#$test_cycle" } get_error_str() { local new_errors=$1 local total_errors=$2 local msg="" [ $new_errors -ne 0 ] && msg+="$COLOR_RED" msg+="$total_errors $(plural_str $total_errors error)" [ $new_errors -ne 0 ] && msg+="$COLOR_NONE" echo "$msg" } get_test_phase_name() { if ! test_state_is_run; then echo "${COLOR_YELLOW}Stopped${COLOR_NONE}" else echo "${test_phase_label[$test_phase]}" fi } get_commands_str() { local start_stop if test_state_is_run; then start_stop="$(emph_paren_first "stop")/" else start_stop="$(emph_paren_first "Start")/" fi echo "press ${start_stop}$(emph_paren_first "help")" } get_progress_indicator() { if phase_has_flag "$HAS_COUNTDOWN"; then get_cycle_countdown elif phase_has_flag "$HAS_PROGRESS"; then get_rec_tracker_progress fi } print_test_phase() { local new_errors=0 local total_errors=0 local status local commands="" local err=0 new_errors=$(sum_errors $ES_CURRENT $((NO_DMESG_FLT | NO_DROPPED))) total_errors=$(sum_errors $ES_TOTAL $((NO_DMESG_FLT | NO_DROPPED))) status="$(get_cycle_prefix): " status+=$(get_test_phase_name) status+=" $(get_progress_indicator)" status+=", $(get_error_str $new_errors $total_errors)" phase_has_flag "$HAS_COMMANDS" && \ commands=", $(get_commands_str)" update_test_phase_str "$status" "$commands" } get_cycle_countdown() { echo "-$("$PRINTF" "%02ds" $cycle_delay_left)" } __update_test_delay() { local now=$1 if ! test_state_is_run || \ [ $test_phase -eq $TEST_PHASE_PREPARE ]; then cycle_delay_expires=$((now + cycle_delay_left)) fi cycle_delay_left=$((cycle_delay_expires - now)) [ $cycle_delay_left -lt 0 ] && cycle_delay_left=0 print_test_phase } update_test_delay() { __update_test_delay $(get_epoch_seconds) } reset_test_delay() { cycle_delay_expires=$(get_epoch_seconds) __update_test_delay $cycle_delay_expires } update_test_phase() { local phase=$1 test_phase=$phase test_phase_start=$(get_epoch_seconds) cycle_delay_expires=$((test_phase_start + $TEST_CYCLE_DELAY_SEC)) __update_test_delay $test_phase_start } suspend_and_autoresume() { local ret=0 if $dry_run; then sleep $AUTORESUME_DELAY_SEC return 0 fi if $reload_network; then test_cmd_no_out $SUDO $RMMOD \ "$NETWORK_MODULE" || ret=$? fi if [ $ret -eq 0 ]; then test_cmd_no_out $SUDO $RTCWAKE \ -m mem \ -s $AUTORESUME_DELAY_SEC || ret=$? fi if $reload_network; then # retry modprobe a few times if it gets interrupted test_cmd_retry_no_out $SUDO $MODPROBE \ "$NETWORK_MODULE" || ret=$? fi return $ret } poke_remote_with_ping() { local err=0 "$ping_tool" $ping_options &> /dev/null || err=$? [ $err -eq 0 ] && return 0 if [ $err -eq 2 ]; then sleep 1 fi return 1 } poke_remote_with_nmap() { local output local err err=0 output=$($ping_tool $ping_options 2>&1) || err=$? if [[ $err -eq 0 ]] && \ [[ "$output" =~ $NMAP_STATUS_UP_REGEX ]]; then return 0 fi sleep 1 return 1 } poke_remote() { local err local ping_output if [ "$(basename "$ping_tool")" = "ping" ]; then err=0 poke_remote_with_ping || err=$? else err=0 poke_remote_with_nmap || err=$? fi return $err } wait_network_connection() { local wait_expires=$(($(get_epoch_seconds) + \ MAX_PING_DURATION_SEC)) [ -z "$ping_tool" ] && return 0 while test_state_is_run && \ [ $(date +%s) -lt $wait_expires ]; do poke_remote && break done return 0 } add_new_other_errors() { local err=$1 [ $err -eq 0 -o $err -eq $ERR_INT ] && return es_other[ES_CURRENT]=$((es_other[ES_CURRENT] + 1)) es_other[ES_TOTAL]=$((es_other[ES_TOTAL] + 1)) } test_suspend_resume() { local err update_test_phase $TEST_PHASE_SUSPENDING || return $? err=0 suspend_and_autoresume || err=$? add_new_other_errors "$err" err=0 wait_network_connection || return $? add_new_other_errors "$err" return 0 } cleanup_suspend_resume() { erase_test_phase log_cr } pause_test_phase() { local pause=$1 local next_state if $pause; then next_state=$TEST_STATE_PAUSE else next_state=$TEST_STATE_RUN fi test_state_is $next_state && return update_test_delay test_state_set $next_state update_test_delay } get_error_header() { printf "${error_line_fmt}" "" New Total } get_error_line() { local -nr __es_prn=$1 local label=$2 printf "${error_line_fmt}\n" \ "$label" \ "${__es_prn[ES_CURRENT]}" \ "${__es_prn[ES_TOTAL]}" } print_error_stats() { erase_test_phase log_cr log "Errors (for all sources, w/o the device filter applied):\n" log_cont "$(get_error_header)\n" log_cont "$(get_error_line es_dmesg "Unfiltered Dmesg")\n" log_cont "$(get_error_line es_dmesg_flt "Filtered Dmesg")\n" log_cont "$(get_error_line es_dropped "Dropped Dmesg")\n" log_cont "$(get_error_line es_dpme "DP BWA mode state (DPME)")\n" log_cont "$(get_error_line es_other "Others")\n" } declare -Ar msg_filter_label=( [$TRACE_FILTER_NONE]="all" [$TRACE_FILTER_EXCLUDE]="unfiltered" [$TRACE_FILTER_ONLY]="filtered" ) get_filter_msg_config_str() { echo "${msg_filter_label[$trace_filter_msg_config]}" } change_msg_filter_config() { erase_test_phase log_cr trace_filter_msg_config=$(((trace_filter_msg_config + 1) % TRACE_FILTER_MAX)) log "$(get_cycle_prefix): Dmesg message filter changed to: $(get_filter_msg_config_str) messages\n" } get_filter_dev_str() { case "$trace_filter_dev_mask" in $FILTER_DEV_NONE) echo "any sources" ;; $FILTER_DEV_TB) echo "TBT device" ;; $FILTER_DEV_GFX) echo "GFX device" ;; $FILTER_DEV_ALL) echo "TBT and GFX devices" ;; *) echo "Invalid" ;; esac } change_dev_filter() { erase_test_phase log_cr trace_filter_dev_mask=$(((trace_filter_dev_mask + 1) % \ (FILTER_DEV_ALL + 1))) log "$(get_cycle_prefix): Dmesg device filter changed to: $(get_filter_dev_str)\n" case "$trace_filter_dev_mask" in $FILTER_DEV_NONE) trace_filter_dev_regex="" ;; $FILTER_DEV_TB) trace_filter_dev_regex="$tb_dev_regex" ;; $FILTER_DEV_GFX) trace_filter_dev_regex="$gfx_dev_regex" ;; $FILTER_DEV_ALL) trace_filter_dev_regex="$tb_dev_regex" if [ -n "$gfx_dev_regex" ]; then trace_filter_dev_regex+="${trace_filter_dev_regex:+|}$gfx_dev_regex" fi ;; *) trace_filter_dev_regex="" esac } dump_recs() { local trace_op=$1 local since=$2 local label=${3-} local -r indent=2 local dump_count local dump_start=$since erase_test_phase log_cr [ $dump_start -lt 0 ] && dump_start=0 if trace_is_clear_only_op "$trace_op"; then log "$(get_cycle_prefix): Clearing all dmesg messages\n" else log "$(get_cycle_prefix): Dmesg$label:\n" fi trace_dump_recs "$trace_op" \ "$trace_filter_msg_config" \ "$trace_filter_msg_regex" \ "$trace_filter_dev_regex" \ "$since" "-1" "$indent" || true } dump_last_cycle_recs() { local label label="Cycle#$last_cycle_with_errors, $(get_filter_msg_config_str), $(get_filter_dev_str))" if [ ${es_dmesg[ES_LAST]} -eq 0 -a \ ${es_dmesg_flt[ES_LAST]} -eq 0 ]; then log "$(get_cycle_prefix): No dmesg messsages to show for $label\n" return fi dump_recs "$TRACE_OP_PRINT" "${last_cycle_start[CS_REC]}" " for $label" } dump_all_recs() { local label label=" (since the test started, $(get_filter_msg_config_str), $(get_filter_dev_str))" if [ ${es_dmesg[ES_TOTAL]} -eq 0 -a \ ${es_dmesg_flt[ES_TOTAL]} -eq 0 ]; then log "$(get_cycle_prefix): No dmesg messsages to show\n" return fi dump_recs "$TRACE_OP_PRINT" "0" "$label" } print_help() { erase_test_phase log_cr declare -a commands=( "d:Dump last dmesg errors" "D:Dump all dmesg errors" "f:Change dmesg message filtering (current: $(get_filter_msg_config_str) messages)" "F:Change dmesg device filtering (current: $(get_filter_dev_str))" "c:Print and clear all dmesg errors" "C:Clear all dmesg errors" "e:Print error stats" "s:Stop test cycle" "S:Start test cycle (pressed again: skip delay)" "q:Quit test" "h:Print this help" ) log "Usage:\n" for cmd in "${commands[@]}"; do log " $(emph_first "${cmd%%:*}") - ${cmd#*:}\n" done } print_invalid_command() { local sym=$(get_symbol_for_char "$1") local msg=" ${COLOR_RED}$sym ?${COLOR_NONE}" local i for ((i = 0; i < 1; i++)) { echo -ne "$msg" sleep 0.1 cursor_erase_str "$msg" [ $i -lt 2 ] && sleep 0.05 } } handle_command() { local char=$1 local pause=true case "$char" in d) dump_last_cycle_recs ;; D) dump_all_recs ;; f) change_msg_filter_config ;; F) change_dev_filter ;; c) dump_recs "$TRACE_OP_PRINT_AND_CLEAR" 0 ;; C) dump_recs "$TRACE_OP_CLEAR" 0 ;; e) print_error_stats ;; s) ;; S) if test_state_is_run; then reset_test_delay pause= else pause=false fi ;; q) test_state_set $TEST_STATE_QUIT pause= ;; h) print_help ;; *) print_invalid_command "$char" ;; esac if [ -n "$pause" ]; then pause_test_phase "$pause" fi } wait_command() { local err=0 char=$(read_char 0.3) if [ -n "$char" ]; then flush_input handle_command "$char" fi check_dmesg_scan check_all_errors || err=$? [ $err -ne 0 -a $err -ne $ERR_STATS_CHANGED ] && return $err update_test_delay case $test_state in $TEST_STATE_BREAK | $TEST_STATE_QUIT) return $ERR_GENERIC ;; $TEST_STATE_PAUSE) return 0 ;; $TEST_STATE_RUN) if [ $test_phase -eq $TEST_PHASE_PREPARE ]; then return 0 elif $skip_test; then log "Init complete, skip test as requested\n" test_state_set $TEST_STATE_QUIT return $ERR_GENERIC elif [ $cycle_delay_left -le 0 ]; then return $ERR_GENERIC else return 0 fi ;; esac } run_test_cycle() { local ret=0 if [ $max_cycle_count -ne 0 -a \ $test_cycle -ge $max_cycle_count ]; then return $ERR_GENERIC fi update_test_phase $TEST_PHASE_PREPARE || return $? check_dpme || true while wait_command; do : done; case $test_state in $TEST_STATE_RUN) test_cycle=$((test_cycle + 1)) test_suspend_resume ;; $TEST_STATE_BREAK) test_state_set $TEST_STATE_PAUSE ;; $TEST_STATE_QUIT) ret=$ERR_GENERIC ;; esac return $ret } test_cycle_init() { local i es_dmesg[ES_CURRENT]=0 es_dmesg_flt[ES_CURRENT]=0 es_dpme[ES_CURRENT]=0 es_other[ES_CURRENT]=0 if [ ${current_cycle_start[CS_SEQ]} -ne -1 ]; then current_cycle_start[CS_SEQ]=${current_cycle_checked[CS_SEQ]} fi current_cycle_start[CS_REC]=${current_cycle_checked[CS_REC]} for i in $RT_SEEK $RT_FEED $RT_PARSE; do rec_tracker_seq[i]=-1 rec_tracker_timesamp[i]=-1 rec_tracker_target_timesamp[i]=-1 done # consecutive trackers will set their target timestamp to the # previous tracker's target timestamp once that tracker is # complete. rec_tracker_target_timestamp[RT_SEEK]=$((($(get_uptime_sec) + \ REC_TRACKER_POLL_DURATION) * \ USEC_PER_SEC)) } test_cycle_cleanup() { local current_errors=$(sum_errors $ES_CURRENT $((NO_DMESG_FLT | NO_DROPPED))) # Leave a trace of separate log line for cycles with errors if [ $current_errors -ne 0 ]; then erase_test_phase_commands log fi } run_test() { local total_errors local msg="" local err log "$(get_prefix Init): Test started at $(sec_to_date_time "$(get_epoch_seconds)")\n" err=0 while [ $err -eq 0 ]; do test_cycle_init run_test_cycle || err=$? test_cycle_cleanup done cleanup_suspend_resume total_errors=$(sum_errors $ES_TOTAL $((NO_DMESG_FLT | NO_DROPPED))) msg+="$test_cycle $(plural_str $test_cycle cycle), " msg+="$(get_error_str $total_errors $total_errors)" log "$(get_prefix End): $msg\n" [ $total_errors -ne 0 ] && print_error_stats "$test_cycle" } load_one_module() { local module=$1 local ret=0 local loaded_mode local mod_list mod_list=$(test_cmd $LSMOD) || return 1 log "$(get_prefix Init): Loading module $module:" loaded_mod=$(test_cmd grep_no_err "\<$module\>" <<< "$mod_list") || return 1 if [ -n "$loaded_mod" ]; then log_cont " Already loaded\n" return 2 fi test_cmd_no_out $SUDO $MODPROBE "$module" || return 1 log_cont " Loaded succesfully\n" return 0 } load_modules() { local had_to_modprobe=false local module for module in $modules; do local err=0 load_one_module "$module" || err=$? if [ $err -eq 0 ]; then had_to_modprobe=true elif [ $err -ne 2 ]; then log_err "Loading modules failed: $err\n" return $err fi done if $had_to_modprobe; then sleep 3 fi return 0 } init_test() { init_test_start init_filter_regex log "$(get_prefix "Init"): Reading in dmesg:" if ! trace_init "$FILTERED_DMESG_ERRORS"; then pr_err "Initialization of dmesg trace failed\n" return 1 fi log_cont " done.\n" if ! load_modules; then trace_cleanup return 1 fi if ! init_dp_in_adapters; then trace_cleanup return 1 fi init_dev_regex return 0 } cleanup_test() { trace_cleanup || true } process_is_descendant() { local target_pid=$1 local current_pid=$BASHPID local -a pids=() local pid [ $target_pid -eq $current_pid ] && return 1 while [ "$target_pid" -gt 1 ]; do if [ "$target_pid" -eq "$current_pid" ]; then return 0 fi if [ -n "${non_descendant_pids[$target_pid]+x}" ]; then return 1 fi pids+=($target_pid) target_pid=$(ps -o ppid= -p $target_pid) [ -z "$target_pid" ] && break done for pid in ${pids[@]}; do non_descendant_pids[$pid]=1 done return 1 } check_descendant_processes() { local pid local -a descendants=() if ! debug_is_enabled; then return fi for pid in $(ps -axo pid | tail -n +2); do if process_is_descendant "$pid"; then descendants+=($pid) fi done if [ ${#descendants[@]} -ne 0 ]; then local pattern="" pr_err "Still active descendant processes:\n" for pid in "${descendants[@]}"; do # filter out the grep process pattern="${pattern}${pattern:+\\|}[${pid:0:1}]${pid:1}" done ps --forest -ax | grep "$pattern" fi } main_cleanup() { cleanup_test || true cleanup_utils check_descendant_processes } setup_sig_handlers() { local sigs=$@ for sig in $sigs; do trap "sig_handler $sig" "$sig" done } restore_sig_handlers() { local sigs=$@ trap - $sigs } handle_graceful_sig() { local sig=$1 case "$successive_sig_count" in 0) log "${COLOR_YELLOW}Test ${sig_causes[$sig]}${COLOR_NONE}\n" if test_state_must_break; then test_state_set $TEST_STATE_QUIT else test_state_set $TEST_STATE_BREAK fi ;; 1) pr_err "Next signal will abort\n" restore_sig_handlers $graceful_sigs ;; *) pr_err "Unexpected signal\n" ;; esac if test_state_must_quit; then successive_sig_count=$((successive_sig_count + 1)) fi return 0 } handle_abort_sig() { local sig=$1 test_state_set $TEST_STATE_QUIT pr_err "\nAbort due to script error, forced abort or interrupted initialization\n" restore_sig_handlers $all_sigs main_cleanup || pr_err "Cleanup failed\n" exit 1 } sig_is_of_type() { local queried_sig=$1 local sig_list=$2 local sig for sig in $sig_list; do [ "$sig" = "$queried_sig" ] && return 0 done return 1 } sig_handler() { local sig=$1 local graceful_sig debug "Main thread sig \"$sig\"\n" if sig_is_of_type "$sig" "$abort_sigs"; then handle_abort_sig "$sig" return 0 fi handle_graceful_sig "$sig" # Trap for the unwary, in the case where set -e # is in effect (although even set +e may cause # surprise): # an implicit return here # would return the value of $? as it was right # before the handler got called: # https://github.com/jsoref/bash/blob/bc007799f0e13/CWRU/changelog#L5955 # Which would probably cause an unintended # immediate exit of the script even though the # interrupted command was part of a conditional, # since the trap handler inherits the non-zero # (EINTR or the like) $? error code of the # interrupted command. # # The above also applies to all functions called # from this handler, so an implicit return from # those will cause an immediate exit (unless the # call is part of a conditional), even though in # normal, non-signal handler context this return # value would be 0. return 0 } main_init() { cache_sudo_right || return 1 init_utils || return 1 init_test_phase_labels setup_sig_handlers $graceful_sigs if ! init_test; then restore_sig_handlers $graceful_sigs cleanup_utils return 1 fi return 0 } init_suspend_resume_tools || err_exit "Init failed\n" parse_options "$@" || exit $? setup_sig_handlers $abort_sigs if ! main_init "$@"; then restore_sig_handlers $abort_sigs err_exit "Initialization failed\n" fi run_test || true # clean up still restore_sig_handlers $all_sigs main_cleanup || err_exit "Cleanup failed\n" tbtools-0.8.0+git20260510.75be11e/scripts/dp-tunnel-status.sh000077500000000000000000000044271517364422400231500ustar00rootroot00000000000000#!/bin/bash -e # # Dumps the DisplayPort tunnel status. # usage() { echo "Usage: $0 [DOMAIN] ROUTE ADAPTER" echo echo "Dumps DP tunnel status" exit 1 } if [ $# -lt 2 ]; then usage exit 1 fi if [ $# -eq 2 ]; then domain=0 route=$1 adapter=$2 else domain=$1 route=$2 adapter=$3 fi # # dp_tunnel_status() - Reads and dumps DisplayPort tunnel status # @domain: Domain number # @router: Route string of the router # @adapter: DisplayPort IN adapter # dp_tunnel_status() { local domain=$1 local route=$2 local adapter=$3 local val # Check type first val=$(tbadapters -d $domain -r $route -a $adapter -S | sed 1d | cut -d, -f 2) if [[ $val != "DisplayPort IN" ]]; then echo "Error: DisplayPort IN adapter expected" 1>&2 exit 1 fi # Check if paths are enabled val=$(tbget -d $domain -r $route -a $adapter ADP_DP_CS_0.VE) if [ $val != "0x1" ]; then printf "No active tunnel\n" return fi # Is BW alloc mode enabled? val=$(tbget -d $domain -r $route -a $adapter ADP_DP_CS_8.DPME) if [ $val = "0x1" ]; then read -r granularity estimated allocated requested group <&2 exit 1 ;; esac printf "Group : $group\n" rate=$((granularity * estimated)) printf "Estimated : $rate Mb/s\n" rate=$((granularity * requested)) printf "Requested : $rate Mb/s\n" rate=$((granularity * allocated)) printf "Allocated : $rate Mb/s\n" else read -r lanes rate <&2 exit 1 ;; esac printf "Rate : $rate Mb/s x $lanes\n" fi printf "Capabilities:\n" tbdump -d $domain -r $route -a $adapter -N 1 -vv DP_COMMON_CAP } dp_tunnel_status $domain $route $adapter tbtools-0.8.0+git20260510.75be11e/scripts/dropped.sql000066400000000000000000000011201517364422400215230ustar00rootroot00000000000000-- Find all packets that were dropped. -- -- $ psql tracedb -- => \i scripts/dropped.sql SELECT e.entry, e.timestamp, e.domain, to_hex(e.route) AS route, e.adapter, e.adapter_type, to_hex(d.vendor) AS vendor, to_hex(d.device) AS device , d.vendor_name, d.device_name, e.pdf, e.cs, f.field_offset, to_hex(f.data_offset) AS data_offset, to_hex(f.value) AS value, f.name FROM trace_field f, trace_entry e, device d WHERE f.entry = e.entry AND e.domain = d.domain AND e.route = d.route AND e.dropped = TRUE; tbtools-0.8.0+git20260510.75be11e/scripts/dump-dpcd.sh000077500000000000000000000003311517364422400215640ustar00rootroot00000000000000#!/bin/sh if [ $# -ne 1 ]; then echo "Usage: dump-dpcd.sh AUX" echo echo "Dumps AUX USB4 DPCD registers." exit 1 fi AUX=$1 dd if=/dev/drm_dp_aux${AUX} bs=1 skip=$((0xe0000)) count=64 2> /dev/null | od -Ax -tx1 tbtools-0.8.0+git20260510.75be11e/scripts/enable-bw.sh000077500000000000000000000005061517364422400215470ustar00rootroot00000000000000#!/bin/sh -e if [ $# -ne 1 ]; then echo "Usage: enable-bw-mode.sh AUX" echo echo "Enables USB4 bandwidth allocation mode from DPCD side." exit 1 fi AUX=$1 VAL=0x80 OVAL=$(printf "\%o" $VAL) printf "%b" "$OVAL" | dd of=/dev/drm_dp_aux${AUX} bs=1 count=1 seek=$((0xe0030)) 2> /dev/null echo "BW allocation mode enabled" tbtools-0.8.0+git20260510.75be11e/scripts/errors.sql000066400000000000000000000022441517364422400214120ustar00rootroot00000000000000-- Finds all error notifications from the trace data. These are all -- notifications where event_code is not any of the following: -- -- 7 = HP_ACK -- 32 = DP_BW -- 33 = ROP_CMPLT -- 34 = POP_CMPLT -- 35 = PCIE_WAKE -- 36 = DP_CON_CHANGE -- 37 = DPTX_DISCOVERY -- 38 = LINK_RECOVERY -- 39 = ASYM_LINK -- -- $ psql tracedb -- => \i scripts/errors.sql SELECT e.entry, e.timestamp, e.domain, to_hex(e.route) AS route, e.adapter, e.adapter_type, to_hex(d.vendor) AS vendor, to_hex(d.device) AS device , d.vendor_name, d.device_name, e.function, e.pdf, to_hex(f.value) AS value, (f.value::bit(32) & x'000000ff'::bit(32))::integer AS event_code, ((f.value::bit(32) & x'00003f00'::bit(32)) >> 8)::integer AS event_info, ((f.value::bit(32) & x'c0000000'::bit(32)) >> 30)::integer AS PG FROM trace_field f, trace_entry e, device d WHERE f.entry = e.entry AND e.domain = d.domain AND e.route = d.route AND d.type = 'Router' AND e.pdf = 'Notification Packet' AND (f.value::bit(32) & x'000000ff'::bit(32))::integer NOT IN (7, 32, 33, 34, 35, 36, 37, 38, 39) AND f.field_offset = 2; tbtools-0.8.0+git20260510.75be11e/scripts/estimated-bw.sh000077500000000000000000000011321517364422400222740ustar00rootroot00000000000000#!/bin/sh -e if [ $# -lt 1 ]; then echo "Usage: estimated-bw.sh AUX" echo echo "Dumps estimated bandwidth (in Mbit/s) through USB4 DPCD registers." exit 1 fi AUX=$1 GR=$(dd if=/dev/drm_dp_aux${AUX} bs=1 skip=$((0xe0022)) count=1 2> /dev/null | od -An -tx1 | sed 's/^[ \t]*//') case $GR in 00) GR=250 ;; 01) GR=500 ;; 02) GR=1000 ;; *) echo "Error: unsupported granularity: $GR" 1>&2 exit 1 ;; esac EST=$(dd if=/dev/drm_dp_aux${AUX} bs=1 skip=$((0xe0023)) count=1 2> /dev/null | od -An -tx1 | sed 's/^[ \t]*//') BW=$((0x$EST * $GR)) printf "Estimated BW: %d Mb/s\n" $BW tbtools-0.8.0+git20260510.75be11e/scripts/lane-1.sql000066400000000000000000000017651517364422400211620ustar00rootroot00000000000000-- Check for writes to lane 1 adapter basic config spaces. This is -- forbidden in the spec. This only applies Intel routers for now. We -- need to expose adapters as well to figure out the lane 1 adapters of -- other vendors (or hard-code them). -- -- $ psql tracedb -- => \i scripts/lane-1.sql SELECT e.entry, e.timestamp, e.domain, to_hex(e.route) AS route, e.adapter, e.adapter_type, to_hex(d.vendor) AS vendor, to_hex(d.device) AS device , d.vendor_name, d.device_name, e.pdf, e.cs, f.field_offset, to_hex(f.data_offset) AS data_offset, to_hex(f.value) AS value, f.name FROM trace_field f, trace_entry e, device d WHERE f.entry = e.entry AND e.domain = d.domain AND e.route = d.route AND d.type = 'Router' AND d.generation = 'USB4' AND e.pdf = 'Write Request' AND e.cs = 'Adapter' AND e.adapter_type = 'Lane' AND -- Intel lane 1 adapters e.adapter IN (2, 4, 6, 8) AND f.name LIKE 'ADP_CS_%'; tbtools-0.8.0+git20260510.75be11e/scripts/merge-logs.py000077500000000000000000000025561517364422400220010ustar00rootroot00000000000000#!/usr/bin/env python3 import heapq import os import re import sys def merge_logs(*logs): files = [open(file, 'r') for file in logs] heap = [] count = 0 pattern = re.compile(r"^\[\s*(\d+\.\d+)\]") for file in files: entry = () while True: line = file.readline() if not line: # Add the last one if multiline if entry: heapq.heappush(heap, entry) file.close() break; line = line.rstrip('\n') match = pattern.match(line) if match: if entry: heapq.heappush(heap, entry) # Make it sorted by timestamp but also insertion order # so that's why we use count here. See more from heapq # documentation (specifically the Priority Queue section). entry = (float(match.group(1)), count, line) count += 1 else: entry = (entry[0], entry[1], entry[2] + "\n" + line) while heap: print(heapq.heappop(heap)[2]) if __name__ == "__main__": logs = sys.argv[1:] if not logs: print("Usage: " + os.path.basename(__file__) + " LOG1 [LOG2...]") print() print("Merges two or more log files into one") sys.exit(1) merge_logs(*logs) tbtools-0.8.0+git20260510.75be11e/scripts/nonspec-register.sql000066400000000000000000000015761517364422400233740ustar00rootroot00000000000000-- Find non-spec register access from trace output. -- -- $ psql tracedb -- => \i scripts/nonspec-register.sql -- -- Check for access with no known register name. These are all non-spec -- registers. SELECT e.entry, e.timestamp, e.domain, to_hex(e.route) AS route, e.adapter, e.adapter_type, to_hex(d.vendor) AS vendor, to_hex(d.device) AS device , d.vendor_name, d.device_name, e.pdf, e.cs, f.field_offset, to_hex(f.data_offset) AS data_offset, to_hex(f.value) AS value, f.name FROM trace_field f, trace_entry e, device d WHERE f.entry = e.entry AND e.domain = d.domain AND e.route = d.route AND d.type = 'Router' AND d.generation = 'USB4' AND f.entry IN ( SELECT DISTINCT entry FROM trace_field WHERE data_offset IS NOT NULL AND name IS NULL ORDER BY entry ); tbtools-0.8.0+git20260510.75be11e/scripts/nvm-version.sh000077500000000000000000000021021517364422400221700ustar00rootroot00000000000000#!/bin/sh -e # # Dumps Intel USB4 router NVM image version. Can be useful at least with # the integrated hosts that do not expose all NVM operations. # # NVM Read router operation NVM_READ=0x22 usage() { echo "Usage: $0 [DOMAIN] ROUTE" echo echo "Reads Intel USB4 router NVM image version" exit 1 } if [ $# -lt 1 ]; then usage exit 1 fi if [ $# -eq 1 ]; then domain=0 route=$1 else domain=$1 route=$2 fi # # nvm_read() - Reads up to 64-bytes from router NVM # @domain: Domain number # @route: Route string of the router # @address: Double word address in the NVM # @len: Number of double words to read (max 16) # nvm_read() { local domain=$1 local route=$2 local address=$3 local len=$4 local metadata=$(printf "0x%08x" $((len << 24 | address << 2))) tbset -d $domain -r $route ROUTER_CS_25=$metadata tbset -d $domain -r $route ROUTER_CS_26.Opcode=$NVM_READ ROUTER_CS_26.OV=1 tbdump -d $domain -r $route -N $len ROUTER_CS_9 } value=$(nvm_read $domain $route 2 1) printf "Thunderbolt binary version: %x.%x\n" \ $(((value >> 16) & 0xff)) $(((value >> 8) & 0xff)) tbtools-0.8.0+git20260510.75be11e/scripts/pcie-downstream-mapping.sh000077500000000000000000000032051517364422400244440ustar00rootroot00000000000000#!/bin/bash # # Runs Get PCIe Downstream Mapping router operation and returns the # results. # set -e GET_PCIE_DOWNSTREAM_MAPPING=0x30 usage() { echo "Usage: $0 [DOMAIN] ROUTE" echo echo "Runs Get PCIe Downstream Mapping router operation" exit 1 } if [ $# -lt 1 ]; then usage exit 1 fi if [ $# -eq 1 ]; then domain=0 route=$1 else domain=$1 route=$2 fi # # pcie_read_one_mapping() - Reads one PCIe mapping entry # @domain: Domain number # @route: Route string of the router # # As long as there are entries left returns 0. When final entry is # encountered returns 1. # pcie_read_one_mapping() { local domain=$1 local route=$2 local metadata tbset -d $domain -r $route \ ROUTER_CS_26.Opcode=$GET_PCIE_DOWNSTREAM_MAPPING \ ROUTER_CS_26.OV=1 metadata=$(tbget -d $domain -r $route ROUTER_CS_25) data=($(tbdump -d $domain -r $route -N 2 ROUTER_CS_9)) entries=$((metadata & 0xff)) index=$(((metadata & 0xff00) >> 8)) native=$((data[0] & 0x1)) rid=$((data[1] & 0xffff)) nfpb_bus=$((rid >> 8)) nfpb_dev=$(((rid >> 3) & 0x1f)) nfpb_fn=$((rid & 0x7)) rid=$(((data[1] & 0xffff0000) >> 16)) fpb_bus=$((rid >> 8)) fpb_dev=$(((rid >> 3) & 0x1f)) fpb_fn=$((rid & 0x7)) if (( $native == 1 )); then printf "$index: Native FPB: %02x:%02x.%x Non-FPB: %02x:%02x.%x\n" \ $fpb_bus $fpb_dev $fpb_fn $nfpb_bus $nfpb_dev $nfpd_fn else adapter=$(((data[0] & 0x7e) >> 1)) printf "$index: Tunneled $adapter FPB: %02x:%02x.%x Non-FPB: %02x:%02x.%x\n" \ $fpb_bus $fpb_dev $fpb_fn $nfpb_bus $nfpb_dev $nfpd_fn fi if (( $index == $entries - 1 )); then return 1 fi return 0 } while pcie_read_one_mapping $domain $route; do :; done tbtools-0.8.0+git20260510.75be11e/scripts/process-devices.py000077500000000000000000000027521517364422400230340ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import csv import os import util def process_devices(args): base = os.path.basename(args.devices) base = os.path.splitext(base)[0] devices = base + '.processed.csv' with open(args.devices) as input_file, open(devices, 'w') as output_file: reader = csv.DictReader(input_file) field_names = [ 'domain', 'route', 'adapter', 'index', 'vendor', 'device', 'vendor_name', 'device_name', 'type', 'generation', ] devices_writer = csv.DictWriter(output_file, fieldnames=field_names) devices_writer.writeheader() for row in reader: if row['type'] == 'Domain': continue util.convert_int(row, 'route') util.convert_int(row, 'vendor') util.convert_int(row, 'device') devices_writer.writerow(row) if __name__ == "__main__": parser = argparse.ArgumentParser(prog='process-devices', description='Process tblist -S output suitable for database.', epilog=""" Processes the tblist -S output into a format that is suitable for inserting into a database. From file DEVICES generates a file called DEVICES.processed.csv. """) parser.add_argument('devices'); args = parser.parse_args() process_devices(args) tbtools-0.8.0+git20260510.75be11e/scripts/process-trace.py000077500000000000000000000055031517364422400225050ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import csv import os import sys import util def write_row(fields, writer, field_names): for field in fields: f = {key: field[key] for key in field_names if key in field} # Convert to integer util.convert_int(f, 'offset') util.convert_int(f, 'data_offset') util.convert_int(f, 'value') # Write them writer.writerow(f) def process_trace(args): base = os.path.basename(args.trace) base = os.path.splitext(base)[0] entries = base + '.processed.entries.csv' fields = base + '.processed.fields.csv' with open(args.trace) as input_file, \ open(entries, 'w') as entries_file, \ open(fields, 'w') as fields_file: reader = csv.DictReader(input_file) entry_names = [ 'entry', 'timestamp', 'datetime', 'function', 'dropped', 'pdf', 'cs', 'domain', 'route', 'adapter', 'adapter_type', ] entries_writer = csv.DictWriter(entries_file, fieldnames=entry_names) entries_writer.writeheader() field_names = [ 'entry', 'offset', 'data_offset', 'value', 'name', ] fields_writer = csv.DictWriter(fields_file, fieldnames=field_names) fields_writer.writeheader() line = None for row in reader: if not line: # First line in the file line = row['entry'] fields = [row] else: if line != row['entry']: # Write the entries first e = {key: row[key] for key in entry_names if key in row} util.convert_timestamp(e, 'datetime') util.convert_int(e, 'route') entries_writer.writerow(e) # Then the fields write_row(fields, fields_writer, field_names) line = row['entry'] fields = [row] else: fields.append(row) # Last one write_row(fields, fields_writer, field_names) if __name__ == "__main__": parser = argparse.ArgumentParser(prog='process-trace', description='Split the trace CSV output into separate files.', epilog=""" Splits trace output into separate files, more suitable for inserting into database. From file TRACE generates two files TRACE.processed.entries.csv and TRACE.processed.fields.csv. These have matching first field that is the trace line number. """) parser.add_argument('trace'); args = parser.parse_args() process_trace(args) tbtools-0.8.0+git20260510.75be11e/scripts/reset-port.sh000077500000000000000000000020131517364422400220120ustar00rootroot00000000000000#!/bin/bash -e # # Triggers Downstream Port Reset on an adapter. Can be useful for # simulating hotplug for instance. # usage() { echo "Usage: $0 [DOMAIN] ROUTE ADAPTER" echo echo "Triggers Downstream Port Reset on adapter" exit 1 } if [ $# -lt 2 ]; then usage exit 1 fi if [ $# -eq 2 ]; then domain=0 route=$1 adapter=$2 else domain=$1 route=$2 adapter=$3 fi # # reset_port() - Issue downstream reset on USB4 port # @domain: Domain number # @router: Route string of the router # @adapter: Lane adapter number # reset_port() { local domain=$1 local route=$2 local adapter=$3 local val # Check type first val=$(tbadapters -d $domain -r $route -a $adapter -S | sed 1d | cut -d, -f 2) if [[ $val != "Lane 0" ]]; then echo "Error: unsupported adapter type: $val" 1>&2 exit 1 fi tbset -d $domain -r $route -a $adapter PORT_CS_19.DPR=1 sleep 1 tbset -d $domain -r $route -a $adapter PORT_CS_19.DPR=0 printf "Domain $domain Route $route Adapter $adapter: reset done\n" } reset_port $domain $route $adapter tbtools-0.8.0+git20260510.75be11e/scripts/tables.sql000066400000000000000000000032551517364422400213530ustar00rootroot00000000000000-- PostgreSQL database tables for tracing. -- -- See following for installing the database itself -- https://www.postgresql.org/docs/ -- https://ubuntu.com/server/docs/install-and-configure-postgresql -- -- The tool output needs to be processed first to be suitable for -- inserting into the database: -- $ scripts/process-devices.py devices.csv -- $ scripts/process-trace.py trace.csv -- -- Following commands can be used to create the database, tables and -- insert the data: -- $ createdb tracedb -- $ psql tracedb -- => \i scripts/tables.sql -- => \copy device from 'devices.processed.csv' WITH (FORMAT CSV, HEADER TRUE) -- => \copy trace_entry from 'trace.processed.entries.csv' WITH (FORMAT CSV, HEADER TRUE) -- => \copy trace_field from 'trace.processed.fields.csv' WITH (FORMAT CSV, HEADER TRUE) -- -- Once you are done with the data you can drop the database: -- $ dropdb tracedb DROP TABLE IF EXISTS device; DROP TABLE IF EXISTS trace_entry; DROP TABLE IF EXISTS trace_field; CREATE TABLE device ( domain INTEGER, route BIGINT, adapter INTEGER, index INTEGER, vendor INTEGER, device INTEGER, vendor_name VARCHAR(256), device_name VARCHAR(256), type VARCHAR(32) NOT NULL, generation VARCHAR(32), PRIMARY KEY (domain, route, type) ); CREATE TABLE trace_entry ( entry INTEGER, timestamp NUMERIC(12, 6), datetime TIMESTAMP, function VARCHAR(16), dropped BOOLEAN, pdf VARCHAR(64), cs VARCHAR(32), domain INTEGER, route BIGINT, adapter INTEGER, adapter_type VARCHAR(32), PRIMARY KEY(entry) ); CREATE TABLE trace_field ( entry INTEGER, field_offset INTEGER, data_offset INTEGER, value BIGINT, name VARCHAR(256), PRIMARY KEY (entry, field_offset) ); tbtools-0.8.0+git20260510.75be11e/scripts/tb-bandwidth.sh000077500000000000000000000121461517364422400222650ustar00rootroot00000000000000#!/bin/bash # # dp_tunnel_status() - Reads DisplayPort tunnel status # @domain: Domain number # @router: Route string of the router # @adapter: DisplayPort IN adapter # dp_tunnel_status() { local domain=$1 local route=$2 local adapter=$3 local val # Check type first val=$(tbget -d $domain -r $route -a $adapter ADP_CS_2) val=$(printf "0x%x" $((val & 0xffffff))) case $val in 0xe0101 | 0xe0102) ;; *) echo "Error: unsupported adapter type: $val" 1>&2 exit 1 ;; esac # Check if paths are enabled val=$(tbget -d $domain -r $route -a $adapter ADP_DP_CS_0.VE) if [ $val != "0x1" ]; then printf "\t\tTunnel : inactive\n" return else printf "\t\tTunnel : active\n" fi # read link rate and line count read -r lanes lrate <&2 exit 1 ;; esac # Is BW alloc mode enabled? val=$(tbget -d $domain -r $route -a $adapter ADP_DP_CS_8.DPME) if [ $val = "0x1" ]; then read -r granularity estimated allocated requested group <&2 exit 1 ;; esac printf "\t\tDPBWMode : enabled\n" printf "\t\tGroup : $group\n" rate=$((granularity * estimated)) rate_gbps=$(awk "BEGIN {printf \"%.2f\", $rate / 1000}") printf "\t\tEstimated : $rate_gbps Gb/s\n" rate=$((granularity * requested)) rate_gbps=$(awk "BEGIN {printf \"%.2f\", $rate / 1000}") printf "\t\tRequested : $rate_gbps Gb/s\n" rate=$((granularity * allocated)) rate_gbps=$(awk "BEGIN {printf \"%.2f\", $rate / 1000}") printf "\t\tAllocated : $rate_gbps Gb/s\n" else printf "\t\tDPBWMode : disabled\n" allocated_bandwidth=$(awk "BEGIN {printf \"%.2f\", $lrate * $lanes * \ 8 / 10}") allocated_bandwidth_gbps=$(awk "BEGIN {printf \"%.2f\", \ $allocated_bandwidth / 1000}") printf "\t\tAllocated : $allocated_bandwidth_gbps Gb/s\n" fi rate_gbps=$(awk "BEGIN {printf \"%.2f\", $lrate / 1000}") printf "\t\tLink Rate : $hbr($rate_gbps Gb/s)\n" printf "\t\tLane Count: $lanes\n" } printf "[Thunderbolt/USB4 Info]:\n" router=0 num_domains=$(tblist -SA 2> /dev/null | sed 1d | awk -F, '$9 ~ /^Domain$/ { num +=1 } END { print num }') for domain in $(seq 0 $((num_domains-1))); do printf "TBHost$domain:\n" while read -r adapter; do printf "\tAdapter:DP IN-$adapter\n" dp_tunnel_status $domain $router $adapter done < <(tbadapters --route 0 | grep "DisplayPort IN" | awk '{sub(/:/,""); print $1}') done echo printf "[Display Info]:\n" # Path to the i915_display_info file DISPLAY_INFO_PATH="/sys/kernel/debug/dri/0000:00:02.0/i915_display_info" # Check if the file exists if [ ! -f "$DISPLAY_INFO_PATH" ]; then echo "$DISPLAY_INFO_PATH not found!" exit 1 fi # Find all unique pipe identifiers pipes=$(awk -F '[][]' '/\[CRTC:[0-9]+:pipe [A-Z]\]:/ { split($2, a, ":"); print a[3] }' "$DISPLAY_INFO_PATH" | sort -u) # Find all connectors that are "connected", excluding eDP connectors connected_connectors=$(grep -E " connected" "$DISPLAY_INFO_PATH" | \ grep -v "eDP" | awk -F '[][]' '{print $2}') # Loop through each pipe identifier and extract its information active_pipe=0 for pipe in $pipes; do crtc_id=$(awk -F '[][]' "/\[CRTC:[0-9]+:pipe $pipe\]:/ { \ split(\$2, a, \":\"); print a[2] }" \ "$DISPLAY_INFO_PATH" | head -n 1) if [ -z "$crtc_id" ]; then continue fi PATTERN="\\\[CRTC:$crtc_id:pipe $pipe\\\]:" pipe_info=$(awk -v pattern="$PATTERN" ' $0 ~ pattern { printing = 1; next } printing && /^\t/ { print; next } printing && !/^\t/ { exit } ' "$DISPLAY_INFO_PATH") for connector in $connected_connectors; do if echo "$pipe_info" | grep -q "$connector"; then printf "\t$connector: connected\n" active_pipe=1 mode_value=$(echo "$pipe_info" | grep -o 'mode="[^"]*"' | awk -F '"' '{print $2}' | head -n 1) printf "\t\tResolution: $mode_value\n" bpp_value=$(echo "$pipe_info" | grep -o 'bpp=[^,]*' | awk -F '=' '{print $2}' | head -n 1) printf "\t\tBPP : $bpp_value\n" freq_value=$(echo "$pipe_info" | grep -o -w 'mode="[^"]*":[[:space:]]*[0-9]*' | awk -F ':' '{print $2}' | tr -d '[:space:]' | head -n 1) printf "\t\tFrequency : ${freq_value}Hz\n" encoder_line=$(echo "$pipe_info" | grep "ENCODER") encoder_line_cleaned=$(echo "$encoder_line" | sed -e 's/[[:space:]]//g' -e 's/:connectors://') printf "\t\tEncoder : $encoder_line_cleaned\n" fi done done if [ $active_pipe -eq 0 ]; then printf "\tNo active display detected!\n" fi tbtools-0.8.0+git20260510.75be11e/scripts/tbtool-db.sh000066400000000000000000000203601517364422400215760ustar00rootroot00000000000000#!/bin/bash declare -r DP_IN_ADAPTER_TYPE="DisplayPort IN" declare -r DP_IN_ADAPTER_STATE_EN="Enabled" declare -r DP_IN_ADAPTER_STATE_DIS="Disabled" declare -r ADAPTER_NOT_IMPLEMENTED_TYPE="Not implemented" declare -r TBLR_DOMAIN_IDX=0 declare -r TBLR_ROUTE=1 declare -r TBLR_UNDEF1=2 declare -r TBLR_UNDEF2=3 declare -r TBLR_PCI_VENDOR=4 declare -r TBLR_PCI_DEVICE=5 declare -r TBLR_VENDOR=6 declare -r TBLR_DEVICE=7 declare -r TBLR_TYPE=8 declare -r TBLR_SIZE=9 declare -r TBLR_TYPE_ROUTER="Router" declare -r DR_DOMAIN_IDX=domain declare -r DR_ROUTE=route declare -r DR_PCI_ID=pci_id declare -r DR_DEV=dev declare -ra DR_KEYS=("$DR_DOMAIN_IDX" "$DR_ROUTE" "$DR_PCI_ID" "$DR_DEV") declare -r TBAD_NUM=0 declare -r TBAD_TYPE=1 declare -r TBAD_STATE=2 declare -r TBAD_SIZE=3 declare -r ADP_ID=id declare -r ADP_TYPE=type declare -r ADP_STATE=state declare -ra ADP_KEYS=("$ADP_ID" "$ADP_TYPE" "$ADP_STATE") declare -ra DR_ADP_KEYS=("${DR_KEYS[@]}" "${ADP_KEYS[@]}") get_adapter_dra() { local -nr __adp_ref=$1 echo "${__adp_ref[$DR_DOMAIN_IDX]}:${__adp_ref[$DR_ROUTE]}:${__adp_ref[$ADP_ID]}" } __domain_route_deserialize() { obj_init "$1" "$2" "$3" check_keys DR_KEYS "$1" "$2" } domain_route_deserialize() { __domain_route_deserialize "$1" "$1" "$2" } domain_route_init() { __domain_route_deserialize "$1" "$1" "( \ [$DR_DOMAIN_IDX]=\"$2\" \ [$DR_ROUTE]=\"$3\" \ [$DR_PCI_ID]=\"$4\" \ [$DR_DEV]=\"$5\" \ )" } __adapter_deserialize() { obj_init "$1" "$2" "$3" check_keys ADP_KEYS "$1" "$2" } adapter_deserialize() { __adapter_deserialize "$1" "$1" "$2" } adapter_init() { __adapter_deserialize "$1" "$1" "( \ [$ADP_ID]=\"$2\" \ [$ADP_TYPE]=\"$3\" \ [$ADP_STATE]=\"$4\" \ )" } __dradapter_deserialize() { obj_init "$1" "$2" "$3" check_keys DR_ADP_KEYS "$1" "$2" } dradapter_deserialize() { __dradapter_deserialize "$1" "$1" "$2" } valid_dr_field() { local -r name=$1 local -r value=$2 local tblist_tool=$(tool_name $TBLIST) if ! valid_number "$value"; then log_err "Invalid $tblist_tool \"$name\" number: \"$value\"" return 1 fi } valid_pci_id() { local -r pci_id=$1 [[ $pci_id =~ ^[[:xdigit:]]{4}:[[:xdigit:]]{4}$ ]] && return 0 log_err "Invalid PCI ID: \"$pci_id\"" return 1 } parse_domain_route_rec() { local -r dr_rec=$1 local -A dr=() local -a tblr local pci_id local tblist_tool=$(tool_name $TBLIST) IFS=, read -r -a tblr <<< "$dr_rec" assert_array_size "tblr" tblr $TBLR_SIZE if [ "${tblr[TBLR_TYPE]}" != "$TBLR_TYPE_ROUTER" ]; then log_err "Unexpected $tblist_tool device type:\"${tblr[TBLR_TYPE]}\" (expected $TBLR_TYPE_ROUTER)" return 1 fi valid_dr_field "Domain IDX" "${tblr[TBLR_DOMAIN_IDX]}" || return 1 valid_dr_field "Route" "${tblr[TBLR_ROUTE]}" || return 1 pci_id="${tblr[TBLR_PCI_VENDOR]}:${tblr[TBLR_PCI_DEVICE]}" valid_pci_id "$pci_id" || return 1 domain_route_init dr "${tblr[TBLR_DOMAIN_IDX]}" "${tblr[TBLR_ROUTE]}" \ "$pci_id" "${tblr[TBLR_DEVICE]}" serialize_obj dr } get_domain_routes() { local dr_table local dr_rec local -a dr_list=() dr_table=$(test_cmd_errout "$TBLIST" -S | "$TAIL" -n +2) || return 1 if [[ -z "$dr_table" ]]; then log_err "$(tool_name $TBLIST): No devices found" return 1 fi while read -r dr_rec; do dr_list+=("$(parse_domain_route_rec "$dr_rec")") || return 1 done <<< "${dr_table}" serialize_array dr_list } get_domain_route() { local -r domain=$1 local -r route=$2 local -a dr_array local dr_desc local drs drs=$(get_domain_routes) || return 1 array_init dr_array "($drs)" || return 1 for dr_desc in "${dr_array[@]}"; do local -A dr=() domain_route_deserialize dr "($dr_desc)" if [ "${dr[$DR_DOMAIN_IDX]}" = "$domain" -a \ "${dr[$DR_ROUTE]}" = "$route" ]; then echo "$dr_desc" return 0 fi done return 2 } parse_adapter_rec() { local -r adapter_rec=$1 local -A adapter local -a tbad IFS=, read -r -a tbad <<< "$adapter_rec" if [ "${tbad[TBAD_TYPE]}" = "$ADAPTER_NOT_IMPLEMENTED_TYPE" -a \ -z ${tbad[TBAD_STATE]+x} ]; then tbad[TBAD_STATE]="N/A" # tbadapters gives this as an empty # field, but arrays in bash can't # contain a trailing empty element # (other elements can be empty). fi assert_array_size "tbad" tbad $TBAD_SIZE if ! valid_number "${tbad[TBAD_NUM]}"; then log_err "Invalid $(tool_name $TBADAPTERS) adapter number: \"${tbad[TBAD_NUM]}\"" return 1 fi adapter_init adapter "${tbad[TBAD_NUM]}" "${tbad[TBAD_TYPE]}" "${tbad[TBAD_STATE]}" serialize_obj adapter } get_adapters_for_domain_route() { local -r domain=$1 local -r route=$2 local -a adapters local dr_desc local -A dr local adapter_table dr_desc="$(get_domain_route "$domain" "$route")" || return 1 domain_route_deserialize dr "($dr_desc)" || return 1 adapter_table=$(test_cmd_errout \ $SUDO "$TBADAPTERS" -S \ -d "$domain" \ -r "$route" | "$TAIL" -n +2) || return 1 while read -r adapter_rec; do local -A adapter=() adapter_deserialize adapter \ "($(parse_adapter_rec "$adapter_rec"))" || return 1 adapters+=("$(concat_obj_values dr adapter)") done <<< "${adapter_table}" serialize_array adapters } get_tb_devices() { local -a dr_list local dr_desc local drs local -a dev_list=() drs=$(get_domain_routes) || return 1 array_init dr_list "($drs)" || return 1 for dr_desc in "${dr_list[@]}"; do local -A dr=() domain_route_deserialize dr "($dr_desc)" || return 1 dev_list+=("${dr[$DR_DOMAIN_IDX]},${dr[$DR_ROUTE]},${dr[$DR_PCI_ID]}") done echo "${dev_list[*]}" } find_enabled_dp_in_adapters() { local -a adapters=() local -a dr_list local dr_desc local drs drs=$(get_domain_routes) || return 1 array_init dr_list "($drs)" || return 1 for dr_desc in "${dr_list[@]}"; do local -A dr=() local -a adapter_table=() local adapter_desc local -a adp_list=() local adapter_table domain_route_deserialize dr "($dr_desc)" || return 1 adapter_table=$(get_adapters_for_domain_route \ "${dr[$DR_DOMAIN_IDX]}" "${dr[$DR_ROUTE]}") || return 1 array_init adp_list "($adapter_table)" || return 1 for adapter_desc in "${adp_list[@]}"; do local -A adapter=() dradapter_deserialize adapter "$adapter_desc" [ "${adapter[$ADP_TYPE]}" != "$DP_IN_ADAPTER_TYPE" ] && continue [ "${adapter[$ADP_STATE]}" != "$DP_IN_ADAPTER_STATE_EN" ] && continue adapters+=("$(concat_obj_values dr adapter)") done done serialize_array adapters } validate_dra_part() { local dra_part=$1 local dra_value=$2 valid_number "$dra_value" && return 0 log_err "Invalid $dra_part in $TBT_DP_IN_ADAPTERS_CONF_NAME: \"$dra_value\" (expected an array of Domain:Route:AdapterID)" return 1 } get_adapter() { local domain=$1 local route=$2 local adapter_id=$3 local -a adp_list local dradapter_desc array_init adp_list \ "($(get_adapters_for_domain_route "$domain" "$route"))" || return 1 for dradapter_desc in "${adp_list[@]}"; do local -A dradapter=() dradapter_deserialize dradapter "$dradapter_desc" if [ "${dradapter[$ADP_ID]}" = "$adapter_id" ]; then echo "$dradapter_desc" return 0 fi done return 2 } get_configured_dp_in_adapters() { local -a adapters=() local adapter_rec [ ${#TBT_DP_IN_ADAPTERS[@]} -eq 0 ] && return 2 for adapter_rec in "${TBT_DP_IN_ADAPTERS[@]}"; do local domain route adapter_id local adapter_desc local -A dr_adapter=() local dra local err=0 IFS=: read -r domain route adapter_id <<< "$adapter_rec" validate_dra_part "domain" "$domain" || return 1 validate_dra_part "route" "$route" || return 1 validate_dra_part "adapter ID" "$adapter_id" || return 1 dra="DRA:$domain:$route:$adapter_id" adapter_desc="$(get_adapter "$domain" "$route" "$adapter_id")" || err=$? if [ $err -eq 2 ]; then log_err "Can't find configured DP IN adapter at $dra" return 1 elif [ $err -ne 0 ]; then return 1 fi dradapter_deserialize dr_adapter "$adapter_desc" if [ "${dr_adapter[$ADP_TYPE]}" != "$DP_IN_ADAPTER_TYPE" ]; then log_err "The configured adapter at $dra is not a DP IN adapter" return 1 fi if [ "${dr_adapter[$ADP_STATE]}" != "$DP_IN_ADAPTER_STATE_EN" ]; then log_err "The configured DP IN adapter at $dra is not enabled" return 1 fi adapters+=("$(get_obj_values dr_adapter)") done [ ${#adapters[@]} -eq 0 ] && log_err "Parse error of $TBT_DP_IN_ADAPTERS_CONF" serialize_array adapters return 0 } tbtools-0.8.0+git20260510.75be11e/scripts/tbtools-completion.bash000066400000000000000000000275611517364422400240620ustar00rootroot00000000000000# -*- shell-script -*- # Bash completion for tbtools. # # Depends on bash-completion scripts typically installed with # "bash-completion" package: # # https://github.com/scop/bash-completion/ # # The way to install this is to copy it to /usr/share/bash-completion/completions/ # and then create symlinks for all the tools you are interested in: # # cd /usr/share/bash-completion/completions/ # ln -s tbtools-completion.bash tbadapters # ln -s tbtools-completion.bash tbauth # ln -s tbtools-completion.bash tbdump # ln -s tbtools-completion.bash tbget # ln -s tbtools-completion.bash tblist # ln -s tbtools-completion.bash tbmargin # ln -s tbtools-completion.bash tbmonitor # ln -s tbtools-completion.bash tbpd # ln -s tbtools-completion.bash tbset # ln -s tbtools-completion.bash tbtrace # ln -s tbtools-completion.bash tbtunnels # _tbtools_domain_route_adapter() { local arg domain= route= adapter= path= counters= for ((i=1; i < ${COMP_CWORD}; i=i+1)); do arg="${COMP_WORDS[i]}" if [[ $arg == '-d' || $arg == '--domain' ]]; then domain=${COMP_WORDS[i + 1]} elif [[ $arg == '-r' || $arg == '--route' ]]; then route=${COMP_WORDS[i + 1]} elif [[ $arg == '-a' || $arg == '--adapter' ]]; then adapter=${COMP_WORDS[i + 1]} elif [[ $arg == '-p' || $arg == '--path' ]]; then path=1 elif [[ $arg == '-c' || $arg == '--counters' ]]; then counters=1 fi done } _tbtools_complete_domains() { local domains domains=$(tblist -SA 2> /dev/null | sed 1d | awk -F, '$9 ~ /^Domain$/ { print $1 }' | xargs) COMPREPLY+=($(compgen -W "$domains" -- "$cur")) } # 1: domain (or default 0 is used) _tbtools_complete_routers() { local domain routers domain=${1:-0} routers=$(tblist -SA 2> /dev/null | sed -e 1d -e 's/"\([^",]\+\),*\([^"]*\)"/\1\2/' | awk -F, -v domain=$domain '$1 ~ domain && $9 ~ /^Router$/ { print $2 }' | xargs) COMPREPLY+=($(compgen -W "$routers" -- "$cur")) } # 1: route # 2: domain (or default 0 is used) _tbtools_complete_all_adapters() { local route domain adapters [[ ! $1 ]] && return route=$1 domain=${2:-0} adapters=$(tbadapters -d $domain -r $route -S 2> /dev/null | sed 1d | cut -d, -f1 | xargs) COMPREPLY+=($(compgen -W "$adapters" -- "$cur")) } # 1: route # 2: domain (or default 0 is used) _tbtools_complete_lane_adapters() { local route domain adapters [[ ! $1 ]] && return route=$1 domain=${2:-0} adapters=$(tbadapters -d $domain -r $route -S 2> /dev/null | sed 1d | awk -F, '$2 ~ /Lane/ { print $1 }' | xargs) COMPREPLY+=($(compgen -W "$adapters" -- "$cur")) } _tbtools_complete_registers() { local registers _tbtools_domain_route_adapter domain=${domain:-0} [[ ! $route ]] && return # Get rid of escaped spaces reg="${cur//\\ / }" if [[ $adapter ]]; then # No completion for path or counter registers if [[ $path || $counters ]]; then return else registers=$(tbget -d $domain -r $route -a $adapter -Q "$reg" 2> /dev/null) fi else registers=$(tbget -d $domain -r $route -Q "$cur" 2> /dev/null) fi # Escape spaces with backslash registers="${registers// /\\\\ }" reg="${cur// /\\\\ }" local IFS=$'\n' COMPREPLY+=($(compgen -W "$registers" -- "$reg")) } _tbadapters() { local cur prev words cword domain route _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --route --adapter --script --help --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; --adapter | -a) _tbtools_domain_route_adapter _tbtools_complete_all_adapters $route $domain return ;; esac fi } && complete -F _tbadapters tbadapters _tbauth() { local cur prev words cword domain _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--add-key-path --challenge-key-path --help --deauthorize --domain --route --version' -- "$cur")) else case $prev in --add-key-path | --challenge-key-path | -[AC]) _filedir return ;; --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; esac fi } && complete -F _tbauth tbauth _tbdump() { local cur prev words cword domain route path counters _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --route --adapter --path --counters --verbose --cap-id --vs-cap-id --nregs --help --drom' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; --adapter | -a) _tbtools_domain_route_adapter _tbtools_complete_all_adapters $route $domain return ;; --cap-id | --vs-cap-id | --nregs | --drom | -[CVND]) return ;; esac if [[ $cur != -* ]]; then _tbtools_complete_registers fi fi } && complete -F _tbdump tbdump _tbget() { local cur prev words cword domain route _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --route --adapter --path --counters --binary --decimal --query --help --verbose --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; --adapter | -a) _tbtools_domain_route_adapter _tbtools_complete_all_adapters $route $domain return ;; esac if [[ $cur != -* ]]; then _tbtools_complete_registers fi fi } && complete -F _tbget tbget _tblist() { local cur prev words cword _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--all --script --tree --verbose --help --version' -- "$cur")) fi } && complete -F _tblist tblist _tbmargin() { local cur prev words cword domain route _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --route --adapter --index --caps --help --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; --adapter | -a) _tbtools_domain_route_adapter _tbtools_complete_lane_adapters $route $domain return ;; esac fi } && complete -F _tbmargin tbmargin _tbmonitor() { local cur prev words cword domain _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --help --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; esac fi } && complete -F _tbmonitor tbmonitor _tbpd() { local cur prev words cword domain route _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --route --adapter --mode --help --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; --adapter | -a) _tbtools_domain_route_adapter _tbtools_complete_lane_adapters $route $domain return ;; --mode | -m) COMPREPLY+=($(compgen -W "safe usb usb4 display-port thunderbolt" -- "$cur")) return ;; esac fi } && complete -F _tbpd tbpd _tbset() { local cur prev words cword domain route adapter _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --route --adapter --path --counters --help --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; --route | -r) _tbtools_domain_route_adapter _tbtools_complete_routers $domain return ;; --adapter | -a) _tbtools_domain_route_adapter _tbtools_complete_all_adapters $route $domain return ;; esac if [[ $cur != -* ]]; then _tbtools_complete_registers fi fi } && complete -F _tbset tbset _tbtunnels() { local cur prev words cword domain _init_completion || return if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--domain --verbose --help --version' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains return ;; esac fi } && complete -F _tbtunnels tbtunnels _tbtrace() { local cur prev words cword _init_completion || return local arg _get_first_arg if [[ -z $arg ]]; then if [[ $cur == -* ]]; then COMPREPLY+=($(compgen -W '--help --version' -- "$cur")) else COMPREPLY+=($(compgen -W 'status enable disable dump clear help' -- "$cur")) fi else case $arg in dump) if [[ $cur == -* ]]; then COMPREPLY=($(compgen -W '--help --input --script --time --verbose' -- "$cur")) else case $prev in --input | -i) _filedir ;; esac fi return ;; enable) if [[ $cur == -* ]]; then COMPREPLY=($(compgen -W '--help --domain' -- "$cur")) else case $prev in --domain | -d) _tbtools_complete_domains ;; esac fi return ;; *) if [[ $cur == -* ]]; then COMPREPLY=($(compgen -W '--help' -- "$cur")) fi return ;; esac fi } && complete -F _tbtrace tbtrace # ex: ts=4 sw=4 et filetype=sh tbtools-0.8.0+git20260510.75be11e/scripts/tools.sh000066400000000000000000000037571517364422400210630ustar00rootroot00000000000000tool_cmd() { echo "$(basename "$1")" } find_tool() { PATH=${TOOL_PATH:+$TOOL_PATH:}$PATH which "$1" || return 2 } assign_tool_path() { local tool_varname=$1 local tool_path=$2 [ -f "$tool_path" ] || return 2 [ -x "$tool_path" ] || err_exit "Tool $tool_path is not executable" eval "$tool_varname=\"$tool_path\"" } tool_name() { echo "$(tool_cmd "${1,,}")" } find_and_assign_tool() { local tool_varname=$1 local tool_name=$(tool_name "$tool_varname") local tool_path local err tool_path=$(find_tool "$tool_name") err=$? [ $err -ne 0 ] && return $err assign_tool_path "$tool_varname" "$tool_path" } init_sudo() { if [ "$(id -u)" -eq 0 ]; then SUDO="" return 0 fi [[ -v SUDO ]] && return 0 pr_err "Cannot find tool sudo, required to acquire root rights\n" return 1 } init_tools() { local -nr tools=$1 local -r tools_are_optional=$2 local tool_varname for tool_varname in "${tools[@]}"; do find_and_assign_tool "$tool_varname" && continue $tools_are_optional && continue pr_err "Cannot find tool \"$(tool_name $tool_varname)\"\n" return 1 done } __test_cmd() { local errout=$1 local err_file local cmd_out local err shift err_file=$($MKTEMP) if $errout; then cmd_out=$("$@" 2>&1) else cmd_out=$("$@" 2> "$err_file") fi err=$? case "$err" in 0) echo "$cmd_out" ;; $ERR_INT) log_note "Command \"$*\" was interrupted\n" ;; *) log_err "Command \"$*\" failed with error code $err:" if $errout; then log "$cmd_out\n" else log "$(cat "$err_file")\n" fi ;; esac $RM "$err_file" return $err } test_cmd() { __test_cmd false "$@" } test_cmd_errout() { __test_cmd true "$@" } test_cmd_no_out() { test_cmd "$@" > /dev/null } test_cmd_retry() { local first_err=0 local err local i for ((i = 0; i < $MAX_TEST_CMD_RETRY_ATTEMPTS; i++)); do test_cmd "$@" err=$? [ $first_err -eq 0 ] && first_err=$err [ $err -ne $ERR_INT ] && break done return $first_err } test_cmd_retry_no_out() { test_cmd_retry "$@" > /dev/null } tbtools-0.8.0+git20260510.75be11e/scripts/trace.sh000077500000000000000000000611001517364422400210060ustar00rootroot00000000000000KMSG_FILE="/dev/kmsg" TRACE_TMP_DIR= TRACE_LOCK_FILE= TRACE_INFO_FILE= TRACE_REC_FILE= KMSG_COUNTER_PID_FILE= KMSG_DUMPER_PID_FILE= KMSG_INFO_FILE= KMSG_LINE_COUNT_FILE= KMSG_DUMPER_LOCK_FILE= KMSG_DUMPER_SEEK_FILE= KMSG_DUMPER_FEED_FILE= KMSG_DUMPER_EXIT_FILE= # last number of kmsg lines checked at startup KMSG_STARTUP_LAST_LINES=10 KMSG_DUMPER_INFO_UPDATE_PERIOD=1000 # 100ms maximum backward step in non-consecutive kmsg # timestamps KMSG_MAX_TIMESTAMP_DELTA_US=$((100 * 1000)) declare -r TRACE_MAX_LINES=10000 declare -r TRACE_SLACK=100 declare -r MAX_BATCH_RECS=200 # must cover any line /dev/kmsg wants to return in one read declare -r KMSG_MIN_BLOCK_SIZE=2048 declare -r TRACE_OP_PRINT=0 declare -r TRACE_OP_COUNT=1 declare -r TRACE_OP_PRINT_AND_CLEAR=2 declare -r TRACE_OP_CLEAR=3 declare -r TRACE_FILTER_NONE=0 declare -r TRACE_FILTER_EXCLUDE=1 declare -r TRACE_FILTER_ONLY=2 declare -r TRACE_FILTER_MAX=3 declare -r REC_FILTER_MATCH=0 declare -r REC_FILTER_UNMATCH=1 declare -r TRACE_FLAG_CLEARED=1 declare -r ERR_GENERIC=240 declare -r ERR_REC_FILE_CHANGED=241 declare -r ERR_READ_TIMEOUT=242 # record info declare -r RI_FIRST_REC=0 declare -r RI_NUM_RECS=1 declare -r RI_KMSG_SEQ=2 declare -r RI_KMSG_TIMESTAMP=3 declare -r RI_FLAGS=4 # dmesg dump info declare -r DI_REC=0 declare -r DI_SEQ=1 declare -r DI_TIMESTAMP=2 declare -r DI_SIZE=3 # dmesg dump stats declare -r DS_SEEK_REC=0 declare -r DS_SEEK_SEQ=1 declare -r DS_SEEK_TIMESTAMP=2 declare -r DS_FEED_REC=3 declare -r DS_FEED_SEQ=4 declare -r DS_FEED_TIMESTAMP=5 # dmesg record declare -r DR_SUBSYSTEM=0 declare -r DR_DEVICE=1 declare -r DR_PRIO=2 declare -r DR_SEQ=3 declare -r DR_TIMESTAMP=4 declare -r DR_CALLER_ID=5 declare -r DR_MESSAGE=6 # record counter declare -r RC_FIRST_REC=0 declare -r RC_NUM_RECS=1 declare -r RC_DROPPED_RECS=2 declare -r RC_MATCH_RECS=3 declare -r RC_LAST_SEQ=4 declare -r RC_LAST_TIMESTAMP=5 declare -r max_prio=4 # kmsg batch read info declare -r KB_RECS_READ=0 declare -r KB_RECS_ADDED=1 declare -r KB_SEQ=2 declare -r KB_TIMESTAMP=3 declare -r KB_LAST_LINE=4 declare -a kmsg_batch=(0 0 -1 -1 "") trace_server_kmsg_dumper_pids= trace_server_pid="" declare -a trace_files=() declare -r kmsg_counter_poll_period_ms=300 kmsg_counter_last_updated= valid_number() { [ -n "$1" -a -z "${1/#+([0-9])/}" ] } if ! shopt -p expand_aliases > /dev/null; then echo "Missing required 'shopt -s expand_aliases'" >&2 exit 1 fi alias cmd_err_ret='return $(to_err $?)' alias get_err='{ err=$?; [ $err -eq 0 ]; }' kmsg_dumper_exiting() { [ -n "${KMSG_DUMPER_EXIT_FILE:+x}" -a \ -f "${KMSG_DUMPER_EXIT_FILE}" ] } trace_get_kmsg_dumper_info_locked() { local -a di_seek local -a di_feed read -a di_seek < "$KMSG_DUMPER_SEEK_FILE" read -a di_feed < "$KMSG_DUMPER_FEED_FILE" echo "${di_seek[@]} ${di_feed[@]}" } trace_get_kmsg_dumper_info() { call_with_flock "$KMSG_DUMPER_LOCK_FILE" \ trace_get_kmsg_dumper_info_locked } trace_write_dumper_info_locked() { local info_file=$1 local -nr __di_wr_locked=$2 assert_array_size "dump_info" __di_wr_locked $DI_SIZE echo "${__di_wr_locked[@]}" > "$info_file" || cmd_err_ret } trace_write_dumper_info() { local info_file=$1 local -nr __di_wr=$2 call_with_flock "$TRACE_LOCK_FILE" \ trace_write_dumper_info_locked "$info_file" __di_wr } parse_kmsg_header() { local line=$1 local prio seq timestamp prio=${line%%,*} line=${line#*,} seq=${line%%,*} line=${line#*,} timestamp=${line%%,*} if ! valid_number "$prio" || \ ! valid_number "$seq" || \ ! valid_number "$timestamp"; then log_kmsg_err "Invalid kmsg header (prio: \"$prio\")" "$seq" "$timestamp" return $ERR_GENERIC fi echo "$prio $seq $timestamp" } write_rec_counter_info() { local info_file=$1 local recno=$2 local line=$3 local header local -a di if [ $((SECONDS - kmsg_counter_last_updated)) -lt 2 ]; then return fi kmsg_counter_last_updated=$SECONDS header=$(parse_kmsg_header "$line") || return $? read -a di < <(echo "$header") trace_write_dumper_info "$info_file" di } is_cont_line() { [ "${1:0:1}" = " " ] } get_defragged_line() { local tmo_ms=$1 local line="" while true; do local frag local frag_err local err=0 IFS= read -r -t $(msec_to_sec $tmo_ms) frag || err=$(to_err $?) case "$err" in 0) line+="$frag" if [ -z "$line" ]; then log_err "Read empty dmesg line\n" return $ERR_READ_TIMEOUT fi echo "$line" return 0 ;; $ERR_ALRM) # In reality not a SIG_ALRM, rather pselect # failing just no other way to report this event # probably # # if [ -n "$frag" ]; then line+="$frag" # give more time for additional # fragments tmo=1 continue fi err=$ERR_READ_TIMEOUT ;& # fallthrough *) if [ -n "$line" ]; then log_err "Drop incomplete dmesg line (err:$err):\"$line\"\n" fi return $err ;; esac done } count_recs() { local rec_info_file=$1 local recno=0 local last_line="" while ! kmsg_dumper_exiting; do local err=0 line=$(get_defragged_line $kmsg_counter_poll_period_ms) || err=$? case $err in 0) recno=$((recno + 1)) if ! is_cont_line "$line"; then last_line=$line fi ;& # fallthrough $ERR_READ_TIMEOUT) if [ -n "$last_line" ]; then write_rec_counter_info "$rec_info_file" \ $recno \ "$last_line" fi ;; *) if ! kmsg_dumper_exiting; then log_err "Read failed with error $err in kmsg record counter, exiting\n" fi break esac done } dump_kmsg_recs() { local line_offset=$1 local pid_file=$2 $SUDO "$STDBUF" -oL \ "$TAIL" -n +$line_offset \ "$KMSG_FILE" & echo "$!" > "$pid_file" } count_kmsg_recs() { local line_offset=$1 local pid_file=$2 local rec_info_file=$3 dump_kmsg_recs $line_offset \ "$pid_file" | \ count_recs "$rec_info_file" || cmd_err_ret } get_kmsg_info() { local last_lines="" local -a di_seek=() local line KMSG_LINE_COUNT_FILE="$TRACE_TMP_DIR/line_count" err=0 last_lines=$($SUDO "$DD" if="$KMSG_FILE" \ iflag=nonblock \ bs=$KMSG_MIN_BLOCK_SIZE \ 2> /dev/null | \ "$TEE" >("$WC" -l > "$KMSG_LINE_COUNT_FILE") | \ "$TAIL" -n $KMSG_STARTUP_LAST_LINES ) || err=$? [ -f "$KMSG_LINE_COUNT_FILE" ] || return $ERR_GENERIC di_seek[DI_REC]=$("$CAT" "$KMSG_LINE_COUNT_FILE") || cmd_err_ret "$RM" "$KMSG_LINE_COUNT_FILE" || cmd_err_ret KMSG_LINE_COUNT_FILE="" valid_number "${di_seek[DI_REC]}" || return $ERR_GENERIC [ -n "$last_lines" ] || return $ERR_GENERIC # Get the seq/timestamp info from the last record that has this # information while IFS= read -r line; do local header local prio is_cont_line "$line" && continue header=$(parse_kmsg_header "$line") || return $? read prio di_seek[DI_SEQ] di_seek[DI_TIMESTAMP] < <(echo "$header") done < <(echo "$last_lines") [ ${#di_seek[@]} -eq $DI_SIZE ] || return $ERR_GENERIC echo "${di_seek[@]}" } trace_kmsg_dumper_sig() { local pid local pid_file debug "Trace kmsg dumper sig\n" > "$KMSG_DUMPER_EXIT_FILE" || true sleep $(msec_to_sec $((kmsg_counter_poll_period_ms * 2))) # wait for the counter process to exit "$RM" -f "$KMSG_DUMPER_EXIT_FILE" || true for pid_file in "$KMSG_DUMPER_PID_FILE" "$KMSG_COUNTER_PID_FILE"; do pid=$(cat "$pid_file" 2> /dev/null) || true if [ -n "$pid" ]; then "$KILL" "$pid" 2> /dev/null || true wait "$pid" 2> /dev/null || true fi "$RM" -f "$pid_file" || true done if [ -n "${KMSG_INFO_FILE:+x}" ]; then "$RM" -f "$KMSG_INFO_FILE" 2> /dev/null fi if [ -n "${KMSG_LINE_COUNT_FILE:+x}" ]; then "$RM" -f "$KMSG_LINE_COUNT_FILE" 2> /dev/null fi exit 0 } trace_kmsg_dumper() { local -a di_seek local kmsg_info local line_offset set -eu KMSG_COUNTER_PID_FILE="$TRACE_TMP_DIR/counter-pid" KMSG_DUMPER_PID_FILE="$TRACE_TMP_DIR/dumper-pid" KMSG_DUMPER_EXIT_FILE="$TRACE_TMP_DIR/dumper-exiting" trap trace_kmsg_dumper_sig EXIT err=0 KMSG_INFO_FILE="$TRACE_TMP_DIR/kmsg-info" get_kmsg_info > "$KMSG_INFO_FILE" || return $? read -a di_seek < "$KMSG_INFO_FILE" || return $? "$RM" "$KMSG_INFO_FILE" || return $? KMSG_INFO_FILE="" line_offset=$((di_seek[DI_REC] - KMSG_STARTUP_LAST_LINES)) [ $line_offset -lt 0 ] && line_offset=0 trace_write_dumper_info "$KMSG_DUMPER_SEEK_FILE" \ di_seek count_kmsg_recs $line_offset \ "$KMSG_COUNTER_PID_FILE" \ "$KMSG_DUMPER_FEED_FILE" & dump_kmsg_recs $line_offset \ "$KMSG_DUMPER_PID_FILE" wait } trace_write_info() { local -nr __ri_write=$1 echo "${__ri_write[@]}" > "$TRACE_INFO_FILE" || cmd_err_ret } trace_clear_recs() { local -nr __ri_clear=$1 local -a ri=("${__ri_clear[@]}") ri[RI_FLAGS]=$((ri[RI_FLAGS] | TRACE_FLAG_CLEARED)) trace_write_info ri } trace_update_rec_info() { local -a ri read -a ri < "$TRACE_INFO_FILE" || cmd_err_ret if [ ${kmsg_batch[KB_RECS_ADDED]} -eq 0 -a \ ${kmsg_batch[KB_SEQ]} -eq ${ri[RI_KMSG_SEQ]} -a \ ${kmsg_batch[KB_TIMESTAMP]} -eq ${ri[RI_KMSG_TIMESTAMP]} ]; then return 0 fi ri[RI_NUM_RECS]=$((ri[RI_NUM_RECS] + kmsg_batch[KB_RECS_ADDED])) ri[RI_KMSG_SEQ]=${kmsg_batch[KB_SEQ]} ri[RI_KMSG_TIMESTAMP]=${kmsg_batch[KB_TIMESTAMP]} trace_write_info ri } self_fd() { echo "/proc/self/fd/$1" } trace_truncate_recs() { local keep_lines local cleared=false local err local tmp_file local in_fd_path=$(self_fd "$TRACE_REC_IN_FD") local out_fd_path=$(self_fd "$TRACE_REC_OUT_FD") local -a ri read -a ri < "$TRACE_INFO_FILE" || cmd_err_ret [ $((ri[RI_FLAGS] & TRACE_FLAG_CLEARED)) -ne 0 ] && cleared=true if ! $cleared && \ [ ${ri[RI_NUM_RECS]} -lt $((TRACE_MAX_LINES + TRACE_SLACK)) ]; then return 0 fi if $cleared; then keep_lines=${kmsg_batch[KB_RECS_ADDED]} ri[RI_FLAGS]=$((ri[RI_FLAGS] & ~TRACE_FLAG_CLEARED)) else keep_lines=$TRACE_MAX_LINES fi # below is only for consistency, not needed for now [ $keep_lines -gt ${ri[RI_NUM_RECS]} ] && keep_lines=${ri[RI_NUM_RECS]} tmp_file=$(mktemp) || cmd_err_ret "$DD" if="$in_fd_path" 2> /dev/null | \ "$TAIL" -n "$keep_lines" > "$tmp_file" || cmd_err_ret "$DD" if="$tmp_file" of="$out_fd_path" 2> /dev/null || cmd_err_ret "$RM" -f "$tmp_file" || cmd_err_ret ri[RI_FIRST_REC]=$((ri[RI_FIRST_REC] + ri[RI_NUM_RECS] - keep_lines)) ri[RI_NUM_RECS]=$keep_lines trace_write_info ri || return $? return $ERR_REC_FILE_CHANGED } update_rec_file_locked() { trace_update_rec_info || return $? trace_truncate_recs } trace_get_record_info_locked() { local -a ri local -a ret local first_rec num_recs read -a ri < "$TRACE_INFO_FILE" || cmd_err_ret first_rec=${ri[RI_FIRST_REC]} num_recs=${ri[RI_NUM_RECS]} if [ $((ri[RI_FLAGS] & TRACE_FLAG_CLEARED)) -ne 0 ]; then first_rec=$((first_rec + num_recs)) num_recs=0 fi ret=($first_rec $num_recs ${ri[RI_KMSG_SEQ]} ${ri[RI_KMSG_TIMESTAMP]}) echo "${ret[@]}" } trace_get_record_info() { call_with_flock "$TRACE_LOCK_FILE" trace_get_record_info_locked } trace_server_sig_exit() { local pid debug "Trace server sig\n" trap - EXIT INT for pid in $trace_server_kmsg_dumper_pids; do "$KILL" "$pid" 2> /dev/null || true wait "$pid" 2> /dev/null || true done "$RM" "$trace_server_kmsg_dumper_fifo" exit 0 } get_dmesg_device() { local line=$1 local device device=${line# DEVICE=} [ "$device" = "$line" ] && device="" echo "$device" [ -n "$device" ] } get_dmesg_subsystem() { local line=$1 local subsystem subsystem=${line# SUBSYSTEM=} [ "$subsystem" = "$line" ] && subsystem="" echo "$subsystem" [ -n "$subsystem" ] } check_kmsg_prio() { local prio=$1 if ! valid_number "$prio"; then log_err "Invalid dmesg priority: $prio\n" return 1 fi if [ $prio -gt $max_prio ]; then return 1 fi return 0 } log_kmsg_err() { local label=$1 local seq=$2 local timestamp=$3 log_err "$label: \"$seq\"/\"$timestamp\" (batch: ${kmsg_batch[KB_SEQ]}/${kmsg_batch[KB_TIMESTAMP]})\n" } check_kmsg_header() { local line=$1 local prio seq timestamp prio=${line%%,*} line=${line#*,} seq=${line%%,*} line=${line#*,} timestamp=${line%%,*} kmsg_batch[KB_RECS_READ]=$((kmsg_batch[KB_RECS_READ] + 1)) if [ ${kmsg_batch[KB_SEQ]} -eq -1 ]; then kmsg_batch[KB_SEQ]=$((seq - 1)) kmsg_batch[KB_TIMESTAMP]=$timestamp fi if [ "$seq" -le ${kmsg_batch[KB_SEQ]} ] || [ "$seq" -gt $((kmsg_batch[KB_SEQ] + 1000)) ] || [ "$timestamp" -lt \ $((${kmsg_batch[KB_TIMESTAMP]} - KMSG_MAX_TIMESTAMP_DELTA_US)) ]; then log_kmsg_err "Non-consecutive kmsg seq/timestamp" "$seq" "$timestamp" return $ERR_GENERIC fi if [ "$seq" -ne $((kmsg_batch[KB_SEQ] + 1)) ]; then log_kmsg_err "Resetting kmsg seq" "$seq" "$timestamp" fi kmsg_batch[KB_SEQ]=$seq kmsg_batch[KB_TIMESTAMP]=$timestamp check_kmsg_prio "$prio" } process_dmesg_line() { local line=$1 local cont_lines=$2 [ -n "$line" ] || return 0 check_kmsg_header "$line" || return 0 echo "$cont_lines,$line" >& "$TRACE_REC_OUT_FD" || cmd_err_ret kmsg_batch[KB_RECS_ADDED]=$((kmsg_batch[KB_RECS_ADDED] + 1)) return 0 } kmsg_batch_emit() { local i local ret="" for ((i = KB_RECS_READ; i < KB_LAST_LINE; i++)); do ret+="${ret:+,}${kmsg_batch[i]}" done ret+=";${kmsg_batch[KB_LAST_LINE]}" echo "$ret" } kmsg_batch_get() { local info=$1 local -n __kb_get=$2 local header=${info%%;*} IFS=, read -a __kb_get < <(echo "$header") || cmd_err_ret __kb_get[KB_LAST_LINE]=${info#*;} } process_dmesg_line_batch() { local line local cont_lines="" local err=0 if [ ${kmsg_batch[KB_RECS_READ]} -ne 0 -o \ ${kmsg_batch[KB_RECS_ADDED]} -ne 0 ]; then pr_err "Unexpected batch read/added recs: ${kmsg_batch[@]}\n" kmsg_batch[KB_RECS_READ]=0 kmsg_batch[KB_RECS_ADDED]=0 fi line=${kmsg_batch[KB_LAST_LINE]} kmsg_batch[KB_LAST_LINE]="" while true; do if [ -z "$line" ] && \ ! { line=$(get_defragged_line 100) || get_err }; then break fi if is_cont_line "$line"; then cont_lines+="$line" line="" continue fi process_dmesg_line "${kmsg_batch[KB_LAST_LINE]}" "$cont_lines" || return $? kmsg_batch[KB_LAST_LINE]=$line if [ ${kmsg_batch[KB_RECS_READ]} -ge $MAX_BATCH_RECS -o \ ${kmsg_batch[KB_RECS_ADDED]} -ge $MAX_BATCH_RECS ]; then kmsg_batch_emit return 0 fi cont_lines="" line="" done [ $err -ne $ERR_READ_TIMEOUT ] && return $err process_dmesg_line "${kmsg_batch[KB_LAST_LINE]}" "$cont_lines" || return $? kmsg_batch[KB_LAST_LINE]="" kmsg_batch_emit return $ERR_READ_TIMEOUT } reopen_fds() { exec {TRACE_REC_OUT_FD}>&- || cmd_err_ret exec {TRACE_REC_IN_FD}>&- || cmd_err_ret exec {TRACE_REC_IN_FD}< "$TRACE_REC_FILE" || cmd_err_ret exec {TRACE_REC_OUT_FD}>> "$TRACE_REC_FILE" || cmd_err_ret } update_rec_file() { local err=0 call_with_flock "$TRACE_LOCK_FILE" \ update_rec_file_locked || err=$? kmsg_batch[KB_RECS_ADDED]=0 kmsg_batch[KB_RECS_READ]=0 [ $err -ne $ERR_REC_FILE_CHANGED ] && return $err reopen_fds } trace_server() { local last_line shopt -s extglob set -eu trap "" INT # handled by the main thread trap trace_server_sig_exit EXIT trace_server_kmsg_dumper_fifo="$TRACE_TMP_DIR/fifo" [ -p "$trace_server_kmsg_dumper_fifo" ] || \ mkfifo "$trace_server_kmsg_dumper_fifo" || cmd_err_ret while IFS= read -r kmsg_batch[KB_LAST_LINE]; do local kmsg_info local recs_added local err=0 while kmsg_info="$(process_dmesg_line_batch)" || get_err; do kmsg_batch_get "$kmsg_info" kmsg_batch || return $? update_rec_file || return $? done if [ $err -ne 0 -a $err -ne $ERR_READ_TIMEOUT ]; then pr_err "Trace server loop: unexpected error:$err, exiting\n" return $err fi kmsg_batch_get "$kmsg_info" kmsg_batch || return $? update_rec_file || return $? done < "$trace_server_kmsg_dumper_fifo" & trace_server_kmsg_dumper_pids+="$!" trace_kmsg_dumper > "$trace_server_kmsg_dumper_fifo" & trace_server_kmsg_dumper_pids+=" $!" wait } trace_is_clear_only_op() { local op=$1 [ "$op" -eq $TRACE_OP_CLEAR ] } trace_is_clear_op() { local op=$1 [ "$op" -eq $TRACE_OP_PRINT_AND_CLEAR -o \ "$op" -eq $TRACE_OP_CLEAR ] } trace_is_print_op() { local op=$1 [ "$op" -eq $TRACE_OP_PRINT_AND_CLEAR -o \ "$op" -eq $TRACE_OP_PRINT ] } trace_is_print_or_count_op() { local op=$1 trace_is_print_op "$op" || \ [ "$op" -eq $TRACE_OP_COUNT ] } trace_get_recs_locked() { local trace_op=$1 local since_rec=$2 local max_recs=$3 local dump_recs local dropped_recs local last_rec local -a ri read -a ri < "$TRACE_INFO_FILE" || cmd_err_ret last_rec=$((ri[RI_FIRST_REC] + ri[RI_NUM_RECS])) [ "$since_rec" -gt "$last_rec" ] && since_rec=$last_rec dropped_recs=${ri[RI_FIRST_REC]} if [ $((ri[RI_FLAGS] & TRACE_FLAG_CLEARED)) -ne 0 ]; then since_rec=$last_rec dropped_recs=$last_rec fi [ "$since_rec" -lt "${ri[RI_FIRST_REC]}" ] && \ since_rec=${ri[RI_FIRST_REC]} dump_recs=$((last_rec - since_rec)) [ $max_recs -ge 0 -a \ $dump_recs -gt $max_recs ] && dump_recs=$max_recs echo "$since_rec" "$dump_recs" "$dropped_recs" since_rec=$((since_rec - ri[RI_FIRST_REC])) # tail -n +1 is starts dumping from the first line if trace_is_print_or_count_op "$trace_op"; then tail -n +"$((since_rec + 1))" < "$TRACE_REC_FILE" | \ head -n "$dump_recs" || cmd_err_ret fi if trace_is_clear_op "$trace_op"; then trace_clear_recs ri fi } trace_get_recs() { local trace_op=$1 local since_rec=$2 local max_recs=$3 call_with_flock "$TRACE_LOCK_FILE" \ trace_get_recs_locked "$trace_op" "$since_rec" "$max_recs" } get_dmesg_prefix() { local -nr __dr_pfx=$1 local caller_id=${__dr_pfx[DR_CALLER_ID]:-} local time_sec local time_nsec local time time_sec=$((__dr_pfx[DR_TIMESTAMP] / 1000000)) time_nsec=$((__dr_pfx[DR_TIMESTAMP] % 1000000)) time_nsec=$(lpad_zero "$time_nsec" 6) time="$time_sec.$time_nsec" time="[$(lpad_space "$time" 13)]" [ -n "$caller_id" ] && caller_id="[$(lpad_space "$caller_id" 8)]" echo -n "${time}${caller_id}" } print_dmesg_line() { local indent=$1 local printed=$2 local rec_filter_match=$3 local -nr __dr_prn=$4 $printed || return 0 [ $rec_filter_match -eq $REC_FILTER_MATCH ] || return 0 echo -e "${indent}$(get_dmesg_prefix __dr_prn)${__dr_prn[DR_MESSAGE]:+ ${__dr_prn[DR_MESSAGE]}}" } print_repeated_dmesg_line() { local indent=$1 local printed=$2 local rec_filter_match=$3 local rep_count=$4 local -nr __dr_rep=$5 local prefix $printed || return 0 [ $rec_filter_match -eq $REC_FILTER_MATCH ] || return 0 case "$rep_count" in 0) return ;; 1) print_dmesg_line "$indent" "$printed" "$rec_filter_match" \ __dr_rep || return $? return ;; esac prefix="${indent}$(get_dmesg_prefix __dr_rep)" echo -e "${prefix} --- Previous line repeated $rep_count times" } rec_matches_filter() { local msg_config=$1 local msg_regex=$2 local dev_regex=$3 local -nr __dr_flt=$4 local pattern if [ -n "$dev_regex" ]; then if ! [[ "${__dr_flt[DR_DEVICE]}" =~ $dev_regex ]]; then return $REC_FILTER_UNMATCH fi fi if [ "$msg_config" -eq $TRACE_FILTER_NONE ]; then return $REC_FILTER_MATCH fi if [[ "${__dr_flt[DR_MESSAGE]}" =~ $msg_regex ]]; then [ "$msg_config" -eq $TRACE_FILTER_ONLY ] && \ return $REC_FILTER_MATCH else [ "$msg_config" -eq $TRACE_FILTER_EXCLUDE ] && \ return $REC_FILTER_MATCH fi return $REC_FILTER_UNMATCH } parse_dmesg_rec() { local line=$1 local -a dr local header cont header=${line%%;*} line=${line#*;} cont=${header%%,*} header=${header#*,} cont=${cont# } fld=${cont%% *} dr[DR_SUBSYSTEM]=${fld#SUBSYSTEM=} [ "${dr[DR_SUBSYSTEM]}" = "$fld" ] && \ dr[DR_SUBSYSTEM]="" fld=${cont#* } dr[DR_DEVICE]=${fld#DEVICE=} [ "${dr[DR_DEVICE]}" = "$fld" ] && \ dr[DR_DEVICE]="" dr[DR_PRIO]=${header%%,*} header=${header#*,} dr[DR_SEQ]=${header%%,*} header=${header#*,} dr[DR_TIMESTAMP]=${header%%,*} header=${header#*,} fld=${header%%,*} # options TODO: line continuation header=${header#*,} fld=${header%%,*} dr[DR_CALLER_ID]=${fld#caller=} [ "${dr[DR_CALLER_ID]}" = "$fld" ] && \ dr[DR_CALLER_ID]="" dr[DR_MESSAGE]="" IFS=, echo "${dr[*]};$line" IFS=$IFS_STD } parse_dmesg_rec_process() { local line while read -r line; do parse_dmesg_rec "$line" done } drecs_match() { local -nr dr1=$1 local -nr dr2=$2 [ "$((dr1[DR_SEQ] + 1))" = "${dr2[DR_SEQ]}" -a \ "${dr1[DR_SUBSYSTEM]:-}" = "${dr2[DR_SUBSYSTEM]:-}" -a \ "${dr1[DR_DEVICE]:-}" = "${dr2[DR_DEVICE]:-}" -a \ "${dr1[DR_PRIO]}" = "${dr2[DR_PRIO]}" -a \ "${dr1[DR_CALLER_ID]:-}" = "${dr2[DR_CALLER_ID]:-}" -a \ "${dr1[DR_MESSAGE]:-}" = "${dr2[DR_MESSAGE]:-}" ] } get_dr() { local dr_line=$1 local -n __dr_get=$2 local header=${dr_line%%;*} local message=${dr_line#*;} IFS=, read -a __dr_get < <(echo "$header") || cmd_err_ret __dr_get[DR_MESSAGE]=$message } trace_dump_recs() { local trace_op=$1 local filter_msg_config=$2 local filter_msg_regex=$3 local filter_dev_regex=$4 local since_rec=$5 local max_recs=$6 local indent=$(repeat_char " " $7) local first_rec num_recs dropped_recs local lines local dr_line local -a dr local -a dr_last=("" "" "" "" "" "" "") local line local last_line="" local rep_count=0 local printed=false local rec_filter_match=$REC_FILTER_UNMATCH local -a rec_count=(0 0) local err=0 lines=$(trace_get_recs "$trace_op" \ "$since_rec" "$max_recs") || return $? trace_is_clear_only_op "$trace_op" && return 0 trace_is_print_op "$trace_op" && printed=true read -r first_rec num_recs dropped_recs \ < <(echo "$lines" | head -1) || cmd_err_ret if $printed && [ $dropped_recs -gt 0 ]; then echo "${indent}Dropped/cleared $dropped_recs records" fi dr_last_set=0 while IFS= read dr_line; do dr_last_set=$((dr_last_set + 1)) if [ $err -ne 0 ] || test_state_must_break; then break fi get_dr "$dr_line" dr || return $? if drecs_match dr_last dr; then dr_last[DR_SEQ]=${dr[DR_SEQ]} dr_last[DR_TIMESTAMP]=${dr[DR_TIMESTAMP]} rep_count=$((rep_count + 1)) continue fi rec_count[rec_filter_match]=$((rec_count[rec_filter_match] + rep_count)) print_repeated_dmesg_line "$indent" "$printed" \ "$rec_filter_match" "$rep_count" \ dr_last || err=$? [ $err -ne 0 -a $err -ne $ERR_INT ] && return $err rep_count=0 dr_last=("${dr[@]}") rec_filter_match=$REC_FILTER_MATCH if ! rec_matches_filter "$filter_msg_config" \ "$filter_msg_regex" \ "$filter_dev_regex" \ dr; then rec_filter_match=$REC_FILTER_UNMATCH fi rec_count[rec_filter_match]=$((rec_count[rec_filter_match] + 1)) print_dmesg_line "$indent" "$printed" \ "$rec_filter_match" \ dr || err=$? [ $err -ne 0 -a $err -ne $ERR_INT ] && return $err done < <(echo "$lines" | stdbuf -oL tail -n +2 | parse_dmesg_rec_process) print_repeated_dmesg_line "$indent" "$printed" \ "$rec_filter_match" "$rep_count" \ dr_last || err=$? [ $err -ne 0 -a $err -ne $ERR_INT ] && return $err rec_count[rec_filter_match]=$((rec_count[rec_filter_match] + rep_count)) rc[RC_FIRST_REC]=$first_rec rc[RC_NUM_RECS]=$num_recs rc[RC_DROPPED_RECS]=$dropped_recs rc[RC_MATCH_RECS]=${rec_count[REC_FILTER_MATCH]} rc[RC_LAST_SEQ]=${dr_last[DR_SEQ]:-0} rc[RC_LAST_TIMESTAMP]=${dr_last[DR_TIMESTAMP]:-0} $printed || echo "${rc[@]}" test_state_must_break && err=$ERR_INT return $err } trace_init() { local -a ri=(0 0 0 0) local -a di=(-1 -1 -1) TRACE_TMP_DIR=$(mktemp -d) || cmd_err_ret debug "Temp dir: $TRACE_TMP_DIR\n" KMSG_DUMPER_LOCK_FILE="$TRACE_TMP_DIR/dumper-lock" KMSG_DUMPER_SEEK_FILE="$TRACE_TMP_DIR/dumper-seek" KMSG_DUMPER_FEED_FILE="$TRACE_TMP_DIR/dumper-feed" trace_files+=("$KMSG_DUMPER_FEED_FILE") trace_files+=("$KMSG_DUMPER_SEEK_FILE") trace_files+=("$KMSG_DUMPER_LOCK_FILE") TRACE_LOCK_FILE="$TRACE_TMP_DIR/trace-lock" TRACE_INFO_FILE="$TRACE_TMP_DIR/trace-info" TRACE_REC_FILE="$TRACE_TMP_DIR/trace-recs" trace_files+=("$TRACE_REC_FILE") trace_files+=("$TRACE_INFO_FILE") trace_files+=("$TRACE_LOCK_FILE") touch "${trace_files[@]}" || cmd_err_ret exec {TRACE_REC_IN_FD}< "$TRACE_REC_FILE" || cmd_err_ret exec {TRACE_REC_OUT_FD}> "$TRACE_REC_FILE" || cmd_err_ret exec {TRACE_INFO_IN_FD}< "$TRACE_INFO_FILE" || cmd_err_ret exec {TRACE_INFO_OUT_FD}> "$TRACE_INFO_FILE" || cmd_err_ret trace_write_dumper_info "$KMSG_DUMPER_SEEK_FILE" di || return $? trace_write_dumper_info "$KMSG_DUMPER_FEED_FILE" di || return $? trace_write_info ri || return $? trace_server & trace_server_pid=$! return 0 } trace_cleanup() { local tmp_dir debug "Trace cleanup: $trace_server_pid\n" if [ -n "$trace_server_pid" ]; then "$KILL" $trace_server_pid 2> /dev/null || true wait "$trace_server_pid" 2> /dev/null || true fi [ -n "${TRACE_INFO_OUT_FD+x}" ] && exec {TRACE_INFO_OUT_FD}>&- || true [ -n "${TRACE_INFO_IN_FD+x}" ] && exec {TRACE_INFO_IN_FD}>&- || true [ -n "${TRACE_REC_OUT_FD+x}" ] && exec {TRACE_REC_OUT_FD}>&- || true [ -n "${TRACE_REC_IN_FD+x}" ] && exec {TRACE_REC_IN_FD}>&- || true "$RM" -f "${trace_files[@]}" || true "$RMDIR" "$TRACE_TMP_DIR" || true } tbtools-0.8.0+git20260510.75be11e/scripts/type-utils.sh000066400000000000000000000063451517364422400220360ustar00rootroot00000000000000declare -r TYPE_ARRAY=array declare -r TYPE_OBJ=obj declare -rA type_codes=([$TYPE_ARRAY]=a [$TYPE_OBJ]=A) serialize_obj() { local -nr obj_ref=$1 local obj_values=() local k for k in "${!obj_ref[@]}"; do obj_values+=("[${k@Q}]=${obj_ref[$k]@Q}") done echo "${obj_values[*]}" } serialize_array() { local -nr array_ref=$1 local array_name=${2:-$1} local array_values=() local i for ((i = 0; i < ${#array_ref[@]}; i++)); do array_values+=("${array_ref[$i]@Q}") done echo "${array_values[*]}" } __get_obj_values() { local -nr __list_obj_ref=$1 local obj_values=() local k for k in "${!__list_obj_ref[@]}"; do obj_values+=("[\"$k\"]=\"${__list_obj_ref["$k"]}\"") done echo "${obj_values[*]}" } get_obj_values() { echo "($(__get_obj_values $1))" } concat_obj_values() { echo "($(__get_obj_values $1) $(__get_obj_values $2))" } get_array_elements() { local -n __get_array_elements_ref=$1 local array_name=${2:-$1} local array_values=() local i for ((i = 0; i < ${#__get_array_elements_ref[@]}; i++)); do array_values+=("[$i]=\"${__get_array_elements_ref["$i"]}\"") done echo "(${array_values[*]})" } describe_obj() { local -nr __describe_obj_ref=$1 local obj_name=${2:-$1} echo "$obj_name=$(get_obj_values __describe_obj_ref)" } describe_array() { local -n __describe_array_ref=$1 local array_name=${2:-$1} echo "$array_name=$(get_array_elements __describe_array_ref)" } assert_var_defined() { local var=$1 [[ "$(declare -p $var 2> /dev/null)" ]] && return 0 err_exit "$var is not defined" } assert_type() { local -r obj_name=$1 local -r type_name=$2 local -r type_code=${type_codes["$type_name"]} assert_var_defined "$obj_name" [[ "$(declare -p $obj_name 2> /dev/null)" =~ "declare -${type_code}" ]] && return 0 err_exit "Type of $obj_name is not a(n) "$type_name" type" } assert_obj_type() { assert_type "$1" "$TYPE_OBJ" } assert_array_type() { assert_type "$1" "$TYPE_ARRAY" } assert_array_size() { local name=$1 local -nr __arr_assert=$2 local size=$3 [ "${#__arr_assert[@]}" -eq $size ] && return 0 err_exit "Array $name has incorrect size: ${#__arr_assert[@]} (expected $size)\n" } assert_empty_obj() { local -rn __assert_empty_obj_ref=$1 local -r obj_name=${2:-$1} assert_obj_type "$obj_name" [ -z "${!__assert_empty_obj_ref[*]}" ] && return 0 err_exit "Object not empty: $(describe_obj __assert_empty_obj_ref "$obj_name")" } assert_empty_array() { local -rn __assert_empty_array_ref=$1 local -r array_name=${2:-$1} assert_array_type "$array_name" [ -z "${__assert_empty_array_ref+set}" ] && return 0 if [ "${#__assert_empty_array_ref[@]}" -ne 0 ]; then err_exit "Array not empty: $(describe_array __assert_empty_array_ref "$array_name")" fi } check_keys() { local -rn exp_keys=$1 local -rn obj=$2 local -r obj_name=$3 local key if [ ${#exp_keys[@]} -ne ${#obj[@]} ]; then err_exit "Obj \"$obj_name\" has key mismatch: ${!obj[@]} (expected:${exp_keys[@]})" fi for key in "${exp_keys[@]}"; do if ! [ "${obj["$key"]+x}" ]; then err_exit "Missing key in obj \"$obj_name\": \"$key\"" fi done } obj_init() { local -rn __obj_ref=$1 assert_empty_obj "$2" eval "__obj_ref=$3" } array_init() { local -rn __array_ref=$1 assert_empty_array "$1" eval "__array_ref=$2" } tbtools-0.8.0+git20260510.75be11e/scripts/util.py000066400000000000000000000005651517364422400207100ustar00rootroot00000000000000import datetime # Convert hex string into integer def convert_int(fields, name): tmp = fields[name] if tmp: fields[name] = int(tmp, 16) # Convert timestamp into datetime def convert_timestamp(fields, name): tmp = fields[name] if tmp: dt = datetime.datetime.fromtimestamp(float(tmp)) fields[name] = dt.strftime("%Y-%m-%dT%H:%M:%S") tbtools-0.8.0+git20260510.75be11e/scripts/utils.sh000066400000000000000000000336431517364422400210600ustar00rootroot00000000000000shopt -s extglob COLOR_RED="" COLOR_GREEN="" COLOR_PURPLE="" COLOR_LIGHT_GREY="" COLOR_YELLOW="" COLOR_GREY="" COLOR_NONE="" declare -r USEC_PER_SEC=1000000 declare -r SIG_HUP=1 declare -r SIG_INT=2 declare -r SIG_QUIT=3 declare -r SIG_ILL=4 declare -r SIG_TRAP=5 declare -r SIG_ABRT=6 declare -r SIG_BUS=7 declare -r SIG_FPE=8 declare -r SIG_KILL=9 declare -r SIG_USR1=10 declare -r SIG_SEGV=11 declare -r SIG_USR2=12 declare -r SIG_PIPE=13 declare -r SIG_ALRM=14 declare -r SIG_TERM=15 declare -r SIG_STKFLT=16 declare -r SIG_CHLD=17 declare -r SIG_CONT=18 declare -r SIG_STOP=19 declare -r SIG_TSTP=20 declare -r SIG_TTIN=21 declare -r SIG_TTOU=22 declare -r SIG_URG=23 declare -r SIG_XCPU=24 declare -r SIG_XFSZ=25 declare -r SIG_VTALRM=26 declare -r SIG_PROF=27 declare -r SIG_WINCH=28 declare -r SIG_IO=29 declare -r SIG_PWR=30 declare -r SIG_SYS=31 declare -r SIG_RTMIN=34 declare -r SIG_RTMAX=64 declare -r ERR_NOEXEC=126 declare -r ERR_RESERVED_START=$ERR_NOEXEC declare -r ERR_NOENT=127 declare -r ERR_SIGNAL=128 sys_err_signal() { echo "$((ERR_SIGNAL + $1))" } # Convenience for common errors declare -r ERR_INT=$(sys_err_signal $SIG_INT) declare -r ERR_ALRM=$(sys_err_signal $SIG_ALRM) declare -r ERR_SCRIPT_START=240 declare -r ERR_CMD_GENERIC=$((ERR_SCRIPT_START - 1)) declare -r IFS_STD=$' \t\n' declare -r ASCII_ESC=$'\33' declare -r ASCII_ESC_SEQ="${ASCII_ESC}[" declare -r MIN_BASH_MAJOR=4 declare -r MIN_BASH_MINOR=1 declare -r SYSFS_DIR=/sys declare -r SYSFS_PCI_DEV_DIR=$SYSFS_DIR/bus/pci/devices declare -r SYSFS_DRM_CLASS_DIR=$SYSFS_DIR/class/drm declare -r INTEL_ALIAS_VENDOR_ID="0x8087" declare -r INTEL_VENDOR_ID="0x8086" declare -r TEST_STATE_RUN=0 declare -r TEST_STATE_PAUSE=1 declare -r TEST_STATE_BREAK=2 declare -r TEST_STATE_QUIT=3 declare -A test_state_names=( [$TEST_STATE_RUN]="run" [$TEST_STATE_PAUSE]="pause" [$TEST_STATE_BREAK]="break" [$TEST_STATE_QUIT]="quit" ) test_state=$TEST_STATE_RUN logger_at_line_start=true debug_enabled=false debug_file= utf8_supported=false # Reserve error codes starting from ERR_SCRIPT_START to script errors. # If commands return an error code in this range (unlikely?) fold these # to ERR_CMD_GENERIC. to_err() { local err=$? [ $err -gt $ERR_CMD_GENERIC ] && err=$ERR_CMD_GENERIC echo "$err" } is_cmd_err() { [ $1 -lt $ERR_SCRIPT_START ] } bit() { local bit_pos=$1 echo "$((1 << bit_pos))" } min() { local a=$1 local b=$2 if [ $a -lt $b ]; then echo "$a" return fi echo "$b" } max() { local a=$1 local b=$2 if [ $a -gt $b ]; then echo "$a" return fi echo "$b" } debug() { $debug_enabled || return 0 if [ -n "${debug_file+x}" ]; then echo -ne "$@" >> "$debug_file" else echo -ne "$@" >&2 fi return 0 } enable_debug() { local file=${1-} debug_enabled=true debug_file="$file" } debug_is_enabled() { [ -n "${debug_enabled+x}" ] || return 1 $debug_enabled } epoch_usec() { local time=$EPOCHREALTIME local sec usec sec="${time/+([^0-9])*/}" usec="${time/+([0-9])+([^0-9])?(+(0))/}" [ -z "$usec" ] && usec=0 echo $((sec * 1000000 + usec)) } debug_delta_start() { local label=$1 debug "$label: start\n" echo $(epoch_usec) } debug_delta_end() { local label=$1 local start=$2 local end=$(epoch_usec) local delta local txt="" [ -z "$start" ] && return delta=$((end - start)) if [ $delta -gt 1000000 ]; then txt="$((delta / 1000000))s " fi txt+="$((delta % 1000000))u" debug "$label: end +$txt\n" echo "$end" } pr_err() { echo -ne "$@" >&2 } err_exit() { pr_err -ne "$@" exit 1 } check_bash_version() { local major minor trail major=${BASH_VERSION%%.*} trail=${BASH_VERSION#*.} minor=${trail%%.*} if [ "$major" -lt "$MIN_BASH_MAJOR" -o \ \( "$major" -eq "$MIN_BASH_MAJOR" -a "$minor" -lt "$MIN_BASH_MINOR" \) ]; then err_exit "Bash version too old:$BASH_VERSION (required at least $MIN_BASH_MAJOR.$MIN_BASH_MINOR)\n" fi } valid_number() { local number=$1 [ -z "${number}" -o -n "${number/#+([0-9])/}" ] && return 1 return 0 } sec_to_time() { local sec=$1 local str="" if [ $sec -ge $((60 * 60)) ]; then str+="$((sec / 60 / 60))h" sec=$((sec % (60 * 60))) fi if [ -n "$str" -o $sec -ge 60 ]; then str+="$((sec / 60))m" sec=$((sec % 60)) fi str+="${sec}s" echo "$str" } sec_to_date_time() { local seconds=$1 "$DATE" +"%Y-%m-%d %H:%M:%S" -d @"$seconds" } get_uptime_sec() { local uptime=$(< /proc/uptime) echo "${uptime%%.*}" } msec_to_sec() { local ms=$1 echo $((ms / 1000)).$((ms % 1000)) } unbuffered_echo() { "$STDBUF" -oL echo "$@" } log_cont() { local line=$1 local start_nl=false local end_nl=false if [ "${line:0:2}" = '\n' ]; then start_nl=true line=${line:2} fi if [ "${line: -2}" = '\n' -o "${line: -2}" = '\r' ]; then end_nl=true fi if $start_nl && ! $logger_at_line_start; then unbuffered_echo fi unbuffered_echo -ne "$line" logger_at_line_start=$end_nl } log() { log_cont "\n$@" >&2 } log_err() { log "${COLOR_RED}$@${COLOR_NONE}" >&2 } log_cr() { log_cont "\r" >&2 } char_to_int() { local char=$1 "$PRINTF" "%d" "'$char" } int_to_char() { local int=$1 echo -e "\x$("$PRINTF" "%x" $int)" } is_ctrl_char_code() { local code=$1 [ $code -eq 127 ] && return 0 [ $code -lt 32 ] && return 0 return 1 } ctrl_char_code_ascii_code() { local code=$1 if [ $code -eq 127 ]; then echo -- "-1" fi echo "$code" } int_to_ascii_ctrl_seq() { local int=$1 echo "^$(int_to_char "$(($base + $int))")" } repeat_char() { local c=$1 local n=$2 local str="" for ((i = 0; i < $n; i++)); do str+="$c" done echo -n "$str" } declare -r pad_spaces=" " declare -r pad_zeroes="000000000000000000000000000000000000000000000000000000" lpad_space() { local str=$1 local n=$2 echo "${pad_spaces:0:$((n - ${#str}))}$str" } rpad_space() { local str=$1 local n=$2 echo "$str${pad_spaces:0:$((n - ${#str}))}" } lpad_zero() { local str=$1 local n=$2 echo "${pad_zeroes:0:$((n - ${#str}))}$str" } emph_first() { local str=$1 echo "${COLOR_YELLOW}${str:0:1}${COLOR_NONE}${str:1}" } emph_paren_first() { local str=$1 echo "(${COLOR_YELLOW}${str:0:1}${COLOR_NONE})${str:1}" } is_complete_esc_seq() { local seq=$1 local len=${#seq} local last_char=${seq: -1} case $len in 1) [ "$last_char" != "$ASCII_ESC" ] && return 0 ;; 2) [ "$last_char" != "[" ] && return 0 ;; *) [ $len -ge 32 ] && return 0 # a random limit [[ "$last_char" =~ [A-Za-z~] ]] && return 0 esac return 1 } append_esc_seq() { local seq=$1 local char=$2 if [ -n "$esc_seq" ]; then esc_seq+="$char" if is_complete_esc_seq "$esc_seq"; then esc_seq="" fi echo "$esc_seq" return 0 fi if [ "$char" = "$ASCII_ESC" ]; then esc_seq=$char echo "$esc_seq" return 0 fi echo "" return 1 } get_str_len() { local str=$(echo -e "$1") local esc_seq="" local str_len=0 local char_len=1 local i for ((i = 0; i < "${#str}"; i += char_len)) { local char=${str:i:1} char_len=1 esc_seq=$(append_esc_seq "$esc_seq" "$char") && continue str_len=$((str_len + 1)) } echo "$str_len" } cursor_move_back() { local n=$1 local back back=$(repeat_char "\b" $n) || return $? [ $n -gt 0 ] || return echo -ne "$back" } cursor_erase_back() { local n=$1 local spaces spaces=$(repeat_char " " $n) || return $? [ $n -gt 0 ] || return cursor_move_back $n || return $? echo -ne "$spaces" cursor_move_back $n || return $? } cursor_update_back() { local old_len=$1 local new_len=$2 local erase_len=$((old_len - new_len)) [ $erase_len -lt 0 ] && erase_len=0 cursor_erase_back $erase_len cursor_move_back $((old_len - erase_len)) } update_back_if_changed() { local old_str=$1 local new_str=$2 local old_len local new_len old_len=$(get_str_len "$old_str") || return $? new_len=$(get_str_len "$new_str") || return $? [ ${#old_str} -eq ${#new_str} ] && return cursor_update_back $old_len $new_len } cursor_erase_str() { local str=$1 local len=$(get_str_len "$str") cursor_erase_back "$(get_str_len "$str")" } escape_char_to_ascii_notation() { local char=$1 local char_code=$(char_to_int "$char") local base=$(char_to_int "@") if ! is_ctrl_char_code "$char_code"; then echo "$char" return fi int_to_ascii_ctrl_seq "$char_code" } int_to_hex_esc_seq() { local int=$1 "$PRINTF" '\\x%02x' $int } escape_string_to_ascii_notation() { local str=$1 local esc_str="" local i for ((i = 0; i < ${#str}; i++)); do esc_str+="$(escape_char_to_ascii_notation "${str:$i:1}")" done echo "$esc_str" } parse_ascii_seq() { local ascii_seq=$1 local seq_table=( "[A:\u2191:^Up" "[B:\u2193:^Down" "[C:\u2192:^Right" "[D:\u2190:^Left" "[5~:\u21D1:^PgUp" "[6~:\u21D3:^PgDown" "" ) local seq_ascii seq_unicode seq_notation for seq in "${seq_table[@]}"; do [ -z "$seq" ] && break IFS=: read -r seq_ascii seq_unicode seq_notation < <(echo "$seq") [ "$seq_ascii" != "${ascii_seq:1}" ] || break done if [ -z "$seq" ]; then echo "$(escape_string_to_ascii_notation ${ascii_seq})" return fi if $utf8_supported; then echo -e "$seq_unicode" return fi echo "$seq_notation" } get_symbol_for_char() { local seq=$1 if [ "${seq:0:1}" = $ASCII_ESC ]; then parse_ascii_seq "$seq" return fi echo "$(escape_string_to_ascii_notation ${seq})" } read_ascii_esc_seq() { local char local seq="$ASCII_ESC" while true; do if ! read -r -n 1 -t 0.1 char; then break fi seq="${seq}${char}" is_complete_esc_seq "$seq" && break done echo "$seq" } read_char() { local timeout=$1 local char="" local tmo_opt="-t $timeout" local ascii_seq local ret=0 if [ "$timeout" = "-1" ]; then tmo_opt="" fi read -n 1 -r $tmo_opt char || ret=$? [ $ret -ne 0 ] && return 1 if [ "$char" = $ASCII_ESC ]; then char="$(read_ascii_esc_seq)" fi echo -n "$char" return 0 } flush_input() { local char while char=$(read_char 0.1); do : done } plural_str() { local num=$1 local str=$2 [ $num -gt 1 ] && str+="s" echo "$str" } shell_esc() { local str=$1 str=${str//\\/\\\\} str="${str//\"/\\\"}" str="${str//$/\\$}" echo "$str" } script_param_inst() { local str=$1 str=${str//\{\}/\"} echo "$str" } test_state_is() { local state=$1 [ $test_state -eq $state ] } test_state_is_run() { test_state_is $TEST_STATE_RUN } test_state_must_break() { [ $test_state -ge $TEST_STATE_BREAK ] } test_state_must_quit() { [ $test_state -ge $TEST_STATE_QUIT ] } test_state_set() { test_state_must_quit && return # refuse changing from quit test_state=$1 } test_state_name() { local state=$1 echo "${test_state_names[$state]}" } get_ssh_server_address() { local ip_address local ip_octet local ip_port local i local -r address_regex="^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3}) ([0-9]{1,6})\b" if ! [ -v SSH_CONNECTION ]; then pr_err "The SSH_CONNECTION environment variable is not set.\n" return 1 fi if ! [[ $SSH_CONNECTION =~ $address_regex ]]; then pr_err "Malformed IP address/port in SSH_CONNECTION variable\n" return 1 fi for ((i=1; i<=4; i++)); do ip_octet=${BASH_REMATCH[i]} if [[ $ip_octet -gt 255 ]]; then pr_err "Invalid IP address octet \"$ip_octet\" in SSH_CONNECTION variable\n" return 1 fi ip_address+="${ip_address:+.}$ip_octet" done ip_port=${BASH_REMATCH[5]} if [ $ip_port -lt 1024 ]; then pr_err "Invalid SSH client IP port \"$ip_port\"\n" return 1 fi echo "$ip_address:$ip_port" } cache_sudo_right() { [ "$(id -u)" -eq 0 ] && return 0 [[ -v SUDO ]] || err_exit "SUDO tool is not initialized\n" $SUDO true || return $? } init_raw_term() { saved_stty_config=$("$STTY" -g) "$STTY" -raw -echoctl -echo } cleanup_raw_term() { # explicit input, accounting for calls from a signal interrupt # handler at a time where stdin was redirected (for instance an # error interrupting a pending read < $file). # saved_stty_config could be unset if a signal triggered before # init_raw_term() [ -z "${saved_stty_config+x}" ] && return 0 "$STTY" "$saved_stty_config" < /dev/tty } is_modern_terminal() { case "$TERM" in linux | xterm*) return 0 esac return 1 } term_supports_colors() { # is stdout a terminal? [ -t 1 ] || return 1 is_modern_terminal && return 0 [ -v TPUT ] || return 1 if [ $(tput colors) -gt 1 ]; then return 0 fi return 1 } color_esc_seq() { local color_code1=$1 local color_code2=${2:-} echo "${ASCII_ESC_SEQ}$color_code1${2+;}${color_code2}m" } init_colors() { term_supports_colors || return 0 COLOR_RED=$(color_esc_seq 0 31) COLOR_GREEN=$(color_esc_seq 0 32) COLOR_PURPLE=$(color_esc_seq 1 35) COLOR_LIGHT_GREY=$(color_esc_seq 37) COLOR_YELLOW=$(color_esc_seq 1 33) COLOR_GREY=$(color_esc_seq 0 90) COLOR_NONE=$(color_esc_seq 0) } init_locale() { local lc_ctype [ -z "${LOCALE+x}" ] && return 0 lc_ctype=$("$LOCALE" | "$GREP" LC_CTYPE) || return 0 lc_ctype=${lc_ctype%\"} [ "${lc_ctype%UTF-8}" != "$lc_ctype" ] && utf8_supported=true return 0 } get_columns() { local size=$("$STTY" size) echo "${size#* }" } sanitize_pci_id_component() { local id=$1 id="0x${id#0x}" id=${id,,} echo "$id" } sanitize_pci_vendor_id() { local vendor=$(sanitize_pci_id_component "$1") get_canonical_intel_vendor_id "$vendor" } sanitize_pci_device_id() { sanitize_pci_id_component "$1" } get_pci_dev_address() { local vendor=$1 local device=$2 local dir for dir in $SYSFS_PCI_DEV_DIR/*; do local iter_vendor iter_device iter_vendor=$(sanitize_pci_vendor_id $(cat "$dir"/vendor)) iter_device=$(sanitize_pci_device_id $(cat "$dir"/device)) [ "$iter_vendor" = "$vendor" ] || continue [ "$iter_device" = "$device" ] || continue echo $(basename "$dir") return 0 done return 1 } call_with_flock() { local lock_file=$1 local fn=$2 shift 2 ( flock "$fd" || exit 1 $fn "$@" ) {fd}> "$lock_file" } init_utils() { check_bash_version if [ $(get_columns) -lt 60 ]; then pr_err "Terminal too narrow\n" return 1 fi # Move the following out from the library init_raw_term || return $? init_colors || return $? init_locale || return $? return 0 } cleanup_utils() { cleanup_raw_term } tbtools-0.8.0+git20260510.75be11e/scripts/verify-sysfs.sh000077500000000000000000000665541517364422400224030ustar00rootroot00000000000000#!/bin/bash # SPDX-License-Identifier: GPL-2.0-only # Copyright (c) 2017 Intel Corporation # @Desc Authorize thunderbolt devices and list detailed stuffs under tbt devices # Author: Pengfei Xu # Created date: July. 27, 2017 CARGO_PATH="/root/.cargo/bin:~/.cargo/bin" export PATH=${PATH}:$CARGO_PATH:./ TIME_FMT="%Y%m%d-%H%M%S.%N" readonly TBAUTH="tbauth" readonly TBLIST="tblist" TBT_DEV_FILE="/tmp/tbt_name.txt" TBT_PATH="/sys/bus/thunderbolt/devices" REGEX_DEVICE="-" REGEX_DOMAIN="domain" DEV_FILE="/tmp/tbt_dev" DEV_LIST="/tmp/dev_list" export DEVICE_FILES="authorized device device_name link_speed link_width nvm_authenticate nvm_version uevent unique_id vendor vendor_name power/control" export POWER_FILES="power/control power/runtime_status power/runtime_enabled" export DOMAIN_FILES="security uevent" AUTHORIZE_FILE="authorized" TOPO_FILE="/tmp/tbt_topo.txt" PCI_PATH="/sys/bus/pci/devices" HOST_EXCLUDE="\-0" PCI_HEX_FILE="/tmp/pci_hex.txt" PCI_DEC_FILE="/tmp/pci_dec.txt" PCI_HEX="" PCI_DEC="" DEV_TYPE="" DEV_SERIAL="" DEV_PCI="" TBT_DEV_NAME="" STUFF_FILE="/tmp/tbt_stuff.txt" TBT_STUFF_LIST="/tmp/tbt_stuff_list.txt" PF_BIOS="" TBT_NUM="" ROOT_PCI="" SYS_PATH="" VERBOSE="" usage() { cat <<-EOF >&2 usage: ./${0##*/} [-v verbose] [-h help] [-s only show topo] Or no need parameter EOF exit 0 } test_print_trc() { log_info=$1 # trace information echo "|$(date +"$TIME_FMT")|TRACE|$log_info|" echo "|$(date +"$TIME_FMT")|TRACE|$log_info|" >> /root/test_tbt_1.log } tbt_us_pci() { local domianx=$1 local pcis="" local pci="" local pci_us="" local pci_content="" local dr_pci_h="" local dr_pci_d="" [[ -n "$ROOT_PCI" ]] || { echo "Could not find tbt root PCI, exit!" exit 2 } # dr_pci_h: BUS /devices/pci0000:00/0000:00:0d.3/domain1 -> 00 dr_pci_h=$(udevadm info -q path --path=${TBT_PATH}/domain"$domianx" \ | awk -F "/" '{print $(NF-1)}' \ | cut -d ':' -f 2) dr_pci_d=$((0x"$dr_pci_h")) pcis=$(ls -1 $PCI_PATH) for pci in $pcis; do pci_us="" PCI_HEX="" PCI_DEC="" pci_content=$(ls -ltra "$PCI_PATH"/"$pci") [[ "$pci_content" == *"$ROOT_PCI"* ]] || continue pci_us=$(lspci -v -s "$pci" | grep -i upstream) if [[ -z "$pci_us" ]]; then continue else # For debug # echo "Upstream pci:$pci" PCI_HEX=$(echo "$pci" | cut -d ':' -f 2) PCI_DEC=$((0x"$PCI_HEX")) # Due to ICL tbt driver PCI 00:0d.2 and 00:0d.3 # ICL no impact, due to ICL dr pci is 00 [[ "$PCI_DEC" -gt "$dr_pci_d" ]] || { #echo "$PCI_DEC not greater than 3, skip" continue } echo "$PCI_HEX" >> $PCI_HEX_FILE echo "$PCI_DEC" >> $PCI_DEC_FILE fi done # As follow is for debug #echo "TBT device upstream PCI in hex:" #cat $PCI_HEX_FILE #echo "TBT device upstream PCI in dec:" #cat $PCI_DEC_FILE } # Integrated TBT root PCI will be changed so need to verify and find it # Input: $1 TBT_ROOT_PCI # Return 0 for true, otherwise false or block_test verify_tbt_root_pci() { local root_pci=$1 local dev_path="/sys/devices/pci0000:00" local result="" local pf="" local pf_name="" local tbt_dev="" # get like 1-3 tbt_dev=$(ls ${TBT_PATH} \ | grep "$REGEX_DEVICE" \ | grep -v "$HOST_EXCLUDE" \ | grep -v ":" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2 \ | head -n1) pf=$(dmidecode --type bios \ | grep Version \ | cut -d ':' -f 2) pf_name=$(echo ${pf: 1: 4}) result=$(ls -1 $dev_path/"$root_pci" | grep "0000" | grep "07") if [[ -z "$result" ]]; then [[ "$tbt_dev" == *"-1"* ]] && { [[ "$root_pci" == *"0d.2" ]] && ROOT_PCI="0000:00:07.0" [[ "$root_pci" == *"0d.3" ]] && ROOT_PCI="0000:00:07.2" } [[ "$tbt_dev" == *"-3"* ]] && { [[ "$root_pci" == *"0d.2" ]] && ROOT_PCI="0000:00:07.1" [[ "$root_pci" == *"0d.3" ]] && ROOT_PCI="0000:00:07.3" } SYS_PATH="$dev_path/$ROOT_PCI" test_print_trc "Discrete or FW CM on $pf_name,ROOT_PCI:$ROOT_PCI, SYS_PATH:$SYS_PATH" elif [[ "$tbt_dev" == *"-1"* ]]; then ROOT_PCI=$(ls -1 $dev_path/"$root_pci" \ | grep "0000" \ | grep "07" \ | head -n 2 \ | head -n 1 \ | awk -F "pci:" '{print $2}') SYS_PATH="$dev_path/$ROOT_PCI" test_print_trc "Integrated on $pf_name, $tbt_dev $root_pci -> $ROOT_PCI" elif [[ "$tbt_dev" == *"-3"* ]]; then ROOT_PCI=$(ls -1 $dev_path/"$root_pci" \ | grep "0000" \ | grep "07" \ | head -n 2 \ | tail -n 1\ | awk -F "pci:" '{print $2}') SYS_PATH="$dev_path/$ROOT_PCI" test_print_trc "Integrated on $pf_name, $tbt_dev $root_pci -> $ROOT_PCI" elif [[ -z "$tbt_dev" ]]; then [[ "$root_pci" == *"0d.2" ]] && ROOT_PCI="0000:00:07.0" [[ "$root_pci" == *"0d.3" ]] && ROOT_PCI="0000:00:07.2" SYS_PATH="$dev_path/$ROOT_PCI" else die "Invalid tbt device sysfs:$tbt_dev" fi test_print_trc "TBT ROOT:$ROOT_PCI SYS_PATH:$dev_path/$ROOT_PCI" } find_root_pci() { local tbt_devs="" local pf_name="" local tbt_dev="" # get like 1-3 tbt sysfs folder tbt_dev=$(ls ${TBT_PATH} \ | grep "$REGEX_DEVICE" \ | grep -v "$HOST_EXCLUDE" \ | grep -v ":" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2 \ | head -n1) [[ -z "$tbt_dev" ]] && tbt_dev="0-0" pf_name=$(dmidecode --type bios \ | grep Version \ | cut -d ':' -f 2 \ | cut -d '.' -f 1) ROOT_PCI=$(udevadm info --attribute-walk --path=/sys/bus/thunderbolt/devices/"$tbt_dev" | grep KERNEL | tail -n 2 | grep -v pci0000 | cut -d "\"" -f 2) verify_tbt_root_pci "$ROOT_PCI" # For debug # echo "PF_BIOS:$PF_BIOS platform, ROOT_PCI:$ROOT_PCI" } enable_authorized() { local aim_folders="" local aim_folder="" local domain="" local route="" aim_folders=$(ls -1 ${TBT_PATH} \ | grep "$REGEX_DEVICE" \ | grep -v ":" \ | grep -v "0$" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2) [ -z "$aim_folders" ] && test_print_trc "Device folder does not exist" && return 1 for aim_folder in ${aim_folders}; do domain="" route="" domain=$(echo "$aim_folder" | awk -F "-" '{print $1}') route=$(echo "$aim_folder" | awk -F "-" '{print $2}') if [ -e "${TBT_PATH}/${aim_folder}/${AUTHORIZE_FILE}" ];then AUTHORIZE_INFO=$(cat "${TBT_PATH}/${aim_folder}/${AUTHORIZE_FILE}") if [ "$AUTHORIZE_INFO" == "0" ]; then test_print_trc "tbauth -d $domain -r $route: ${TBT_PATH}/${aim_folder}/${AUTHORIZE_FILE}" tbauth -d "$domain" -r "$route" || \ test_print_trc "Change ${TBT_PATH}/${aim_folder}/${AUTHORIZE_FILE} for $aim_folder failed!" sleep 4 else test_print_trc "${TBT_PATH}/${aim_folder}/${AUTHORIZE_FILE}: ${AUTHORIZE_INFO}" fi fi done } check_device_sysfs() { REGEX_TARGET=$1 AIM_FILE=$2 local aim_folders="" test_print_trc "____________________Now Cheking '$AIM_FILE'___________________" aim_folders=$(ls -1 ${TBT_PATH} \ | grep "$REGEX_TARGET" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2) [ -z "$aim_folders" ] && test_print_trc "AIM floder does not exist" && return 1 for aim_folder in ${aim_folders}; do if [ -e "${TBT_PATH}/${aim_folder}/${AIM_FILE}" ];then #test_print_trc "File $AIM_FILE is found on $TBT_PATH/$aim_folder" FILE_INFO=$(cat ${TBT_PATH}/"$aim_folder"/"$AIM_FILE") if [ "$FILE_INFO" != "" ]; then test_print_trc "${TBT_PATH}/${aim_folder}/${AIM_FILE}: |$FILE_INFO" else test_print_trc "TBT file $TBT_PATH/${aim_folder}/${AIM_FILE} is null, should not be null" return 1 fi else test_print_trc "File $AIM_FILE is not found or not a file on $TBT_PATH/$aim_folder" continue fi done return $? } fill_key() { local source_key=$1 local aim_folder=$2 local key_file="key" local key_path="$HOME/keys" local home_key="" local verify_key="" cat "$key_path"/"$source_key" > "$TBT_PATH"/"$aim_folder"/"$key_file" home_key=$(cat "$key_path"/"$source_key") verify_key=$(cat "$TBT_PATH"/"$aim_folder"/"$key_file") test_print_trc "${key_path}/${source_key}:$home_key" test_print_trc "${TBT_PATH}/${aim_folder}/${key_file}:$verify_key" } check_security_mode() { DOMAIN="domain0/security" [ -e ${TBT_PATH}/${DOMAIN} ] && SECURITY_RESULT=$(cat ${TBT_PATH}/${DOMAIN}) [ -z "$SECURITY_RESULT" ] && test_print_trc "SECURITY_RESULT is null." && \ return 1 echo "$SECURITY_RESULT" } check_device_file() { for DEVICE_FILE in ${DEVICE_FILES}; do check_device_sysfs "$REGEX_DEVICE" "$DEVICE_FILE" done test_print_trc "Check power sysfs files" for POWER_FILE in ${POWER_FILES}; do check_device_sysfs "$REGEX_DEVICE" "$POWER_FILE" done } check_domain_file() { for DOMAIN_FILE in ${DOMAIN_FILES}; do check_device_sysfs "$REGEX_DOMAIN" "$DOMAIN_FILE" done } check_32bytes_key() { local key_file=$1 local key_content="" local key_len="" local key_number=64 if [ -e "$key_file" ]; then test_print_trc "$key_file already exist" key_content=$(cat "$key_file") key_len=${#key_content} if [ "$key_len" -eq "$key_number" ]; then test_print_trc "$key_file lenth is 64, ok" return 0 else test_print_trc "$key_file lenth is not 64:$key_content" test_print_trc "Key lenth wrong, regenerate $key_file" openssl rand -hex 32 > "$key_file" return 2 fi else test_print_trc "No key file, generate $key_file" openssl rand -hex 32 > "$key_file" return 1 fi } wrong_password_check() { local key_file="key" local key_path="$HOME/keys" local aim_folder=$1 local error_key="error.key" local key_content="" local key_len="" local compare="" local author_result="" local author_file="authorized" if [ -e "${TBT_PATH}/${aim_folder}/${key_file}" ];then if [ -e "${TBT_PATH}/${aim_folder}/${author_file}" ]; then author_result=$(cat ${TBT_PATH}/${aim_folder}/${author_file}) if [ "$author_result" != "0" ]; then test_print_trc "${TBT_PATH}/${aim_folder}/${author_file}:$author_result" test_print_trc "authorized already passed, skip" author_result="" return 1 else author_result="" fi fi check_32bytes_key "${key_path}/${error_key}" if [ -e "${key_path}/${aim_folder}.key" ]; then compare=$(diff "${key_path}/${error_key}" "${key_path}/${aim_folder}.key") if [ -z "$compare" ]; then test_print_trc "${key_path}/${error_key} the same as correct one, regenerate" openssl rand -hex 32 > "${key_path}/${error_key}" fi fi fill_key "$error_key" "$aim_folder" test_print_trc "fill 2 into ${TBT_PATH}/${aim_folder}/${author_file}:" echo 2 > "${TBT_PATH}/${aim_folder}/${author_file}" sleep 1 author_result=$(cat "${TBT_PATH}/${aim_folder}/${author_file}") if [ "$author_result" == "0" ]; then test_print_trc "${TBT_PATH}/${aim_folder}/${author_file}:$author_result passed" else test_print_trc "${TBT_PATH}/${aim_folder}/${author_file}:$author_result failed" fi else test_print_trc "File key is not found on $TBT_PATH/$aim_folder" fi } wrong_password_test() { local key_path="$HOME/keys" local regex_target=$REGEX_DEVICE local aim_folder="" local aim_folders="" local author_file="authorized" test_print_trc "Secure mode wrong passsword test next:" aim_folders=$(ls -1 ${TBT_PATH} \ | grep "$regex_target" \ | grep -v ":" \ | grep -v "0$" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2) [ -z "$aim_folders" ] && test_print_trc "aim_folders:aim_folders does not exist" && return 1 [ -d "$key_path" ] || mkdir "$key_path" for aim_folder in ${aim_folders}; do wrong_password_check "$aim_folder" done } secure_mode_test() { local key_path="$HOME/keys" local regex_target=$REGEX_DEVICE local aim_folder="" local domain="" local route="" local aim_folders="" local author_file="authorized" local time=3 local author_path="/sys/bus/thunderbolt/devices/*/authorized" local result="" test_print_trc "Secure mode verify correct passsword test next:" aim_folders=$(ls -1 ${TBT_PATH} \ | grep "$regex_target" \ | grep -v ":" \ | grep -v "0$" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2) [ -z "$aim_folders" ] && test_print_trc "AIM floder:$aim_folders does not exist" && return 1 [ -d "$key_path" ] || { rm -rf "$key_path" mkdir -p "$key_path" } for((i=1; i<=time; i++)); do result=$(grep -H . "$author_path" 2>/dev/null |awk -F ':' '{print $NF}' | grep 0) if [ -z "$result" ]; then test_print_trc "authorized all ok" break else test_print_trc "$i round set 2 to authorized for secure mode:" for aim_folder in ${aim_folders}; do domain=$(echo "$aim_folder" | awk -F "-" '{print $1}') route=$(echo "$aim_folder" | awk -F "-" '{print $2}') echo "tbauth -d domain -r route -A ${key_path}_${aim_folder}" tbauth -d domain -r route -A "${key_path}_${aim_folder}" echo "tbauth -d domain -r route -C ${key_path}_${aim_folder}" tbauth -d domain -r route -C "${key_path}_${aim_folder}" done fi done if [ "$i" -ge "$time" ]; then test_print_trc "[WARN] Need to check log carefully i reach $i, please check log!" test_print_trc "It's better unplug and plug the TBT devices and test again!" enable_authorized fi } # This function will view request domain and request tbt branch devices # and will write the topo result into $TOPO_FILE # Inuput: # $1: domain num, 0 for domain0, 1 for domain1 # $2: branch num, 1 for domainX-1, 3 for domainX-3 # Return: 0 for true, otherwise false or die topo_view() { local domainx=$1 local tn=$2 local tbt_sys="" local tbt_file="" local dev_name="device_name" local device_topo="" local file_topo="" local device_num="" # Get tbt sys file in connection order tbt_sys=$(ls -l ${TBT_PATH}/"$domainx"*"$tn" 2>/dev/null \ | grep "-" \ | awk '{ print length(), $0 | "sort -n" }' \ | tail -n 1 \ | awk -F "${REGEX_DOMAIN}${domainx}/" '{print $2}' \ | tr '/' ' ') [ -n "$tbt_sys" ] || { #echo "No tbt device in $domainx-$tn!" return 1 } # Get last file last=$(echo "$tbt_sys" | awk '{print $NF}') device_num=$(echo "$tbt_sys" | awk '{print NF-1}') # Last file not add <-> in the end for tbt_file in ${tbt_sys}; do device_file="" if [ "$tbt_file" == "$last" ]; then device_file=$(cat "${TBT_PATH}/${tbt_file}/${dev_name}" 2>/dev/null) device_topo=${device_topo}${device_file} file_topo=${file_topo}${tbt_file} else device_file=$(cat "${TBT_PATH}/${tbt_file}/${dev_name}" 2>/dev/null) [[ -n "$device_file" ]] || device_file="no_name" # For alignment for such as 0-0 and device name, device name is longer device_file_num=${#device_file} tbt_file_num=${#tbt_file} if [[ "$device_file_num" -gt "$tbt_file_num" ]]; then gap=$((device_file_num - tbt_file_num)) device_topo=${device_topo}${device_file}" <-> " file_topo=${file_topo}${tbt_file} for ((c=1; c<=gap; c++)); do file_topo=${file_topo}" " done file_topo=${file_topo}" <-> " else device_topo=${device_topo}${device_file}" <-> " file_topo=${file_topo}${tbt_file}" <-> " fi fi done echo "device_topo: $device_topo" >> $TOPO_FILE echo "file_topo : $file_topo" >> $TOPO_FILE tblist 2>/dev/null } topo_name() { local tbt_sys=$1 local devs_file=$2 local tbt_file="" local dev_name="device_name" local device_topo="" local file_topo="" [ -n "$tbt_sys" ] || { echo "No tbt device in tbt_sys:$tbt_sys" return 1 } # Get last file last=$(echo "$tbt_sys" | awk '{print $NF}') # Last file not add <-> in the end for tbt_file in ${tbt_sys}; do device_file="" if [ "$tbt_file" == "$last" ]; then device_file=$(cat "${TBT_PATH}/${tbt_file}/${dev_name}" 2>/dev/null) device_topo=${device_topo}${device_file} file_topo=${file_topo}${tbt_file} else device_file=$(cat "${TBT_PATH}/${tbt_file}/${dev_name}" 2>/dev/null) [[ -n "$device_file" ]] || device_file="no_name" # For alignment for such as 0-0 and device name, device name is longer device_file_num=${#device_file} tbt_file_num=${#tbt_file} if [[ "$device_file_num" -gt "$tbt_file_num" ]]; then gap=$((device_file_num - tbt_file_num)) device_topo=${device_topo}${device_file}" <-> " file_topo=${file_topo}${tbt_file} for ((c=1; c<=gap; c++)); do file_topo=${file_topo}" " done file_topo=${file_topo}" <-> " else device_topo=${device_topo}${device_file}" <-> " file_topo=${file_topo}${tbt_file}" <-> " fi fi done echo "device_topo: $device_topo" echo "device_topo: $device_topo" >> "$devs_file" echo "file_topo : $file_topo" echo "file_topo : $file_topo" >> "$devs_file" } usb4_view() { local domainx=$1 local tn=$2 local tbt_sys_file="/tmp/tbt_sys.txt" local tbt_devs="" local device_num="" local dev_item="" local check_point="" ls -l "$TBT_PATH"/"$domainx"*"$tn" 2>/dev/null \ | grep "-" \ | awk -F "${REGEX_DOMAIN}${domainx}/" '{print $2}' \ | awk '{ print length(), $0 | "sort -n" }' \ | grep -v ":" \ | grep -v "_" \ | cut -d ' ' -f 2 \ | tr '/' ' ' \ > $tbt_sys_file # need tbt devices in order tbt_devs=$(ls ${TBT_PATH} 2>/dev/null \ | grep "-" \ | grep -v ":" \ | grep "^${domainx}" \ | grep "${tn}$" \ | awk '{ print length(), $0 | "sort -n" }' \ | cut -d ' ' -f 2) device_num=$(ls ${TBT_PATH} \ | grep "^${domainx}" \ | grep -v ":" \ | grep "${tn}$" \ | wc -l) echo "$domainx-$tn contains $device_num tbt devices." echo "$domainx-$tn contains $device_num tbt devices." >> $TOPO_FILE cat /dev/null > "${DEV_FILE}_${domainx}_${tn}" cp -rf "$tbt_sys_file" "${DEV_FILE}_${domainx}_${tn}" for tbt_dev in $tbt_devs; do dev_item="" dev_item=$(cat "$tbt_sys_file" | grep "${tbt_dev}$") [[ -z "$dev_item" ]] && { echo "WARN:dev_item is null for tbt_dev:$tbt_dev" continue } check_point=$(cat "$tbt_sys_file" \ | grep -v "${dev_item}$" \ | grep "${dev_item}" \ | head -n 1) [[ -z "$check_point" ]] && { #echo "check_point for ${dev_item} is null" continue } sed -i "/${check_point}$/d" "${DEV_FILE}_${domainx}_${tn}" sed -i "s/${dev_item}$/${check_point}/g" "${DEV_FILE}_${domainx}_${tn}" done while IFS= read -r line do topo_name "$line" "$TOPO_FILE" done < "${DEV_FILE}_${domainx}_${tn}" } tbt_dev_name() { local domainx=$1 local tn=$2 local dev="" local tbt_devs="" local tbt_dev="" local dev_name="device_name" local cp="" cat /dev/null > "${DEV_LIST}_${domainx}_${tn}" while IFS= read -r line do for dev in $line; do cp="" cp=$(cat ${DEV_LIST}_"$domainx"_"$tn" | grep "$dev") [[ -z "$cp" ]] || continue [[ "$dev" == *"-0" ]] && continue echo "$dev" >> "${DEV_LIST}_${domainx}_${tn}" done done < "${DEV_FILE}_${domainx}_${tn}" # Get tbt dev file in connection order tbt_devs="" tbt_devs=$(cat "$DEV_LIST"_"$domainx"_"$tn") for tbt_dev in $tbt_devs; do echo "$tbt_dev" >> "$TBT_DEV_FILE" done } # This function will check how many tbt device connected and # show the tbt devices how to connect, which one connect with which one # Inuput: NA # Return: 0 for true, otherwise false or die topo_tbt_show() { # tbt spec design tbt each domain will seprate to like 0-1 or 0-3 branch local t1="1" local t3="3" local domains="" local domain="" local topo_result="" # domains example 0 1 domains=$(ls $TBT_PATH/ \ | grep "$REGEX_DOMAIN" \ | grep -v ":" \ | awk -F "$REGEX_DOMAIN" '{print $2}' \ | awk -F "->" '{print $1}') cat /dev/null > "$TBT_DEV_FILE" cat /dev/null > $TOPO_FILE for domain in ${domains}; do #topo_view "$domain" "$t1" usb4_view "$domain" "$t1" #topo_view "$domain" "$t3" usb4_view "$domain" "$t3" tbt_dev_name "$domain" "$t1" tbt_dev_name "$domain" "$t3" done topo_result=$(cat $TOPO_FILE) [[ -n "$topo_result" ]] || { echo "tbt $TOPO_FILE is null:$topo_result!!!" exit 2 } } only_show_topo() { # tbt spec design tbt each domain will seprate to like 0-1 or 0-3 branch local t1="1" local t3="3" local domains="" local domain="" # domains example 0 1 domains=$(ls $TBT_PATH/ \ | grep "$REGEX_DOMAIN" \ | grep -v ":" \ | awk -F "$REGEX_DOMAIN" '{print $2}' \ | awk -F "->" '{print $1}') cat /dev/null > "$TBT_DEV_FILE" cat /dev/null > $TOPO_FILE for domain in ${domains}; do #topo_view "$domain" "$t1" usb4_view "$domain" "$t1" #topo_view "$domain" "$t3" usb4_view "$domain" "$t3" done } authorize_show_tbt_devices() { SECURITY=$(check_security_mode) if [ "$SECURITY" == "user" ]; then for ((i = 1; i <= 10; i++)) do CHECK_RESULT=$(cat ${TBT_PATH}/*/${AUTHORIZE_FILE} | grep 0) if [ -z "$CHECK_RESULT" ]; then test_print_trc "All authorized set to 1" break else test_print_trc "$i round to enable authorized" enable_authorized fi done elif [ "$SECURITY" == "none" ]; then test_print_trc "Security Mode: $SECURITY" #check_authorized elif [ "$SECURITY" == "secure" ]; then test_print_trc "Security Mode: $SECURITY" wrong_password_test secure_mode_test elif [ "$SECURITY" == "dponly" ]; then test_print_trc "Security Mode: $SECURITY" else test_print_trc "Get wrong mode: $SECURITY" return 1 fi if [[ -n "$VERBOSE" ]]; then check_device_file check_domain_file fi echo "tblist:" tblist echo "tbt topo:" topo_tbt_show } check_usb_type() { local dev_node=$1 local speed="" speed=$(udevadm info --attribute-walk --name="$dev_node" \ | grep speed \ | head -n 1 \ | cut -d '"' -f 2) case $speed in 480) DEV_TYPE="USB2.0" ;; 5000) DEV_TYPE="USB3.0" ;; 10000) DEV_TYPE="USB3.1" ;; *) echo "WARN:$dev_node:USB unknow speed->$speed" DEV_TYPE="USB_unknow_type" ;; esac } stuff_in_tbt() { local dev_node=$1 local dev_pci_h="" local dev_pci_d="" local tbt_pci="" local num="" local num_add="" dev_pci_h=$(udevadm info --attribute-walk --name="$dev_node" \ | grep "looking" \ | head -n 1 \ | awk -F "0000:" '{print $NF}' \ | cut -d ':' -f 1) dev_pci_d=$((0x"$dev_pci_h")) for ((num=1;num<=TBT_NUM;num++)); do TBT_DEV_NAME="" DEV_PCI="" num_add=$((num+1)) [[ "$num_add" -gt "$TBT_NUM" ]] && { TBT_DEV_NAME=$(sed -n ${num}p $TBT_DEV_FILE) DEV_PCI=$dev_pci_h break } tbt_pci=$(sed -n ${num_add}p $PCI_DEC_FILE) if [[ "$dev_pci_d" -lt "$tbt_pci" ]]; then TBT_DEV_NAME=$(sed -n ${num}p $TBT_DEV_FILE) DEV_PCI=$dev_pci_h #echo "$dev_node pci:$DEV_PCI connected with $TBT_DEV_NAME" break else continue fi done [[ -n "$TBT_DEV_NAME" ]] || { echo " No detect $dev_node dev:$dev_pci_d us:$tbt_pci connected with which tbt device!!!" return 1 } } dev_under_tbt() { local dev_node=$1 local dev_tp="" DEV_SERIAL="" DEV_TYPE="" pci_dev=$(udevadm info --attribute-walk --name="$dev_node" \ | grep "KERNEL" \ | tail -n 2 \ | head -n 1 \ | awk -F '==' '{print $NF}' \ | cut -d '"' -f 2) #echo "$dev_node pci_dev:$pci_dev" if [[ "$pci_dev" == *"$ROOT_PCI"* ]]; then #echo "$dev_node is under tbt device" dev_tp=$(udevadm info --query=all --name="$dev_node" \ | grep "ID_BUS=" \ | cut -d '=' -f 2) DEV_SERIAL=$(udevadm info --query=all --name="$dev_node" \ | grep "ID_SERIAL=" \ | cut -d '-' -f 1 \ | cut -d '=' -f 2) case $dev_tp in ata) DEV_TYPE="HDD" ;; usb) check_usb_type "$dev_node" ;; *) echo "WARN:$dev_node is one unknow type:$dev_tp" DEV_TYPE="$dev_tp" ;; esac stuff_in_tbt "$dev_node" echo " |-> $dev_node $DEV_TYPE pci-${DEV_PCI}:00 $DEV_SERIAL $TBT_DEV_NAME" >> $STUFF_FILE return 0 else return 1 fi } list_tbt_stuff() { local tbt_devs="" local tbt_dev="" local tbt_stuff="" local tbt_de_name="" cat /dev/null > $TBT_STUFF_LIST tbt_devs=$(cat $TBT_DEV_FILE) for tbt_dev in $tbt_devs; do tbt_stuff="" tbt_de_name="" tbt_de_name=$(cat ${TBT_PATH}/${tbt_dev}/device_name) echo "$tbt_dev:$tbt_de_name" >> $TBT_STUFF_LIST tbt_stuff=$(cat $STUFF_FILE \ | grep "${tbt_dev}$" \ | awk -F " $tbt_dev" '{print $1}') [[ -z "$tbt_stuff" ]] || \ echo "$tbt_stuff" >> $TBT_STUFF_LIST done echo "Show detailed stuff under each tbt device:" cat $TBT_STUFF_LIST } find_tbt_dev_stuff() { local dev_nodes="" local dev_node="" cat /dev/null > $STUFF_FILE dev_nodes=$(ls -1 /dev/sd? 2>/dev/null) [[ -z "$dev_nodes" ]] && { echo "No /dev/sd? node find:$dev_nodes" exit 0 } for dev_node in $dev_nodes; do dev_under_tbt "$dev_node" [[ "$?" -eq 0 ]] || continue done list_tbt_stuff } check_tbt_us_pci() { local tbt_dev_num="" local tbt_us_num="" tbt_dev_num=$(cat $TBT_DEV_FILE | wc -l) tbt_us_num=$(cat $PCI_DEC_FILE | wc -l) [[ "$tbt_dev_num" -eq "$tbt_us_num" ]] || { echo "$TBT_DEV_FILE num:$tbt_dev_num not equal $PCI_DEC_FILE num:$tbt_us_num" echo "WARN: tbt stuffs maybe not correct due to above reason!!!" if [[ "$tbt_dev_num" -gt "$tbt_us_num" ]]; then TBT_NUM=$tbt_us_num else TBT_NUM=$tbt_dev_num fi echo "TBT_NUM:$TBT_NUM" return 1 } TBT_NUM=$tbt_dev_num } check_tb_tools() { local check_tbauth="" local check_tblist="" check_tbauth=$(which "$TBAUTH") [[ -z "$check_tbauth" ]] && { test_print_trc "[ERROR] No $TBAUTH tool in $CARGO_PATH, please cargo build; cargo install --path ." } check_tblist=$(which "$TBLIST") [[ -z "$check_tblist" ]] && { test_print_trc "[ERROR] No $TBLIST in $CARGO_PATH, please cargo build; cargo install --path ." } } show_authorized_tbt_details() { check_tb_tools rm -rf /root/test_tbt_1.log if [[ -n "$VERBOSE" ]]; then pci_result=$(lspci -t) test_print_trc "lspci -t: $pci_result" check_device_file check_domain_file topo_tbt_show fi authorize_show_tbt_devices echo # due to falcon ridge find pci a little slow sleep 1 find_root_pci cat /dev/null > $PCI_HEX_FILE cat /dev/null > $PCI_DEC_FILE # will not impact, it will scan all PCI tbt_us_pci "0" check_tbt_us_pci find_tbt_dev_stuff } while getopts ':vsh' arg; do case $arg in h) usage ;; v) VERBOSE="1" ;; s) only_show_topo exit 0 ;; *) usage ;; esac done show_authorized_tbt_details tbtools-0.8.0+git20260510.75be11e/src/000077500000000000000000000000001517364422400164535ustar00rootroot00000000000000tbtools-0.8.0+git20260510.75be11e/src/bin/000077500000000000000000000000001517364422400172235ustar00rootroot00000000000000tbtools-0.8.0+git20260510.75be11e/src/bin/tbadapters.rs000066400000000000000000000160471517364422400217320ustar00rootroot00000000000000// Dump Thunderbolt/USB4 router adapter states // // Copyright (C) 2023, Intel Corporation // Author: Mika Westerberg use ansi_term::{ Colour::{Green, Red, White, Yellow}, Style, }; use clap::Parser; use csv::Writer; use nix::unistd::Uid; use std::io::{self, ErrorKind, IsTerminal, Write}; use std::process; use tbtools::{ self, Address, Device, debugfs::{self, Adapter, BitFields, State, Type}, util, }; #[derive(Parser, Debug)] #[command(version)] #[command(about = "Dump Thunderbolt/USB4 router adapter states", long_about = None)] struct Args { /// Domain number #[arg(short, long, default_value_t = 0)] domain: u8, /// Route string of the device #[arg(value_parser = util::parse_route, short, long)] route: u64, /// Select only specific adapters #[arg(short, long, value_parser = clap::value_parser!(u8).range(1..64))] adapter: Option>, /// Output suitable for scripting #[arg(short = 'S', long)] script: bool, } fn dump_adapter_num(adapter_num: u8, mut record: Option<&mut Vec>) { if let Some(ref mut record) = record { record.push(adapter_num.to_string()); } else if io::stdout().is_terminal() { print!("{}: ", White.bold().paint(format!("{adapter_num:>2}"))); } else { print!("{adapter_num:>2}: "); } } fn dump_adapter_type(adapter: &Adapter, mut record: Option<&mut Vec>) { let mut kind: String = if adapter.is_lane0() { String::from("Lane 0") } else if adapter.is_lane1() { String::from("Lane 1") } else { adapter.kind().to_string() }; if adapter.is_upstream() { kind.push_str(" (upstream)"); } if let Some(ref mut record) = record { record.push(kind); } else { print!("{kind:<30}"); } } fn protocol_state(adapter: &Adapter) -> (&str, Style) { match adapter.kind() { Type::PcieDown | Type::PcieUp => { if let Some(reg) = adapter.register_by_name("ADP_PCIE_CS_0") && let Some(field) = reg.field_by_name("LTSSM") { let v = reg.field_value(field); match field.value_name(v) { Some("L0 state") => return ("L0", Green.normal()), Some("L1 state") => return ("L1", Green.bold()), Some("L2 state") => return ("L2", Green.bold()), Some("Disabled state") => return ("Disabled", Red.normal()), Some("Hot Reset state") => return ("Hot Reset", Red.normal()), Some(state) => return (state.trim_end_matches(" state"), Yellow.normal()), None => (), } } } Type::Usb3Down | Type::Usb3Up => { if let Some(reg) = adapter.register_by_name("ADP_USB3_GX_CS_4") && let Some(field) = reg.field_by_name("PLS") { let v = reg.field_value(field); match field.value_name(v) { Some("U0 state") => return ("U0", Green.normal()), Some("U2 state") => return ("U2", Green.bold()), Some("U3 state") => return ("U3", Green.bold()), Some("Disabled state") => return ("Disabled", Red.normal()), Some("Hot Reset state") => return ("Hot Reset", Red.normal()), Some(state) => return (state.trim_end_matches(" state"), Yellow.normal()), None => (), } } } _ => (), } ("Enabled", Green.normal()) } fn dump_adapter_state(adapter: &Adapter, mut record: Option<&mut Vec>) { let (name, style) = match adapter.state() { State::Disabled => ("Disabled", Red.normal()), State::Enabled => protocol_state(adapter), State::Training => ("Training/Bonding", Yellow.normal()), State::Cl0 => ("CL0", Green.normal()), State::Cl0sTx => ("CL0s Tx", Green.bold()), State::Cl0sRx => ("CL0s Rx", Green.bold()), State::Cl1 => ("CL1", Green.bold()), State::Cl2 => ("CL2", Green.bold()), State::Cld => ("CLd", Red.normal()), _ => ("Unknown", White.dimmed()), }; if let Some(ref mut record) = record { record.push(name.to_string()); } else if io::stdout().is_terminal() { print!("{}", style.paint(format!("{name:<10}"))); } else { print!("{name:<10}"); } } fn dump_other(mut record: Option<&mut Vec>) { if let Some(ref mut record) = record { record.push(String::from("Not implemented")); record.push(String::new()); } else { print!("Not implemented"); } } fn dump_adapter(adapter: &Adapter, mut writer: Option<&mut Writer>) -> io::Result<()> { let mut record: Option> = if writer.is_some() { Some(Vec::new()) } else { None }; dump_adapter_num(adapter.adapter(), record.as_mut()); if adapter.is_lane() || adapter.is_protocol() { dump_adapter_type(adapter, record.as_mut()); dump_adapter_state(adapter, record.as_mut()); } else { dump_other(record.as_mut()); } if let Some(ref mut writer) = writer { writer.write_record(record.unwrap())?; } else { println!(); } Ok(()) } fn dump_adapters(device: &mut Device, args: &Args) -> io::Result<()> { let mut writer = if args.script { let mut writer = Writer::from_writer(io::stdout()); writer.write_record(["adapter", "type", "state"])?; Some(writer) } else { None }; device.read_adapters()?; if let Some(adapter_numbers) = &args.adapter { for adapter_num in adapter_numbers { if let Some(adapter) = device.adapter(*adapter_num) { dump_adapter(adapter, writer.as_mut())?; } else { eprintln!("Warning: non-existing adapter: {}!", *adapter_num); } } } else if let Some(adapters) = device.adapters() { for adapter in adapters { dump_adapter(adapter, writer.as_mut())?; } } Ok(()) } fn dump(args: &Args) -> io::Result<()> { let address = Address::Router { domain: args.domain, route: args.route, }; let mut device = match tbtools::find_device(&address)? { Some(device) => device, None => { eprintln!("Error: no such device found!"); process::exit(1); } }; dump_adapters(&mut device, args)?; Ok(()) } fn main() { let args = Args::parse(); if !Uid::current().is_root() { eprintln!("Error: debugfs access requires root permissions"); process::exit(1); } if let Err(err) = debugfs::mount() { eprintln!("Error: failed to mount debugfs: {err}"); process::exit(1); } if let Err(err) = dump(&args) { eprintln!("Error: {err}"); if err.kind() == ErrorKind::Unsupported { eprintln!("Error: device does not support register access"); } process::exit(1); } } tbtools-0.8.0+git20260510.75be11e/src/bin/tbauth.rs000066400000000000000000000072111517364422400210610ustar00rootroot00000000000000// Authorize Thunderbolt/USB4 devices // // Copyright (C) 2023, Intel Corporation // Author: Mika Westerberg use std::fmt::{self, Write}; use std::fs::{File, OpenOptions}; use std::io::{self, Read, Write as IoWrite}; use std::process; use clap::Parser; use rand::prelude::*; use tbtools::{Address, Device, Kind, SecurityLevel, util}; #[derive(Parser, Debug)] #[command(version)] #[command(about = "Authorize Thunderbolt/USB4 devices", long_about = None)] struct Args { /// Domain number #[arg(short, long, default_value_t = 0)] domain: u8, /// Route string of the device to authorize #[arg(value_parser = util::parse_route, short, long)] route: u64, /// De-authorize instead of authorize #[arg(short = 'D', long)] deauthorize: bool, /// Authorize with key (generated does not exist) #[arg(short = 'A', long, group = "key")] add_key_path: Option, /// Challenge with key #[arg(short = 'C', long, group = "key")] challenge_key_path: Option, } fn is_deauthorization_supported(device: &Device) -> bool { device.domain().unwrap().deauthorization().unwrap_or(false) } fn is_secure_supported(device: &Device) -> bool { if device.domain().unwrap().security_level().unwrap() != SecurityLevel::Secure { return false; } device.has_key() } fn gen_key() -> Result { let mut data = [0u8; 32]; rand::rng().fill_bytes(&mut data); let mut key = String::new(); for b in data { write!(key, "{b:02x}")?; } Ok(key) } fn main() -> io::Result<()> { let args = Args::parse(); let address = Address::Router { domain: args.domain, route: args.route, }; let mut device = match tbtools::find_device(&address)? { Some(device) => device, None => { eprintln!("Error: No such device found!"); process::exit(1); } }; if device.kind() != Kind::Router { eprintln!("Error: Only routers can be authorized!"); process::exit(1); } if args.deauthorize { if !is_deauthorization_supported(&device) { eprintln!("Error: Domain does not support de-authorization!"); process::exit(1); } return tbtools::authorize_device(&mut device, 0); } let authorized: u32; if let Some(path) = &args.add_key_path { if !is_secure_supported(&device) { eprintln!("Error: Domain security level is not 'secure'"); process::exit(1); } let key = match gen_key() { Err(err) => { eprintln!("Error: Key generation failed {err}"); process::exit(1); } Ok(key) => key, }; device.set_key(&key)?; let file = OpenOptions::new() .create(true) .write(true) .truncate(true) .open(path); write!(file?, "{key}")?; authorized = 1; } else if let Some(path) = &args.challenge_key_path { if !is_secure_supported(&device) { eprintln!("Error: Domain security level is not 'secure'"); process::exit(1); } let mut file = File::open(path)?; let mut key = String::new(); file.read_to_string(&mut key)?; device.set_key(&key)?; authorized = 2; } else { authorized = 1; } if let Err(err) = tbtools::authorize_device(&mut device, authorized) { eprintln!( "Error: Device {} authorization failed {}", device.kernel_name(), err ); process::exit(1); } Ok(()) } tbtools-0.8.0+git20260510.75be11e/src/bin/tbdump.rs000066400000000000000000000460231517364422400210710ustar00rootroot00000000000000// Dump Thunderbolt/USB4 config spaces // // Copyright (C) 2023, Intel Corporation // Author: Mika Westerberg use ansi_term::Colour::{Cyan, Green, Red, White, Yellow}; use clap::Parser; use nix::unistd::Uid; use std::{ io::{self, ErrorKind, IsTerminal}, process, }; use tbtools::{ self, Address, Device, Version, debugfs::{self, BitFields, Name, Register}, drom::{Drom, DromEntry, RankType, SingleDataPathPreference, TmuMode, TmuRate}, usb4, util, }; #[derive(Parser, Debug)] #[command(about = "Dump Thunderbolt/USB4 config spaces", long_about = None)] struct Args { /// Domain number #[arg(short, long, default_value_t = 0)] domain: u8, /// Route string of the device #[arg(value_parser = util::parse_route, short, long)] route: u64, /// Adapter number if accessing adapters #[arg(short, long, value_parser = clap::value_parser!(u8).range(1..64))] adapter: Option, /// Select path config space of an adapter #[arg(short, long)] path: bool, /// Select counters config space of an adapter #[arg(short, long)] counters: bool, /// Verbose output (use multiple times to get more detailed output) #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, /// Dump starting from specific capability only (OFFSET and NREGS are relative) #[arg(short = 'C', long, group = "cap")] cap_id: Option, /// Dump starting from specific VSEC capability only (OFFSET and NREGS are relative) #[arg(short = 'V', long, group = "cap")] vs_cap_id: Option, /// Dump router DROM instead of registers #[arg(short = 'R', long)] drom: bool, /// Number of double words to read #[arg(short = 'N', long)] nregs: Option, /// Double word offset or name of a register offset: Option, } fn offset(regs: &[Register], args: &Args) -> u16 { if let Some(offset) = &args.offset { match util::parse_number(offset) { Some(offset) => offset, None => { // It is name, so lookup for a register with matching name and return its offset. // Otherwise bail out, no such register found. regs.iter() .find(|r| { r.name() .is_some_and(|n| n.to_lowercase() == offset.to_lowercase()) }) .map(|r| r.offset()) .unwrap_or_else(|| { eprintln!("Error: Valid register name expected!"); process::exit(1); }) } } } else { 0 } } fn dump_value(reg: &Register, args: &Args) { let value = reg.value(); print!("0x{value:08x}"); if args.verbose > 1 { print!(" 0b{:08b}", (value >> 24) & 0xff); print!(" {:08b}", (value >> 16) & 0xff); print!(" {:08b}", (value >> 8) & 0xff); print!(" {:08b} ", value & 0xff); print!("{}", util::bytes_to_ascii(&value.to_be_bytes())); } } fn color_field_value(value: &str) -> String { if io::stdout().is_terminal() { Cyan.paint(format!("{value:>10}")).to_string() } else { format!("{value:>10}") } } fn color_field_value_name(name: &str) -> String { if io::stdout().is_terminal() { Cyan.bold().paint(name).to_string() } else { name.to_string() } } fn color_field_short_name(short_name: &str) -> String { if io::stdout().is_terminal() { Yellow.bold().paint(short_name).to_string() } else { String::from(short_name) } } fn color_goodbad(yesno: bool) -> String { if io::stdout().is_terminal() { if yesno { Green.bold().paint("✔").to_string() } else { Red.bold().paint("✘").to_string() } } else if yesno { String::from("✔") } else { String::from("✘") } } fn color_bool(val: bool) -> String { (match val { true => { if io::stdout().is_terminal() { Green.paint("Yes").to_string() } else { "Yes".to_string() } } false => String::from("No"), }) .to_string() } fn color_value(value: &str) -> String { if io::stdout().is_terminal() { Cyan.paint(value.to_string()).to_string() } else { value.to_string() } } fn color_adapter_num(adapter_num: u8) -> String { if io::stdout().is_terminal() { White.bold().paint(format!("{adapter_num}")).to_string() } else { format!("{adapter_num}").to_string() } } fn dump_regs(regs: &Vec, args: &Args) { let offset = offset(regs, args); let mut i = 0; for reg in regs { if let Some(cap_id) = args.cap_id { if reg.cap_id() != cap_id { continue; } // Offset and nregs are now relative to the cap_id. if reg.relative_offset() < offset { continue; } } else if let Some(vs_cap_id) = args.vs_cap_id { if reg.cap_id() != usb4::CAP_ID_VSEC || vs_cap_id != reg.vs_cap_id() { continue; } // Offset and nregs are now relative to the vs_cap_id. if reg.relative_offset() < offset { continue; } } else if reg.offset() < offset { continue; } if let Some(nregs) = args.nregs { if i >= nregs { return; } i += 1; } if args.verbose > 0 { print!("0x{:04x} ", reg.offset()); } dump_value(reg, args); if args.verbose > 0 && let Some(name) = reg.name() { print!(" {name:<15}"); } println!(); if args.verbose > 1 && let Some(fields) = reg.fields() { for field in fields { let v = reg.field(field.name()); let value = color_field_value(&format!("{v:#x}")); let value_name = if let Some(value_name) = field.value_name(v) { format!(" → {}", color_field_value_name(value_name)) } else { String::from("") }; let short_name = if let Some(short_name) = field.short_name() { format!(" ({})", color_field_short_name(short_name)) } else { String::from("") }; println!( " [{:>02}:{:>02}] {} {}{}{}", field.range().start(), field.range().end(), value, field.name(), short_name, value_name, ); } } } } fn dump_bytes(bytes: &[u8], offset: usize, args: &Args) { let mut offset = offset; for chunks in bytes.chunks(8) { if args.verbose > 0 { print!("0x{offset:04x} "); } for byte in chunks { print!("0x{byte:02x} "); } if args.verbose > 0 { let indent = 8 * (4 + 1) - chunks.len() * 4; print!("{:>1$}", util::bytes_to_ascii(chunks), indent); } println!(); offset += chunks.len(); } } fn dump_drom(drom: &Drom, args: &Args) { if args.verbose < 2 { dump_bytes(drom.bytes(), 0, args); return; } dump_bytes(drom.header(), 0, args); if drom.is_tb3_compatible() { println!( " CRC8: {} {}", color_value(&format!("{:#x}", drom.crc8().unwrap())), color_goodbad(drom.is_crc8_valid()), ); println!( " UUID: {}", color_value(&format!("{:#x}", drom.uuid().unwrap())) ); } println!( " CRC32: {} {}", color_value(&format!("{:#x}", drom.crc32())), color_goodbad(drom.is_crc32_valid()), ); println!( " Version: {} ({})", color_value(&format!("{}", drom.version())), if drom.is_tb3_compatible() { "Thunderbolt 3 Compatible" } else { "USB4" } ); let mut entries = drom.entries(); while let Some(entry) = entries.next() { dump_bytes(entries.bytes(), entries.start(), args); match entry { // Adapter entries. DromEntry::Adapter { disabled, adapter_num, } => { println!(" Adapter {}", color_adapter_num(adapter_num)); if disabled { println!(" Disabled"); } } DromEntry::UnusedAdapter { adapter_num } => { println!(" Unused Adapter {}", color_adapter_num(adapter_num)) } DromEntry::DisplayPortAdapter { adapter_num, preferred_lane_adapter_num, preference_valid, } => { println!(" DisplayPort Adapter {}", color_adapter_num(adapter_num)); if preference_valid { println!( " Preferred Lane adapter: {}", color_adapter_num(preferred_lane_adapter_num), ); } } DromEntry::LaneAdapter { adapter_num, lane_1_adapter, dual_lane_adapter, dual_lane_adapter_num, } => { println!( " Lane {} Adapter {}", color_adapter_num(adapter_num), if lane_1_adapter { 1 } else { 0 } ); if dual_lane_adapter { println!( " Dual Lane Adapter: {}", color_adapter_num(dual_lane_adapter_num) ); } } DromEntry::PcieUpAdapter { adapter_num, function_num, device_num, } => { println!(" PCIe Upstream Adapter {}", color_adapter_num(adapter_num)); println!( " {}", color_value(&format!("{device_num:02x}.{function_num:x}")) ); } DromEntry::PcieDownAdapter { adapter_num, function_num, device_num, } => { println!( " PCIe Downstream Adapter {}", color_adapter_num(adapter_num) ); println!( " {}", color_value(&format!("{device_num:02x}.{function_num:x}")) ); } // Generic entries. DromEntry::Generic { kind, .. } => { println!(" Generic Entry: {}", color_value(&format!("{kind:#x}"))); } DromEntry::AsciiVendorName(vendor) => println!(" Vendor: {vendor}"), DromEntry::AsciiModelName(model) => println!(" Model: {model}"), DromEntry::Utf16VendorName(vendor) => println!(" Vendor: {vendor}"), DromEntry::Utf16ModelName(model) => println!(" Model: {model}"), DromEntry::Tmu { mode, rate } => { println!( " TMU: {}", match mode { TmuMode::Unknown => "Unknown", TmuMode::Off => "Off", TmuMode::Unidirectional => { if rate == TmuRate::HiFi { "Unidirectional, HiFi" } else if rate == TmuRate::LowRes { "Unidirectional, LowRes" } else { "Unidirectional" } } TmuMode::Bidirectional => { if rate == TmuRate::HiFi { "Bidirectional, HiFi" } else { "Bidirectional" } } } ); } DromEntry::ProductDescriptor { usb4_version: Version { major: usb4_major, minor: usb4_minor, }, vendor, product, fw_version: Version { major: fw_major, minor: fw_minor, }, test_id, hw_revision, } => { println!(" Product Descriptor:"); println!( " bcdUSBSpec: {}", color_value(&format!("{usb4_major:x}.{usb4_minor:x}")) ); println!(" idVendor: {}", color_value(&format!("{vendor:04x}"))); println!(" idProduct: {}", color_value(&format!("{product:04x}"))); println!( " bcdProductFWRevision: {}", color_value(&format!("{fw_major:x}.{fw_minor:x}")) ); println!(" TID: {}", color_value(&format!("{test_id:04x}"))); println!( " productHWRevision: {}", color_value(&format!("{hw_revision}")) ); } DromEntry::SerialNumber { lang_id, serial_number, } => { println!(" Serial Number:"); println!(" wLANGID: {}", color_value(&format!("{lang_id}"))); println!( " SerialNumber: {}", color_value(&serial_number.to_string()) ); } DromEntry::Usb3PortMapping(mappings) => { println!(" USB Port Mappings:"); for mapping in mappings { println!(" USB 3 Port {}", mapping.usb3_port_num); println!(" xHCI Index: {}", mapping.xhci_index); if mapping.usb_type_c { println!(" PD Port: {}", mapping.pd_port_num); } if mapping.tunneling { println!( " USB 3 Downstream Adapter: {}", color_adapter_num(mapping.usb3_adapter_num) ); } } } DromEntry::SingleDataPath(pref) => { println!( " Preferred Single Data Path: {}", match pref { SingleDataPathPreference::PcieTunneling => String::from("PCIe Tunneling"), SingleDataPathPreference::Usb3GenTTunneling => String::from("USB 3 GenT Tunneling"), SingleDataPathPreference::Reserved(v) => format!("Reserved {v}"), } ); } DromEntry::DptxRanking { adapter_num, nrr, mh, preferred_order, mu, records, } => { println!(" DPTX Ranking:"); println!(" Adapter: {}", color_adapter_num(adapter_num)); println!(" NRR: {}", color_value(&format!("{nrr}"))); println!(" MST Hub: {}", color_bool(mh)); println!( " Preferred Order: {}", color_value(&format!("{preferred_order}")) ); if mh { println!(" MST Upstream: {}", color_value(&format!("{mu}"))); } if nrr > 0 { println!(" Records:"); for record in records { println!( " Rank Type: {}", match record.rank_type { RankType::Power => String::from("Power"), RankType::Performance => String::from("Performance"), RankType::Reserved(v) => format!("Reserved {v}"), } ); println!(" Rank: {}", color_value(&format!("{}", record.rank))); } } } DromEntry::EmbeddedLink(adapter) => println!(" Embedded USB4 Link: {adapter}"), DromEntry::Unknown(_) => println!("Unknown Entry"), } } } fn dump_router(device: &mut Device, args: &Args) -> io::Result<()> { device.read_registers()?; if args.drom { device.read_drom()?; if let Some(drom) = device.drom() { dump_drom(drom, args); } } else if let Some(regs) = device.registers() { dump_regs(regs, args); } Ok(()) } fn dump_adapter(device: &mut Device, adapter: u8, args: &Args) -> io::Result<()> { device.read_adapters()?; if let Some(adapter) = device.adapter_mut(adapter) { if args.path { adapter.read_paths()?; if let Some(regs) = adapter.path_registers() { dump_regs(regs, args); } } else if args.counters { adapter.read_counters()?; if let Some(regs) = adapter.counter_registers() { dump_regs(regs, args); } } else if let Some(regs) = adapter.registers() { dump_regs(regs, args); } } Ok(()) } fn dump(args: &Args) -> io::Result<()> { let address = Address::Router { domain: args.domain, route: args.route, }; let mut device = match tbtools::find_device(&address)? { Some(device) => device, None => { eprintln!("Error: No such device found!"); process::exit(1); } }; if let Some(adapter) = args.adapter { if args.vs_cap_id.is_some() { eprintln!("Error: Adapters do not have vendor specific capabilities!"); process::exit(1); } else if args.drom { eprintln!("Error: Only routers have DROM!"); process::exit(1); } dump_adapter(&mut device, adapter, args)? } else { dump_router(&mut device, args)? } Ok(()) } fn main() { let args = Args::parse(); if !Uid::current().is_root() { eprintln!("Error: debugfs access requires root permissions"); process::exit(1); } debugfs::mount().unwrap_or_else(|e| { eprintln!("Error: failed to mount debugfs: {e}"); process::exit(1); }); dump(&args).unwrap_or_else(|e| { eprintln!("Error: {e}"); if e.kind() == ErrorKind::Unsupported { eprintln!("Device does not support register access"); } process::exit(1); }); } tbtools-0.8.0+git20260510.75be11e/src/bin/tbget.rs000066400000000000000000000274151517364422400207070ustar00rootroot00000000000000// Read Thunderbolt/USB4 config spaces // // Copyright (C) 2023, Intel Corporation // Author: Mika Westerberg use std::{ io::{self, ErrorKind}, ops::RangeInclusive, process, }; use clap::Parser; use csv::Writer; use nix::unistd::Uid; use tbtools::{ Address, Device, debugfs::{self, BitFields, Name, Register}, util, }; #[derive(Parser, Debug)] #[command(version)] #[command(about = "Read Thunderbolt/USB4 config spaces", long_about = None)] struct Args { /// Domain number #[arg(short, long, default_value_t = 0)] domain: u8, /// Route string of the device #[arg(value_parser = util::parse_route, short, long)] route: u64, /// Adapter number if accessing adapters #[arg(short, long, value_parser = clap::value_parser!(u8).range(1..64))] adapter: Option, /// Select path config space of an adapter #[arg(short, long)] path: bool, /// Select counters config space of an adapter #[arg(short, long)] counters: bool, /// Output in binary instead of hex #[arg(short = 'B', long, group = "output")] binary: bool, /// Output in decimal instead of hex #[arg(short = 'D', long, group = "output")] decimal: bool, /// Query all register names starting with name #[arg(short = 'Q', long, group = "output")] query: bool, /// Output suitable for scripting (only works with --query) #[arg(short = 'S', long)] script: bool, /// Verbose output (only works with --query) #[arg(short, long)] verbose: bool, /// One or more registers to read in format offset or name[.field] regs: Vec, } fn dump_value(value: u32, args: &Args) { if args.binary { println!("{value:#b}"); } else if args.decimal { println!("{value}"); } else { println!("{value:#x}"); } } fn query_register(registers: &[Register], args: &Args) -> io::Result<()> { struct NameInfo { name: String, offset: Option, field: Option, range: Option>, } let mut names = Vec::new(); if args.regs.is_empty() { registers.iter().for_each(|r| { if let Some(name) = r.name() { names.push(NameInfo { name: name.to_string(), offset: Some(r.offset()), field: None, range: None, }); } }); } else { for reg in &args.regs { let query: Vec<_> = reg.split('.').collect(); registers.iter().for_each(|r| { if let Some(name) = r.name() { if query.len() > 1 { if name.to_lowercase().contains(&query[0].to_lowercase()) || query[0].is_empty() { // Special case "." dump all known names. if query[0].is_empty() && query[1].is_empty() { names.push(NameInfo { name: name.to_string(), offset: Some(r.offset()), field: None, range: None, }); } if let Some(fields) = r.fields() { fields.iter().for_each(|f| { if f.name().to_lowercase().contains(&query[1].to_lowercase()) { names.push(NameInfo { name: name.to_string(), offset: Some(r.offset()), field: Some(f.name().to_string()), range: Some(f.range().clone()), }); } }); } } } else if name.to_lowercase().contains(&query[0].to_lowercase()) { names.push(NameInfo { name: name.to_string(), offset: Some(r.offset()), field: None, range: None, }); } } }); } } if args.script { let mut writer = Writer::from_writer(io::stdout()); let mut headers = vec!["domain", "route", "adapter", "index", "name", "field"]; if args.verbose { headers.push("offset"); headers.push("range_start"); headers.push("range_end"); } writer.write_record(headers)?; for name in &names { let mut record = Vec::new(); record.push(format!("{}", args.domain)); record.push(format!("{:x}", args.route)); if let Some(adapter) = &args.adapter { record.push(format!("{adapter}")); } else { record.push(String::new()); } // TODO: Add index with retimer support. record.push(String::new()); record.push(name.name.clone()); if let Some(field) = &name.field { record.push(field.clone()); } else { record.push(String::new()); } if args.verbose { if let Some(offset) = name.offset { record.push(format!("0x{offset:04x}")) } else { record.push(String::new()); } if let Some(range) = &name.range { record.push(format!("{}", range.start())); record.push(format!("{}", range.end())); } else { record.push(String::new()); record.push(String::new()); } } writer.write_record(record)?; } } else { for name in &names { print!("{}", name.name); if let Some(field) = &name.field { print!(".{field}"); } if args.verbose { if let Some(offset) = name.offset { print!(" 0x{offset:04x}"); } if let Some(range) = &name.range { print!(" [{:>02}:{:>02}]", range.start(), range.end()); } } println!(); } } Ok(()) } fn query_router(device: &Device, args: &Args) -> io::Result<()> { if let Some(registers) = device.registers() { query_register(registers, args)?; } Ok(()) } fn read_router(device: &mut Device, args: &Args) -> io::Result<()> { for reg in &args.regs { match util::parse_hex::(reg) { Some(offset) => { if let Some(reg) = device.register_by_offset_mut(offset) { dump_value(reg.value(), args); } else { eprintln!("Warning: invalid offset {offset}!"); } } None => { let name: Vec<_> = reg.split('.').collect(); if let Some(reg) = device.register_by_name_mut(name[0]) { if name.len() > 1 { if reg.has_field(name[1]) { dump_value(reg.field(name[1]), args); } else { eprintln!("Warning: field name {} not found!", &name[1]); } } else { dump_value(reg.value(), args); } } else { eprintln!("Warning: register name {} not found!", &name[0]); } } } } Ok(()) } fn query_adapter(device: &mut Device, adapter: u8, args: &Args) -> io::Result<()> { device.read_adapters()?; if let Some(adapter) = device.adapter_mut(adapter) && let Some(registers) = if args.path { adapter.read_paths()?; adapter.path_registers() } else if args.counters { adapter.read_counters()?; adapter.counter_registers() } else { adapter.registers() } { query_register(registers, args)?; } Ok(()) } fn read_adapter(device: &mut Device, adapter: u8, args: &Args) -> io::Result<()> { device.read_adapters()?; if let Some(adapter) = device.adapter_mut(adapter) { if args.path { adapter.read_paths()?; } else if args.counters { adapter.read_counters()?; } for reg in &args.regs { match util::parse_hex::(reg) { Some(offset) => { let reg = if args.path { adapter.path_register_by_offset_mut(offset) } else if args.counters { adapter.counter_register_by_offset_mut(offset) } else { adapter.register_by_offset_mut(offset) }; if let Some(reg) = reg { dump_value(reg.value(), args); } else { eprintln!("Warning: invalid offset {offset}!"); } } None => { if args.path || args.counters { eprintln!("Warning: path and counters registers do not have names!"); } else { let name: Vec<_> = reg.split('.').collect(); if let Some(reg) = adapter.register_by_name_mut(name[0]) { if name.len() > 1 { if reg.has_field(name[1]) { dump_value(reg.field(name[1]), args); } else { eprintln!("Warning: field name {} not found!", &name[1]); } } else { dump_value(reg.value(), args); } } else { eprintln!("Warning: register name {} not found!", &name[0]); } } } } } } else { eprintln!("Error: adapter {adapter} not found!"); process::exit(1); } Ok(()) } fn read(args: &Args) -> io::Result<()> { let address = Address::Router { domain: args.domain, route: args.route, }; let mut device = match tbtools::find_device(&address)? { Some(device) => device, None => { eprintln!("Error: No such device found!"); process::exit(1); } }; device.read_registers()?; if let Some(adapter) = args.adapter { if args.query { query_adapter(&mut device, adapter, args)?; } else { read_adapter(&mut device, adapter, args)? } } else if args.query { query_router(&device, args)?; } else { read_router(&mut device, args)?; } Ok(()) } fn main() { let args = Args::parse(); if !Uid::current().is_root() { eprintln!("Error: debugfs access requires root permissions!"); process::exit(1); } if let Err(err) = debugfs::mount() { eprintln!("Error: failed to mount debugfs: {err}"); process::exit(1); } if let Err(err) = read(&args) { eprintln!("Error: {err}"); if err.kind() == ErrorKind::Unsupported { eprintln!("Device does not support register access"); } process::exit(1); } } tbtools-0.8.0+git20260510.75be11e/src/bin/tblist.rs000066400000000000000000000333041517364422400210750ustar00rootroot00000000000000// List Thunderbolt/USB4 devices // // Copyright (C) 2023, Intel Corporation // Author: Mika Westerberg use ansi_term::Colour::{Cyan, Green, Yellow}; use ansi_term::Style; use clap::Parser; use csv::Writer; use std::io::{self, IsTerminal}; use tbtools::{self, Device, Kind, SecurityLevel}; #[derive(Parser, Debug)] #[command(version)] #[command(about = "List Thunderbolt/USB4 devices", long_about = None)] struct Args { /// List all devices, not just routers #[arg(short = 'A', long)] all: bool, /// Output suitable for scripting #[arg(short = 'S', long, group = "output")] script: bool, /// List devices in tree format #[arg(short, long, group = "output")] tree: bool, /// Verbose output #[arg(short, long)] verbose: bool, } fn indent(args: &Args, device: &Device) -> String { if args.tree { // Add extra indent so that services appear under the XDomain device. let extra = device.is_service() as u32; if args.all { return " ".repeat(((device.depth() + 1 + extra) * 4) as usize); } return " ".repeat(((device.depth() + extra) * 4) as usize); } String::from("") } fn color_root() -> String { if io::stdout().is_terminal() { return Yellow.paint("/").to_string(); } String::from("/") } fn kind(device: &Device) -> String { match device.kind() { Kind::Domain => String::from("Domain"), Kind::Router => String::from("Router"), Kind::Retimer => String::from("Retimer"), Kind::Xdomain => String::from("XDomain"), Kind::Service => String::from("Service"), _ => String::from("Unknown"), } } fn color_kind(device: &Device) -> String { let name = kind(device); if io::stdout().is_terminal() { return Cyan.paint(name).to_string(); } name } fn color_name(device: &Device) -> String { let bold: Option
Thunderbolt Logo

Thunderbolt/USB4 Trace

tbtools {VERSION}
"#; const HTML_FOOTER: &str = r##"
Timestamp Function PDF / CS Domain Route Adapter Address Size
"##; #[derive(Parser, Debug)] #[command(version)] #[command(about = "Trace Thunderbolt/USB4 transport layer configuration traffic", long_about = None)] struct Args { #[command(subcommand)] command: Commands, } #[derive(Subcommand, Debug)] enum Commands { /// Shows tracing status (enabled/disabled) Status, /// Enables tracing Enable { /// Filter by domain number #[arg(short, long)] domain: Option, }, /// Disables tracing Disable, /// Dumps the current tracing buffer Dump { /// Trace input file if not reading through tracefs #[arg(short, long)] input: Option, /// Output suitable for scripting #[arg(short = 'S', long, group = "output")] script: bool, /// HTML output #[arg(short = 'H', long, group = "output")] html: bool, /// Timestamp as system wall clock time instead of seconds from boot #[arg(short = 'T', long)] time: bool, /// Verbose output #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, }, /// Clears the tracing buffer Clear, } fn color_function(function: &str) -> String { if io::stdout().is_terminal() { White.bold().paint(function).to_string() } else { function.to_string() } } fn color_dropped(dropped: bool) -> String { if !dropped { return String::from(""); } if io::stdout().is_terminal() { Red.paint("!").to_string() } else { "!".to_string() } } fn color_pdf(pdf: &Pdf) -> String { if io::stdout().is_terminal() { Green.paint(pdf.to_string()).to_string() } else { pdf.to_string() } } fn color_field_value(value: &str) -> String { if io::stdout().is_terminal() { Cyan.paint(format!("{value:>10}")).to_string() } else { format!("{value:>10}") } } fn color_field_value_name(name: &str) -> String { if io::stdout().is_terminal() { Cyan.bold().paint(name).to_string() } else { name.to_string() } } fn color_field_short_name(short_name: &str) -> String { if io::stdout().is_terminal() { Yellow.bold().paint(short_name).to_string() } else { short_name.to_string() } } fn color_address(address: u16) -> String { if io::stdout().is_terminal() { Purple.bold().paint(format!("{address:04x}")).to_string() } else { format!("{address:04x}") } } fn color_tracing(enabled: bool) -> String { let s = if enabled { "Enabled" } else { "Disabled" }; if io::stdout().is_terminal() { let c = if enabled { Green } else { White }; c.paint(s).to_string() } else { s.to_string() } } fn timestamp(ts: &TimeVal, boot_time: Option) -> TimeVal { if let Some(boot_time) = boot_time { boot_time + *ts } else { *ts } } fn dump_header( entry: &trace::Entry, packet: &trace::ControlPacket, record: Option<&mut Vec>, device: Option<&Device>, boot_time: Option, ) { let ts = timestamp(entry.timestamp(), boot_time); if let Some(record) = record { record.push(format!( "{}.{:06}", entry.timestamp().tv_sec(), entry.timestamp().tv_usec() )); if ts != *entry.timestamp() { record.push(format!("{}.{:06}", ts.tv_sec(), ts.tv_usec())); } else { record.push(String::new()); } record.push(entry.function().to_string()); record.push(entry.dropped().to_string()); record.push(entry.pdf().to_string()); if let Some(cs) = entry.cs() { record.push(cs.to_string()); } else { record.push(String::new()); } record.push(format!("{}", entry.domain_index())); record.push(format!("{:x}", entry.route())); if let Some(adapter_num) = packet.adapter_num() { record.push(adapter_num.to_string()); if let Some(device) = device { if let Some(adapter) = device.adapter(adapter_num) { record.push(adapter.kind().to_string()); } else { record.push(String::new()); } } else { record.push(String::new()); } } else { record.push(String::new()); record.push(String::new()); } } else { print!("[{:5}.{:06}] ", ts.tv_sec(), ts.tv_usec()); print!("{} ", color_function(entry.function())); print!("{}", color_dropped(entry.dropped())); print!("{} ", color_pdf(&entry.pdf())); print!("Domain {} ", entry.domain_index()); print!("Route {:x} ", entry.route()); if let Some(adapter_num) = packet.adapter_num() { print!("Adapter {adapter_num} "); if let Some(device) = device && let Some(adapter) = device.adapter(adapter_num) { print!("/ {}", adapter.kind()); } } println!(); } } fn dump_name(verbose: u8, record: Option<&mut Vec>, name: &dyn Name) { if verbose < 1 { println!(); return; } if let Some(name) = name.name() { if let Some(record) = record { record.push(name.to_string()); } else { println!("{name}"); } } else if let Some(record) = record { record.push(String::new()); } else { println!(); } } fn dump_fields(verbose: u8, bitfields: &dyn BitFields) { if verbose < 2 { return; } if let Some(fields) = bitfields.fields() { for field in fields { let v = bitfields.field(field.name()); let value = color_field_value(&format!("{v:#x}")); let value_name = if let Some(value_name) = field.value_name(v) { format!(" → {}", color_field_value_name(value_name)) } else { String::from("") }; let short_name = if let Some(short_name) = field.short_name() { format!(" ({})", color_field_short_name(short_name)) } else { String::from("") }; println!( "{:15} [{:>02}:{:>02}] {} {}{}{}", "", field.range().start(), field.range().end(), value, field.name(), short_name, value_name, ); } } } fn extract_register_info( entry: &trace::Entry, device: Option<&Device>, data_address: u16, data: u32, ) -> Option + Name> { if let Some(device) = device { // Use the register metadata to print the details if it is available. if let Some(register) = match entry.cs() { Some(ConfigSpace::Adapter) => { if let Some(adapter_num) = entry.adapter_num() { if let Some(adapter) = device.adapter(adapter_num) { adapter.register_by_offset(data_address) } else { None } } else { None } } Some(ConfigSpace::Path) => { if let Some(adapter_num) = entry.adapter_num() { if let Some(adapter) = device.adapter(adapter_num) { adapter.path_register_by_offset(data_address) } else { None } } else { None } } Some(ConfigSpace::Router) => device.register_by_offset(data_address), _ => None, } { // Clone it so that we can fill in the value and use the field metadata too // without changing the actual contents. let mut register = register.clone(); register.set_value(data); return Some(register); } } None } fn dump_packet( entry: &trace::Entry, packet: &trace::ControlPacket, verbose: u8, device: Option<&Device>, ) { let mut data_address = packet.data_address().unwrap_or(0); let data_start = packet.data_start().unwrap_or(0); for (i, f) in packet .fields() .iter() .enumerate() .map(|(i, f)| (i as u16, f)) { print!("{:15}", ""); print!("0x{i:02x}"); if verbose > 1 { if packet.data().is_some() && i >= data_start { print!("/{}", color_address(data_address)); } else { print!("/----"); } } let d = f.value(); print!(" 0x{d:08x} "); print!("0b{:08b}", (d >> 24) & 0xff); print!(" {:08b}", (d >> 16) & 0xff); print!(" {:08b}", (d >> 8) & 0xff); print!(" {:08b} ", d & 0xff); print!("{} ", util::bytes_to_ascii(&d.to_be_bytes())); if verbose > 0 { if packet.data().is_some() && i >= data_start { data_address += 1; if let Some(register) = extract_register_info(entry, device, data_address - 1, d) { dump_name(verbose, None, ®ister); dump_fields(verbose, ®ister); continue; } } dump_name(verbose, None, f); dump_fields(verbose, f); } else { println!(); } } } fn dump_script_packet( entry: &trace::Entry, packet: &trace::ControlPacket, header: &[String], writer: &mut Writer, verbose: u8, device: Option<&Device>, ) -> io::Result<()> { let mut data_address = packet.data_address().unwrap_or(0); let data_start = packet.data_start().unwrap_or(0); for (i, f) in packet .fields() .iter() .enumerate() .map(|(i, f)| (i as u16, f)) { let mut record = header.to_owned(); record.push(format!("0x{i:02x}")); if verbose > 1 { if packet.data().is_some() && i >= data_start { record.push(format!("0x{data_address:04x}")); } else { record.push(String::new()); } } else { record.push(String::new()); } record.push(format!("0x{:08x}", f.value())); if verbose > 0 { if packet.data().is_some() && i >= data_start { data_address += 1; if let Some(register) = extract_register_info(entry, device, data_address - 1, f.value()) { dump_name(verbose, Some(&mut record), ®ister); } else { dump_name(verbose, Some(&mut record), f); } } else { dump_name(verbose, Some(&mut record), f); } } else { record.push(String::new()); } writer.write_record(record)?; } Ok(()) } fn dump_html_bitfields(bitfields: &dyn BitFields, verbose: u8) -> Vec { let mut bf = Vec::new(); if verbose > 1 && let Some(fields) = bitfields.fields() { for field in fields { let value = bitfields.field(field.name()); bf.push(json!({ "start": field.range().start(), "end": field.range().end(), "value": value, "name": field.name(), "short_name": field.short_name(), "value_name": field.value_name(value), })); } } bf } fn dump_html( entry: &trace::Entry, packet: &trace::ControlPacket, verbose: u8, device: Option<&Device>, boot_time: Option, ) { let ts = timestamp(entry.timestamp(), boot_time); let mut data_address = packet.data_address().unwrap_or(0); let data_start = packet.data_start().unwrap_or(0); let mut fields = Vec::new(); for (i, f) in packet .fields() .iter() .enumerate() .map(|(i, f)| (i as u16, f)) { let value = f.value(); if packet.data().is_some() && i >= data_start { data_address += 1; if verbose > 0 && let Some(register) = extract_register_info(entry, device, data_address - 1, value) { fields.push(json!({ "address": i, "name": register.name(), "value": value, "bitfields": dump_html_bitfields(®ister, verbose), })); continue; } } fields.push(json!({ "address": i, "name": if verbose > 0 { f.name() } else { None }, "value": value, "bitfields": dump_html_bitfields(f, verbose), })); } let mut details = json!(entry); if let Some(adapter_num) = packet.adapter_num() { if let Some(device) = device { if let Some(adapter) = device.adapter(adapter_num) { details["adapter"] = format!("{} / {}", adapter_num, adapter.kind()).into(); } } else { details["adapter"] = adapter_num.to_string().into(); } } let mut indent = 5; if entry.dropped() { println!( r#"{}"#, " ".repeat(indent), entry.domain_index(), entry.route(), details, json!(fields) ); } else { println!( r#"{}"#, " ".repeat(indent), entry.domain_index(), entry.route(), details, json!(fields) ); } indent += 1; println!( "{}{}.{:06}", " ".repeat(indent), ts.tv_sec(), ts.tv_usec() ); println!( "{}{}", " ".repeat(indent), entry.function() ); if let Some(cs) = entry.cs() { println!("{}{} / {}", " ".repeat(indent), entry.pdf(), cs); } else { println!("{}{}", " ".repeat(indent), entry.pdf()); } println!("{}{}", " ".repeat(indent), entry.domain_index()); println!("{}{:x}", " ".repeat(indent), entry.route()); if let Some(adapter) = details["adapter"].as_str() { println!("{}{}", " ".repeat(indent), adapter); } else { println!("{}", " ".repeat(indent)); } if let Some(data_address) = packet.data_address() { println!("{}{:#x}", " ".repeat(indent), data_address); } else { println!("{}", " ".repeat(indent)); } if let Some(data_size) = packet.data_size() { println!("{}{}", " ".repeat(indent), data_size); } else { println!("{}", " ".repeat(indent)); } indent -= 1; println!("{}", " ".repeat(indent)); } fn dump( input: Option, script: bool, html: bool, time: bool, verbose: u8, ) -> io::Result<()> { let mut devices: Vec = Vec::new(); let trace_buf; if let Some(input) = input { trace_buf = trace::buffer(Path::new(&input)).unwrap_or_else(|e| { eprintln!("Error: failed open trace input file: {e}"); process::exit(1); }); } else { trace_buf = trace::live_buffer()?; // Only add register information if we are running on a live system. if verbose > 0 { let devs = tbtools::find_devices(None)?; let mut devs: Vec<_> = devs .into_iter() .filter(|d| d.kind() == Kind::Router) .collect(); devices.append(&mut devs); } }; let boot_time = if time { Some(util::system_boot_time()?) } else { None }; let mut writer = if script { let mut writer = Writer::from_writer(io::stdout()); // Add header. writer.write_record([ "entry", "timestamp", "datetime", "function", "dropped", "pdf", "cs", "domain", "route", "adapter", "adapter_type", "offset", "data_offset", "value", "name", ])?; Some(writer) } else { None }; let mut domains = HashSet::new(); let mut routes = HashSet::new(); if html { let header = HTML_HEADER.replace("{VERSION}", env!("CARGO_PKG_VERSION")); print!("{header}"); } let mut line = 0; for entry in trace_buf { let mut device = devices .iter_mut() .find(|d| d.domain_index() == entry.domain_index() && d.route() == entry.route()); domains.insert(entry.domain_index()); routes.insert(entry.route()); if let Some(ref mut device) = device { device.read_registers_cached()?; device.read_adapters_cached()?; if let Some(adapters) = device.adapters_mut() { for adapter in adapters { let _ = adapter.read_paths_cached(); } } } if let Some(packet) = entry.packet() { // The kernel records both the event and the immediate receive packet which is the same so // we skip the event to avoid outputting duplicate data. if packet.is_xdomain() && entry.function() == "tb_event" { continue; } if let Some(ref mut writer) = writer { let mut header: Vec = vec![line.to_string()]; // Header part is always the same. dump_header( &entry, &packet, Some(&mut header), device.as_deref(), boot_time, ); dump_script_packet(&entry, &packet, &header, writer, verbose, device.as_deref())?; line += 1; } else if html { dump_html(&entry, &packet, verbose, device.as_deref(), boot_time); } else { dump_header(&entry, &packet, None, device.as_deref(), boot_time); dump_packet(&entry, &packet, verbose, device.as_deref()); } } } if html { let mut domains: Vec<_> = domains.iter().collect(); domains.sort(); let mut routes: Vec<_> = routes.iter().collect(); routes.sort(); let domain_filters = domains .iter() .map(|d| format!(r#"{}"#, " ".repeat(20), d, d)) .collect::>(); let header = HTML_FOOTER.replace("{DOMAINS}", &domain_filters.join("\n")); let route_filters = routes .iter() .map(|d| format!(r#"{}"#, " ".repeat(20), d, d)) .collect::>(); let header = header.replace("{ROUTES}", &route_filters.join("\n")); print!("{header}"); } Ok(()) } fn check_access() { if !Uid::current().is_root() { eprintln!("Error: debugfs access requires root permissions!"); process::exit(1); } if let Err(err) = debugfs::mount() { eprintln!("Error: failed to mount debugfs: {err}"); process::exit(1); } if let Err(err) = trace::mount() { eprintln!("Error: failed to mount tracefs: {err}"); process::exit(1); } if !trace::supported() { eprintln!("Error: no tracing support detected"); process::exit(1); } } fn main() -> io::Result<()> { let args = Args::parse(); match args.command { Commands::Status => { check_access(); if !trace::enabled() { println!("Thunderbolt/USB4 tracing: {}", color_tracing(false)); process::exit(1); } println!("Thunderbolt/USB4 tracing: {}", color_tracing(true)); } Commands::Enable { domain } => { check_access(); if let Some(domain) = domain { trace::add_filter(&Address::Domain { domain })?; } trace::enable()?; println!("Thunderbolt/USB4 tracing: {}", color_tracing(true)); } Commands::Disable => { check_access(); trace::disable()?; println!("Thunderbolt/USB4 tracing: {}", color_tracing(false)); } Commands::Dump { input, script, html, time, verbose, } => { if input.is_none() { check_access(); } if verbose > 0 { if trace::enabled() { eprintln!( "Note you should disable tracing to avoid this tool affecting the results" ); } if input.is_some() { eprintln!("Note register details need live system"); } } if time && input.is_some() { eprintln!( "Note you should run on the same system you took the trace to get accurate times" ); } dump(input, script, html, time, verbose)?; } Commands::Clear => { check_access(); trace::clear()?; println!("Thunderbolt/USB4 tracing: cleared"); } } Ok(()) } tbtools-0.8.0+git20260510.75be11e/src/bin/tbtunnels.rs000066400000000000000000000241771517364422400216220ustar00rootroot00000000000000// Dump tunnels in the Thunderbolt/USB4 domain. // // Copyright (C) 2025, Intel Corporation // Author: Mika Westerberg use ansi_term::{ Colour::{Cyan, Yellow}, Style, }; use clap::Parser; use nix::unistd::Uid; use std::{ io::{self, ErrorKind, IsTerminal}, process, }; use tbtools::{ Device, debugfs::{self, BitFields}, tunnel::{Direction, Hop, Path, Tunnel, Type}, }; #[derive(Parser, Debug)] #[command(version)] #[command(about = "Dump tunnels in the Thunderbolt/USB4 domain", long_about = None)] struct Args { /// Domain number #[arg(short, long, default_value_t = 0)] domain: u8, /// Verbose output (use multiple times to get more detailed output) #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, } fn discover_tunnels<'a>(device: &'a Device, devices: &'a [Device]) -> io::Result>> { let mut tunnels = Vec::new(); if let Some(adapters) = device.adapters() { for adapter in adapters { if adapter.is_enabled() && let Some(mut tuns) = Tunnel::discover(device, adapter, devices) { tunnels.append(&mut tuns); } } } Ok(tunnels) } fn color_type_name(type_name: &str) -> String { if io::stdout().is_terminal() { Yellow.bold().paint(type_name).to_string() } else { String::from(type_name) } } fn color_number(num: u32) -> String { let s = format!("{num}"); if io::stdout().is_terminal() { Cyan.paint(s).to_string() } else { s } } fn dump_hop(index: usize, hop: &Hop, args: &Args) { if args.verbose <= 1 { return; } let bold: Option