├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── docker.yml │ ├── homebrew.yml │ ├── test.yml │ └── winget.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── Dockerfile ├── LICENSE ├── gping.1 ├── gping ├── Cargo.toml ├── build.rs └── src │ ├── colors.rs │ ├── main.rs │ ├── plot_data.rs │ └── region_map.rs ├── images └── readme-example.gif ├── pinger ├── Cargo.toml ├── README.md ├── examples │ └── simple-ping.rs └── src │ ├── bsd.rs │ ├── fake.rs │ ├── lib.rs │ ├── linux.rs │ ├── macos.rs │ ├── target.rs │ ├── test.rs │ ├── tests │ ├── alpine.txt │ ├── android.txt │ ├── bsd.txt │ ├── debian.txt │ ├── macos.txt │ ├── ubuntu.txt │ └── windows.txt │ └── windows.rs └── readme.md /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .git/ 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | groups: 13 | dependencies: 14 | patterns: 15 | - "*" 16 | - package-ecosystem: "github-actions" # See documentation for possible values 17 | directory: "/" # Location of package manifests 18 | schedule: 19 | interval: "daily" 20 | groups: 21 | dependencies: 22 | patterns: 23 | - "*" 24 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # GitHub recommends pinning actions to a commit SHA. 7 | # To get a newer version, you will need to update the SHA. 8 | # You can also reference a tag or branch, but the action may change without warning. 9 | 10 | name: Create and publish a Docker image 11 | 12 | on: 13 | push: 14 | tags: 15 | - gping-v* 16 | branches: 17 | - master 18 | workflow_dispatch: 19 | 20 | env: 21 | REGISTRY: ghcr.io 22 | IMAGE_NAME: ${{ github.repository }} 23 | 24 | concurrency: build-docker-image 25 | 26 | jobs: 27 | build-and-push-image: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: read 31 | packages: write 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v4 36 | 37 | - name: Set up QEMU 38 | uses: docker/setup-qemu-action@v3 39 | 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | 43 | - name: Log in to the Container registry 44 | if: github.event_name == 'tag' || github.ref_name == 'master' 45 | uses: docker/login-action@v3 46 | with: 47 | registry: ${{ env.REGISTRY }} 48 | username: ${{ github.actor }} 49 | password: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Extract metadata (tags, labels) for Docker 52 | id: meta 53 | uses: docker/metadata-action@v5 54 | with: 55 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 56 | 57 | - name: Build and push Docker image 58 | uses: docker/build-push-action@v6 59 | with: 60 | context: . 61 | push: ${{ github.event_name == 'tag' || github.ref_name == 'master' }} 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} 64 | platforms: | 65 | linux/arm64 66 | linux/amd64 67 | linux/arm/v7 68 | provenance: true 69 | cache-from: type=gha 70 | cache-to: type=gha,mode=max 71 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Homebrew Bump 2 | on: 3 | push: 4 | tags: 5 | - 'gping-v*' 6 | - '!gping-v*-post*' 7 | 8 | jobs: 9 | homebrew: 10 | name: Bump Homebrew formula 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Extract version 14 | id: extract-version 15 | run: | 16 | echo "VERSION=${GITHUB_REF#refs/tags/gping-}" >> "$GITHUB_OUTPUT" 17 | - uses: mislav/bump-homebrew-formula-action@v3 18 | with: 19 | formula-name: gping 20 | commit-message: | 21 | gping ${{ steps.extract-version.outputs.VERSION }} 22 | 23 | Created by https://github.com/mislav/bump-homebrew-formula-action 24 | env: 25 | COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - gping-v* 5 | branches: 6 | - master 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | name: CI 11 | 12 | jobs: 13 | cross_builds: 14 | name: ${{ matrix.target }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - target: aarch64-apple-darwin 21 | os: macos-latest 22 | - target: x86_64-apple-darwin 23 | os: macos-latest 24 | - target: x86_64-pc-windows-msvc 25 | os: windows-latest 26 | archive: zip 27 | os: [ 'ubuntu-24.04' ] 28 | target: 29 | - armv7-linux-androideabi 30 | - armv7-unknown-linux-gnueabihf 31 | - armv7-unknown-linux-musleabihf 32 | - x86_64-unknown-linux-gnu 33 | - x86_64-unknown-linux-musl 34 | - aarch64-unknown-linux-gnu 35 | - aarch64-unknown-linux-musl 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Install Rust 40 | id: rust 41 | uses: actions-rust-lang/setup-rust-toolchain@v1 42 | with: 43 | cache: 'false' 44 | cache-on-failure: false 45 | target: ${{ matrix.target }} 46 | 47 | - name: Setup Rust Caching 48 | uses: Swatinem/rust-cache@v2 49 | with: 50 | cache-on-failure: false 51 | prefix-key: ${{ matrix.target }} 52 | key: ${{ steps.rust.outputs.cachekey }} 53 | 54 | - name: Test 55 | uses: houseabsolute/actions-rust-cross@v1 56 | with: 57 | command: test 58 | target: ${{ matrix.target }} 59 | args: --locked 60 | 61 | - name: Sanity check 62 | if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-apple-darwin' || matrix.target == 'x86_64-pc-windows-msvc' 63 | run: cargo run --target ${{ matrix.target }} -- --help 64 | 65 | - name: Build release 66 | uses: houseabsolute/actions-rust-cross@v1 67 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 68 | with: 69 | command: build 70 | target: ${{ matrix.target }} 71 | args: --release --locked 72 | 73 | - name: Publish artifacts and release 74 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 75 | uses: houseabsolute/actions-rust-release@v0 76 | with: 77 | executable-name: gping 78 | target: ${{ matrix.target }} 79 | extra-files: gping.1 80 | 81 | create_release: 82 | name: Release 83 | runs-on: ubuntu-latest 84 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 85 | needs: 86 | - cross_builds 87 | steps: 88 | - name: Checkout sources 89 | uses: actions/checkout@v4 90 | - uses: actions/download-artifact@v4 91 | with: 92 | merge-multiple: true 93 | - name: Publish 94 | if: startsWith(github.ref, 'refs/tags/') 95 | uses: softprops/action-gh-release@v2 96 | with: 97 | draft: false 98 | files: | 99 | gping.1 100 | **/*.tar.gz 101 | **/*.zip 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | 105 | checks: 106 | name: Checks 107 | runs-on: ubuntu-20.04 108 | steps: 109 | - name: Checkout sources 110 | uses: actions/checkout@v4 111 | 112 | - uses: actions/setup-python@v5 113 | with: 114 | python-version: '3.11' 115 | 116 | - name: Install stable toolchain 117 | uses: actions-rust-lang/setup-rust-toolchain@v1 118 | with: 119 | cache-on-failure: false 120 | components: rustfmt,clippy 121 | 122 | - name: Rustfmt Check 123 | uses: actions-rust-lang/rustfmt@v1 124 | 125 | - name: Run cargo check 126 | if: success() || failure() 127 | run: cargo check 128 | 129 | - if: success() || failure() 130 | run: cargo clippy --all-targets --all-features --locked -- -D warnings 131 | 132 | - if: success() || failure() 133 | uses: pre-commit/action@v3.0.1 134 | -------------------------------------------------------------------------------- /.github/workflows/winget.yml: -------------------------------------------------------------------------------- 1 | name: Publish to WinGet 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | tag: 6 | description: Tag to release 7 | required: true 8 | release: 9 | types: [ released ] 10 | 11 | jobs: 12 | publish: 13 | runs-on: windows-latest 14 | steps: 15 | - uses: vedantmgoyal9/winget-releaser@main 16 | with: 17 | identifier: orf.gping 18 | release-tag: '${{ github.event.release.tag_name || inputs.tag }}' 19 | token: ${{ secrets.COMMITTER_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea/ 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | exclude: 'ping.1' 9 | - repo: local 10 | hooks: 11 | - id: rustfmt 12 | name: rustfmt 13 | entry: cargo fmt -- --check 14 | pass_filenames: false 15 | language: system 16 | - id: clippy 17 | name: clippy 18 | entry: cargo clippy --all-targets --all-features -- -D warnings 19 | pass_filenames: false 20 | language: system 21 | - id: mangen 22 | name: mangen 23 | entry: env GENERATE_MANPAGE="gping.1" cargo run 24 | pass_filenames: false 25 | language: system 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "allocator-api2" 16 | version = "0.2.21" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.7" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 79 | dependencies = [ 80 | "anstyle", 81 | "once_cell", 82 | "windows-sys 0.59.0", 83 | ] 84 | 85 | [[package]] 86 | name = "anyhow" 87 | version = "1.0.95" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 90 | 91 | [[package]] 92 | name = "autocfg" 93 | version = "1.4.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.8.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 102 | 103 | [[package]] 104 | name = "bumpalo" 105 | version = "3.17.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 108 | 109 | [[package]] 110 | name = "byteorder" 111 | version = "1.5.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 114 | 115 | [[package]] 116 | name = "cassowary" 117 | version = "0.3.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 120 | 121 | [[package]] 122 | name = "castaway" 123 | version = "0.2.3" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 126 | dependencies = [ 127 | "rustversion", 128 | ] 129 | 130 | [[package]] 131 | name = "cc" 132 | version = "1.2.10" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" 135 | dependencies = [ 136 | "shlex", 137 | ] 138 | 139 | [[package]] 140 | name = "cfg-if" 141 | version = "1.0.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 144 | 145 | [[package]] 146 | name = "chrono" 147 | version = "0.4.39" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 150 | dependencies = [ 151 | "android-tzdata", 152 | "iana-time-zone", 153 | "js-sys", 154 | "num-traits", 155 | "wasm-bindgen", 156 | "windows-targets", 157 | ] 158 | 159 | [[package]] 160 | name = "clap" 161 | version = "4.5.27" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" 164 | dependencies = [ 165 | "clap_builder", 166 | "clap_derive", 167 | ] 168 | 169 | [[package]] 170 | name = "clap_builder" 171 | version = "4.5.27" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" 174 | dependencies = [ 175 | "anstream", 176 | "anstyle", 177 | "clap_lex", 178 | "strsim", 179 | ] 180 | 181 | [[package]] 182 | name = "clap_derive" 183 | version = "4.5.24" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" 186 | dependencies = [ 187 | "heck", 188 | "proc-macro2", 189 | "quote", 190 | "syn 2.0.96", 191 | ] 192 | 193 | [[package]] 194 | name = "clap_lex" 195 | version = "0.7.4" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 198 | 199 | [[package]] 200 | name = "clap_mangen" 201 | version = "0.2.26" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" 204 | dependencies = [ 205 | "clap", 206 | "roff", 207 | ] 208 | 209 | [[package]] 210 | name = "colorchoice" 211 | version = "1.0.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 214 | 215 | [[package]] 216 | name = "compact_str" 217 | version = "0.8.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" 220 | dependencies = [ 221 | "castaway", 222 | "cfg-if", 223 | "itoa", 224 | "rustversion", 225 | "ryu", 226 | "static_assertions", 227 | ] 228 | 229 | [[package]] 230 | name = "const_format" 231 | version = "0.2.34" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" 234 | dependencies = [ 235 | "const_format_proc_macros", 236 | ] 237 | 238 | [[package]] 239 | name = "const_format_proc_macros" 240 | version = "0.2.34" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" 243 | dependencies = [ 244 | "proc-macro2", 245 | "quote", 246 | "unicode-xid", 247 | ] 248 | 249 | [[package]] 250 | name = "core-foundation-sys" 251 | version = "0.8.7" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 254 | 255 | [[package]] 256 | name = "crossterm" 257 | version = "0.28.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 260 | dependencies = [ 261 | "bitflags", 262 | "crossterm_winapi", 263 | "mio", 264 | "parking_lot", 265 | "rustix", 266 | "signal-hook", 267 | "signal-hook-mio", 268 | "winapi", 269 | ] 270 | 271 | [[package]] 272 | name = "crossterm_winapi" 273 | version = "0.9.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 276 | dependencies = [ 277 | "winapi", 278 | ] 279 | 280 | [[package]] 281 | name = "darling" 282 | version = "0.20.10" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" 285 | dependencies = [ 286 | "darling_core", 287 | "darling_macro", 288 | ] 289 | 290 | [[package]] 291 | name = "darling_core" 292 | version = "0.20.10" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" 295 | dependencies = [ 296 | "fnv", 297 | "ident_case", 298 | "proc-macro2", 299 | "quote", 300 | "strsim", 301 | "syn 2.0.96", 302 | ] 303 | 304 | [[package]] 305 | name = "darling_macro" 306 | version = "0.20.10" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" 309 | dependencies = [ 310 | "darling_core", 311 | "quote", 312 | "syn 2.0.96", 313 | ] 314 | 315 | [[package]] 316 | name = "deranged" 317 | version = "0.3.11" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 320 | dependencies = [ 321 | "powerfmt", 322 | ] 323 | 324 | [[package]] 325 | name = "displaydoc" 326 | version = "0.2.5" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 329 | dependencies = [ 330 | "proc-macro2", 331 | "quote", 332 | "syn 2.0.96", 333 | ] 334 | 335 | [[package]] 336 | name = "either" 337 | version = "1.13.0" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 340 | 341 | [[package]] 342 | name = "equivalent" 343 | version = "1.0.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 346 | 347 | [[package]] 348 | name = "errno" 349 | version = "0.3.10" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 352 | dependencies = [ 353 | "libc", 354 | "windows-sys 0.59.0", 355 | ] 356 | 357 | [[package]] 358 | name = "fnv" 359 | version = "1.0.7" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 362 | 363 | [[package]] 364 | name = "foldhash" 365 | version = "0.1.4" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 368 | 369 | [[package]] 370 | name = "getrandom" 371 | version = "0.3.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 374 | dependencies = [ 375 | "cfg-if", 376 | "libc", 377 | "wasi 0.13.3+wasi-0.2.2", 378 | "windows-targets", 379 | ] 380 | 381 | [[package]] 382 | name = "gping" 383 | version = "1.19.0" 384 | dependencies = [ 385 | "anyhow", 386 | "chrono", 387 | "clap", 388 | "clap_mangen", 389 | "const_format", 390 | "crossterm", 391 | "idna", 392 | "itertools 0.14.0", 393 | "pinger", 394 | "ratatui", 395 | "shadow-rs", 396 | ] 397 | 398 | [[package]] 399 | name = "hashbrown" 400 | version = "0.15.2" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 403 | dependencies = [ 404 | "allocator-api2", 405 | "equivalent", 406 | "foldhash", 407 | ] 408 | 409 | [[package]] 410 | name = "heck" 411 | version = "0.5.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 414 | 415 | [[package]] 416 | name = "iana-time-zone" 417 | version = "0.1.61" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 420 | dependencies = [ 421 | "android_system_properties", 422 | "core-foundation-sys", 423 | "iana-time-zone-haiku", 424 | "js-sys", 425 | "wasm-bindgen", 426 | "windows-core", 427 | ] 428 | 429 | [[package]] 430 | name = "iana-time-zone-haiku" 431 | version = "0.1.2" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 434 | dependencies = [ 435 | "cc", 436 | ] 437 | 438 | [[package]] 439 | name = "icu_collections" 440 | version = "1.5.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 443 | dependencies = [ 444 | "displaydoc", 445 | "yoke", 446 | "zerofrom", 447 | "zerovec", 448 | ] 449 | 450 | [[package]] 451 | name = "icu_locid" 452 | version = "1.5.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 455 | dependencies = [ 456 | "displaydoc", 457 | "litemap", 458 | "tinystr", 459 | "writeable", 460 | "zerovec", 461 | ] 462 | 463 | [[package]] 464 | name = "icu_locid_transform" 465 | version = "1.5.0" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 468 | dependencies = [ 469 | "displaydoc", 470 | "icu_locid", 471 | "icu_locid_transform_data", 472 | "icu_provider", 473 | "tinystr", 474 | "zerovec", 475 | ] 476 | 477 | [[package]] 478 | name = "icu_locid_transform_data" 479 | version = "1.5.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 482 | 483 | [[package]] 484 | name = "icu_normalizer" 485 | version = "1.5.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 488 | dependencies = [ 489 | "displaydoc", 490 | "icu_collections", 491 | "icu_normalizer_data", 492 | "icu_properties", 493 | "icu_provider", 494 | "smallvec", 495 | "utf16_iter", 496 | "utf8_iter", 497 | "write16", 498 | "zerovec", 499 | ] 500 | 501 | [[package]] 502 | name = "icu_normalizer_data" 503 | version = "1.5.0" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 506 | 507 | [[package]] 508 | name = "icu_properties" 509 | version = "1.5.1" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 512 | dependencies = [ 513 | "displaydoc", 514 | "icu_collections", 515 | "icu_locid_transform", 516 | "icu_properties_data", 517 | "icu_provider", 518 | "tinystr", 519 | "zerovec", 520 | ] 521 | 522 | [[package]] 523 | name = "icu_properties_data" 524 | version = "1.5.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 527 | 528 | [[package]] 529 | name = "icu_provider" 530 | version = "1.5.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 533 | dependencies = [ 534 | "displaydoc", 535 | "icu_locid", 536 | "icu_provider_macros", 537 | "stable_deref_trait", 538 | "tinystr", 539 | "writeable", 540 | "yoke", 541 | "zerofrom", 542 | "zerovec", 543 | ] 544 | 545 | [[package]] 546 | name = "icu_provider_macros" 547 | version = "1.5.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 550 | dependencies = [ 551 | "proc-macro2", 552 | "quote", 553 | "syn 2.0.96", 554 | ] 555 | 556 | [[package]] 557 | name = "ident_case" 558 | version = "1.0.1" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 561 | 562 | [[package]] 563 | name = "idna" 564 | version = "1.0.3" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 567 | dependencies = [ 568 | "idna_adapter", 569 | "smallvec", 570 | "utf8_iter", 571 | ] 572 | 573 | [[package]] 574 | name = "idna_adapter" 575 | version = "1.2.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 578 | dependencies = [ 579 | "icu_normalizer", 580 | "icu_properties", 581 | ] 582 | 583 | [[package]] 584 | name = "indexmap" 585 | version = "2.7.1" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 588 | dependencies = [ 589 | "equivalent", 590 | "hashbrown", 591 | ] 592 | 593 | [[package]] 594 | name = "indoc" 595 | version = "2.0.5" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 598 | 599 | [[package]] 600 | name = "instability" 601 | version = "0.3.7" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" 604 | dependencies = [ 605 | "darling", 606 | "indoc", 607 | "proc-macro2", 608 | "quote", 609 | "syn 2.0.96", 610 | ] 611 | 612 | [[package]] 613 | name = "is_debug" 614 | version = "1.0.2" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "e8ea828c9d6638a5bd3d8b14e37502b4d56cae910ccf8a5b7f51c7a0eb1d0508" 617 | 618 | [[package]] 619 | name = "is_terminal_polyfill" 620 | version = "1.70.1" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 623 | 624 | [[package]] 625 | name = "itertools" 626 | version = "0.13.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 629 | dependencies = [ 630 | "either", 631 | ] 632 | 633 | [[package]] 634 | name = "itertools" 635 | version = "0.14.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 638 | dependencies = [ 639 | "either", 640 | ] 641 | 642 | [[package]] 643 | name = "itoa" 644 | version = "1.0.14" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 647 | 648 | [[package]] 649 | name = "js-sys" 650 | version = "0.3.77" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 653 | dependencies = [ 654 | "once_cell", 655 | "wasm-bindgen", 656 | ] 657 | 658 | [[package]] 659 | name = "lazy-regex" 660 | version = "3.4.1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" 663 | dependencies = [ 664 | "lazy-regex-proc_macros", 665 | "once_cell", 666 | "regex", 667 | ] 668 | 669 | [[package]] 670 | name = "lazy-regex-proc_macros" 671 | version = "3.4.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" 674 | dependencies = [ 675 | "proc-macro2", 676 | "quote", 677 | "regex", 678 | "syn 2.0.96", 679 | ] 680 | 681 | [[package]] 682 | name = "lazy_static" 683 | version = "1.5.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 686 | 687 | [[package]] 688 | name = "libc" 689 | version = "0.2.169" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 692 | 693 | [[package]] 694 | name = "linux-raw-sys" 695 | version = "0.4.15" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 698 | 699 | [[package]] 700 | name = "litemap" 701 | version = "0.7.4" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 704 | 705 | [[package]] 706 | name = "lock_api" 707 | version = "0.4.12" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 710 | dependencies = [ 711 | "autocfg", 712 | "scopeguard", 713 | ] 714 | 715 | [[package]] 716 | name = "log" 717 | version = "0.4.25" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 720 | 721 | [[package]] 722 | name = "lru" 723 | version = "0.12.5" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 726 | dependencies = [ 727 | "hashbrown", 728 | ] 729 | 730 | [[package]] 731 | name = "memchr" 732 | version = "2.7.4" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 735 | 736 | [[package]] 737 | name = "mio" 738 | version = "1.0.3" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 741 | dependencies = [ 742 | "libc", 743 | "log", 744 | "wasi 0.11.0+wasi-snapshot-preview1", 745 | "windows-sys 0.52.0", 746 | ] 747 | 748 | [[package]] 749 | name = "ntest" 750 | version = "0.9.3" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "fb183f0a1da7a937f672e5ee7b7edb727bf52b8a52d531374ba8ebb9345c0330" 753 | dependencies = [ 754 | "ntest_test_cases", 755 | "ntest_timeout", 756 | ] 757 | 758 | [[package]] 759 | name = "ntest_test_cases" 760 | version = "0.9.3" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "16d0d3f2a488592e5368ebbe996e7f1d44aa13156efad201f5b4d84e150eaa93" 763 | dependencies = [ 764 | "proc-macro2", 765 | "quote", 766 | "syn 1.0.109", 767 | ] 768 | 769 | [[package]] 770 | name = "ntest_timeout" 771 | version = "0.9.3" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "fcc7c92f190c97f79b4a332f5e81dcf68c8420af2045c936c9be0bc9de6f63b5" 774 | dependencies = [ 775 | "proc-macro-crate", 776 | "proc-macro2", 777 | "quote", 778 | "syn 1.0.109", 779 | ] 780 | 781 | [[package]] 782 | name = "num-conv" 783 | version = "0.1.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 786 | 787 | [[package]] 788 | name = "num-traits" 789 | version = "0.2.19" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 792 | dependencies = [ 793 | "autocfg", 794 | ] 795 | 796 | [[package]] 797 | name = "num_threads" 798 | version = "0.1.7" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 801 | dependencies = [ 802 | "libc", 803 | ] 804 | 805 | [[package]] 806 | name = "once_cell" 807 | version = "1.20.2" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 810 | 811 | [[package]] 812 | name = "os_info" 813 | version = "3.9.2" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "6e6520c8cc998c5741ee68ec1dc369fc47e5f0ea5320018ecf2a1ccd6328f48b" 816 | dependencies = [ 817 | "log", 818 | "serde", 819 | "windows-sys 0.52.0", 820 | ] 821 | 822 | [[package]] 823 | name = "parking_lot" 824 | version = "0.12.3" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 827 | dependencies = [ 828 | "lock_api", 829 | "parking_lot_core", 830 | ] 831 | 832 | [[package]] 833 | name = "parking_lot_core" 834 | version = "0.9.10" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 837 | dependencies = [ 838 | "cfg-if", 839 | "libc", 840 | "redox_syscall", 841 | "smallvec", 842 | "windows-targets", 843 | ] 844 | 845 | [[package]] 846 | name = "paste" 847 | version = "1.0.15" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 850 | 851 | [[package]] 852 | name = "pinger" 853 | version = "2.0.0" 854 | dependencies = [ 855 | "anyhow", 856 | "lazy-regex", 857 | "ntest", 858 | "os_info", 859 | "rand", 860 | "thiserror", 861 | "winping", 862 | ] 863 | 864 | [[package]] 865 | name = "powerfmt" 866 | version = "0.2.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 869 | 870 | [[package]] 871 | name = "ppv-lite86" 872 | version = "0.2.20" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 875 | dependencies = [ 876 | "zerocopy 0.7.35", 877 | ] 878 | 879 | [[package]] 880 | name = "proc-macro-crate" 881 | version = "3.2.0" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 884 | dependencies = [ 885 | "toml_edit", 886 | ] 887 | 888 | [[package]] 889 | name = "proc-macro2" 890 | version = "1.0.93" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 893 | dependencies = [ 894 | "unicode-ident", 895 | ] 896 | 897 | [[package]] 898 | name = "quote" 899 | version = "1.0.38" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 902 | dependencies = [ 903 | "proc-macro2", 904 | ] 905 | 906 | [[package]] 907 | name = "rand" 908 | version = "0.9.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 911 | dependencies = [ 912 | "rand_chacha", 913 | "rand_core", 914 | "zerocopy 0.8.14", 915 | ] 916 | 917 | [[package]] 918 | name = "rand_chacha" 919 | version = "0.9.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 922 | dependencies = [ 923 | "ppv-lite86", 924 | "rand_core", 925 | ] 926 | 927 | [[package]] 928 | name = "rand_core" 929 | version = "0.9.0" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" 932 | dependencies = [ 933 | "getrandom", 934 | "zerocopy 0.8.14", 935 | ] 936 | 937 | [[package]] 938 | name = "ratatui" 939 | version = "0.29.0" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 942 | dependencies = [ 943 | "bitflags", 944 | "cassowary", 945 | "compact_str", 946 | "crossterm", 947 | "indoc", 948 | "instability", 949 | "itertools 0.13.0", 950 | "lru", 951 | "paste", 952 | "strum", 953 | "unicode-segmentation", 954 | "unicode-truncate", 955 | "unicode-width 0.2.0", 956 | ] 957 | 958 | [[package]] 959 | name = "redox_syscall" 960 | version = "0.5.8" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 963 | dependencies = [ 964 | "bitflags", 965 | ] 966 | 967 | [[package]] 968 | name = "regex" 969 | version = "1.11.1" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 972 | dependencies = [ 973 | "aho-corasick", 974 | "memchr", 975 | "regex-automata", 976 | "regex-syntax", 977 | ] 978 | 979 | [[package]] 980 | name = "regex-automata" 981 | version = "0.4.9" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 984 | dependencies = [ 985 | "aho-corasick", 986 | "memchr", 987 | "regex-syntax", 988 | ] 989 | 990 | [[package]] 991 | name = "regex-syntax" 992 | version = "0.8.5" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 995 | 996 | [[package]] 997 | name = "roff" 998 | version = "0.2.2" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" 1001 | 1002 | [[package]] 1003 | name = "rustix" 1004 | version = "0.38.44" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1007 | dependencies = [ 1008 | "bitflags", 1009 | "errno", 1010 | "libc", 1011 | "linux-raw-sys", 1012 | "windows-sys 0.59.0", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "rustversion" 1017 | version = "1.0.19" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1020 | 1021 | [[package]] 1022 | name = "ryu" 1023 | version = "1.0.19" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 1026 | 1027 | [[package]] 1028 | name = "scopeguard" 1029 | version = "1.2.0" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1032 | 1033 | [[package]] 1034 | name = "serde" 1035 | version = "1.0.217" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1038 | dependencies = [ 1039 | "serde_derive", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "serde_derive" 1044 | version = "1.0.217" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1047 | dependencies = [ 1048 | "proc-macro2", 1049 | "quote", 1050 | "syn 2.0.96", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "shadow-rs" 1055 | version = "0.38.0" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "69d433b5df1e1958a668457ebe4a9c5b7bcfe844f4eb2276ac43cf273baddd54" 1058 | dependencies = [ 1059 | "const_format", 1060 | "is_debug", 1061 | "time", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "shlex" 1066 | version = "1.3.0" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1069 | 1070 | [[package]] 1071 | name = "signal-hook" 1072 | version = "0.3.17" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 1075 | dependencies = [ 1076 | "libc", 1077 | "signal-hook-registry", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "signal-hook-mio" 1082 | version = "0.2.4" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1085 | dependencies = [ 1086 | "libc", 1087 | "mio", 1088 | "signal-hook", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "signal-hook-registry" 1093 | version = "1.4.2" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1096 | dependencies = [ 1097 | "libc", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "smallvec" 1102 | version = "1.13.2" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1105 | 1106 | [[package]] 1107 | name = "stable_deref_trait" 1108 | version = "1.2.0" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1111 | 1112 | [[package]] 1113 | name = "static_assertions" 1114 | version = "1.1.0" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1117 | 1118 | [[package]] 1119 | name = "strsim" 1120 | version = "0.11.1" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1123 | 1124 | [[package]] 1125 | name = "strum" 1126 | version = "0.26.3" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 1129 | dependencies = [ 1130 | "strum_macros", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "strum_macros" 1135 | version = "0.26.4" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 1138 | dependencies = [ 1139 | "heck", 1140 | "proc-macro2", 1141 | "quote", 1142 | "rustversion", 1143 | "syn 2.0.96", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "syn" 1148 | version = "1.0.109" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1151 | dependencies = [ 1152 | "proc-macro2", 1153 | "quote", 1154 | "unicode-ident", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "syn" 1159 | version = "2.0.96" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 1162 | dependencies = [ 1163 | "proc-macro2", 1164 | "quote", 1165 | "unicode-ident", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "synstructure" 1170 | version = "0.13.1" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1173 | dependencies = [ 1174 | "proc-macro2", 1175 | "quote", 1176 | "syn 2.0.96", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "thiserror" 1181 | version = "2.0.11" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1184 | dependencies = [ 1185 | "thiserror-impl", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "thiserror-impl" 1190 | version = "2.0.11" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1193 | dependencies = [ 1194 | "proc-macro2", 1195 | "quote", 1196 | "syn 2.0.96", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "time" 1201 | version = "0.3.37" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 1204 | dependencies = [ 1205 | "deranged", 1206 | "itoa", 1207 | "libc", 1208 | "num-conv", 1209 | "num_threads", 1210 | "powerfmt", 1211 | "serde", 1212 | "time-core", 1213 | "time-macros", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "time-core" 1218 | version = "0.1.2" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1221 | 1222 | [[package]] 1223 | name = "time-macros" 1224 | version = "0.2.19" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 1227 | dependencies = [ 1228 | "num-conv", 1229 | "time-core", 1230 | ] 1231 | 1232 | [[package]] 1233 | name = "tinystr" 1234 | version = "0.7.6" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1237 | dependencies = [ 1238 | "displaydoc", 1239 | "zerovec", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "toml_datetime" 1244 | version = "0.6.8" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1247 | 1248 | [[package]] 1249 | name = "toml_edit" 1250 | version = "0.22.23" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" 1253 | dependencies = [ 1254 | "indexmap", 1255 | "toml_datetime", 1256 | "winnow", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "unicode-ident" 1261 | version = "1.0.16" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 1264 | 1265 | [[package]] 1266 | name = "unicode-segmentation" 1267 | version = "1.12.0" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1270 | 1271 | [[package]] 1272 | name = "unicode-truncate" 1273 | version = "1.1.0" 1274 | source = "registry+https://github.com/rust-lang/crates.io-index" 1275 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 1276 | dependencies = [ 1277 | "itertools 0.13.0", 1278 | "unicode-segmentation", 1279 | "unicode-width 0.1.14", 1280 | ] 1281 | 1282 | [[package]] 1283 | name = "unicode-width" 1284 | version = "0.1.14" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1287 | 1288 | [[package]] 1289 | name = "unicode-width" 1290 | version = "0.2.0" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1293 | 1294 | [[package]] 1295 | name = "unicode-xid" 1296 | version = "0.2.6" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 1299 | 1300 | [[package]] 1301 | name = "utf16_iter" 1302 | version = "1.0.5" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1305 | 1306 | [[package]] 1307 | name = "utf8_iter" 1308 | version = "1.0.4" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1311 | 1312 | [[package]] 1313 | name = "utf8parse" 1314 | version = "0.2.2" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1317 | 1318 | [[package]] 1319 | name = "wasi" 1320 | version = "0.11.0+wasi-snapshot-preview1" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1323 | 1324 | [[package]] 1325 | name = "wasi" 1326 | version = "0.13.3+wasi-0.2.2" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1329 | dependencies = [ 1330 | "wit-bindgen-rt", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "wasm-bindgen" 1335 | version = "0.2.100" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1338 | dependencies = [ 1339 | "cfg-if", 1340 | "once_cell", 1341 | "rustversion", 1342 | "wasm-bindgen-macro", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "wasm-bindgen-backend" 1347 | version = "0.2.100" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1350 | dependencies = [ 1351 | "bumpalo", 1352 | "log", 1353 | "proc-macro2", 1354 | "quote", 1355 | "syn 2.0.96", 1356 | "wasm-bindgen-shared", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "wasm-bindgen-macro" 1361 | version = "0.2.100" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1364 | dependencies = [ 1365 | "quote", 1366 | "wasm-bindgen-macro-support", 1367 | ] 1368 | 1369 | [[package]] 1370 | name = "wasm-bindgen-macro-support" 1371 | version = "0.2.100" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1374 | dependencies = [ 1375 | "proc-macro2", 1376 | "quote", 1377 | "syn 2.0.96", 1378 | "wasm-bindgen-backend", 1379 | "wasm-bindgen-shared", 1380 | ] 1381 | 1382 | [[package]] 1383 | name = "wasm-bindgen-shared" 1384 | version = "0.2.100" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1387 | dependencies = [ 1388 | "unicode-ident", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "winapi" 1393 | version = "0.3.9" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1396 | dependencies = [ 1397 | "winapi-i686-pc-windows-gnu", 1398 | "winapi-x86_64-pc-windows-gnu", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "winapi-i686-pc-windows-gnu" 1403 | version = "0.4.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1406 | 1407 | [[package]] 1408 | name = "winapi-x86_64-pc-windows-gnu" 1409 | version = "0.4.0" 1410 | source = "registry+https://github.com/rust-lang/crates.io-index" 1411 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1412 | 1413 | [[package]] 1414 | name = "winapi_forked_icmpapi" 1415 | version = "0.3.7" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "42aecb895d6340af9ccc8dab9aeabfeab6d5d7266c5fd172c8be7e07db71c1e3" 1418 | dependencies = [ 1419 | "winapi-i686-pc-windows-gnu", 1420 | "winapi-x86_64-pc-windows-gnu", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "windows-core" 1425 | version = "0.52.0" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1428 | dependencies = [ 1429 | "windows-targets", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "windows-sys" 1434 | version = "0.52.0" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1437 | dependencies = [ 1438 | "windows-targets", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "windows-sys" 1443 | version = "0.59.0" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1446 | dependencies = [ 1447 | "windows-targets", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "windows-targets" 1452 | version = "0.52.6" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1455 | dependencies = [ 1456 | "windows_aarch64_gnullvm", 1457 | "windows_aarch64_msvc", 1458 | "windows_i686_gnu", 1459 | "windows_i686_gnullvm", 1460 | "windows_i686_msvc", 1461 | "windows_x86_64_gnu", 1462 | "windows_x86_64_gnullvm", 1463 | "windows_x86_64_msvc", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "windows_aarch64_gnullvm" 1468 | version = "0.52.6" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1471 | 1472 | [[package]] 1473 | name = "windows_aarch64_msvc" 1474 | version = "0.52.6" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1477 | 1478 | [[package]] 1479 | name = "windows_i686_gnu" 1480 | version = "0.52.6" 1481 | source = "registry+https://github.com/rust-lang/crates.io-index" 1482 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1483 | 1484 | [[package]] 1485 | name = "windows_i686_gnullvm" 1486 | version = "0.52.6" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1489 | 1490 | [[package]] 1491 | name = "windows_i686_msvc" 1492 | version = "0.52.6" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1495 | 1496 | [[package]] 1497 | name = "windows_x86_64_gnu" 1498 | version = "0.52.6" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1501 | 1502 | [[package]] 1503 | name = "windows_x86_64_gnullvm" 1504 | version = "0.52.6" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1507 | 1508 | [[package]] 1509 | name = "windows_x86_64_msvc" 1510 | version = "0.52.6" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1513 | 1514 | [[package]] 1515 | name = "winnow" 1516 | version = "0.7.0" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" 1519 | dependencies = [ 1520 | "memchr", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "winping" 1525 | version = "0.10.1" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "79ed0e3a789beb896b3de9fb7e93c76340f6f4adfab7770d6222b4b8625ef0aa" 1528 | dependencies = [ 1529 | "lazy_static", 1530 | "static_assertions", 1531 | "winapi_forked_icmpapi", 1532 | ] 1533 | 1534 | [[package]] 1535 | name = "wit-bindgen-rt" 1536 | version = "0.33.0" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1539 | dependencies = [ 1540 | "bitflags", 1541 | ] 1542 | 1543 | [[package]] 1544 | name = "write16" 1545 | version = "1.0.0" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1548 | 1549 | [[package]] 1550 | name = "writeable" 1551 | version = "0.5.5" 1552 | source = "registry+https://github.com/rust-lang/crates.io-index" 1553 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1554 | 1555 | [[package]] 1556 | name = "yoke" 1557 | version = "0.7.5" 1558 | source = "registry+https://github.com/rust-lang/crates.io-index" 1559 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1560 | dependencies = [ 1561 | "serde", 1562 | "stable_deref_trait", 1563 | "yoke-derive", 1564 | "zerofrom", 1565 | ] 1566 | 1567 | [[package]] 1568 | name = "yoke-derive" 1569 | version = "0.7.5" 1570 | source = "registry+https://github.com/rust-lang/crates.io-index" 1571 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1572 | dependencies = [ 1573 | "proc-macro2", 1574 | "quote", 1575 | "syn 2.0.96", 1576 | "synstructure", 1577 | ] 1578 | 1579 | [[package]] 1580 | name = "zerocopy" 1581 | version = "0.7.35" 1582 | source = "registry+https://github.com/rust-lang/crates.io-index" 1583 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1584 | dependencies = [ 1585 | "byteorder", 1586 | "zerocopy-derive 0.7.35", 1587 | ] 1588 | 1589 | [[package]] 1590 | name = "zerocopy" 1591 | version = "0.8.14" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" 1594 | dependencies = [ 1595 | "zerocopy-derive 0.8.14", 1596 | ] 1597 | 1598 | [[package]] 1599 | name = "zerocopy-derive" 1600 | version = "0.7.35" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1603 | dependencies = [ 1604 | "proc-macro2", 1605 | "quote", 1606 | "syn 2.0.96", 1607 | ] 1608 | 1609 | [[package]] 1610 | name = "zerocopy-derive" 1611 | version = "0.8.14" 1612 | source = "registry+https://github.com/rust-lang/crates.io-index" 1613 | checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" 1614 | dependencies = [ 1615 | "proc-macro2", 1616 | "quote", 1617 | "syn 2.0.96", 1618 | ] 1619 | 1620 | [[package]] 1621 | name = "zerofrom" 1622 | version = "0.1.5" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1625 | dependencies = [ 1626 | "zerofrom-derive", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "zerofrom-derive" 1631 | version = "0.1.5" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1634 | dependencies = [ 1635 | "proc-macro2", 1636 | "quote", 1637 | "syn 2.0.96", 1638 | "synstructure", 1639 | ] 1640 | 1641 | [[package]] 1642 | name = "zerovec" 1643 | version = "0.10.4" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1646 | dependencies = [ 1647 | "yoke", 1648 | "zerofrom", 1649 | "zerovec-derive", 1650 | ] 1651 | 1652 | [[package]] 1653 | name = "zerovec-derive" 1654 | version = "0.10.3" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1657 | dependencies = [ 1658 | "proc-macro2", 1659 | "quote", 1660 | "syn 2.0.96", 1661 | ] 1662 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "gping", 5 | "pinger" 6 | ] 7 | 8 | [profile.release] 9 | lto = true 10 | codegen-units = 1 11 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | #[target."armv7-linux-androideabi"] 2 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 3 | # 4 | #[target."armv7-unknown-linux-gnueabihf"] 5 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 6 | # 7 | #[target."armv7-unknown-linux-musleabihf"] 8 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 9 | # 10 | #[target."aarch64-linux-android"] 11 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 12 | # 13 | #[target."aarch64-unknown-linux-gnu"] 14 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 15 | # 16 | #[target."aarch64-unknown-linux-musl"] 17 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 18 | # 19 | #[target."x86_64-unknown-linux-musl"] 20 | #pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 21 | # 22 | 23 | [build] 24 | pre-build = ["apt-get update && apt-get install --assume-yes iputils-ping"] 25 | 26 | [build.env] 27 | passthrough = ["CI", "GITHUB_ACTIONS"] 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM rust:slim-bookworm AS builder 4 | 5 | WORKDIR /usr/src/gping 6 | 7 | COPY gping/ gping/ 8 | COPY pinger/ pinger/ 9 | COPY Cargo.* ./ 10 | 11 | RUN cargo install --locked --path ./gping 12 | 13 | 14 | FROM debian:bookworm-slim 15 | 16 | RUN apt-get update \ 17 | && apt-get install -y iputils-ping \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | COPY --link --from=builder /usr/local/cargo/bin/gping /usr/local/bin/gping 21 | 22 | ENTRYPOINT ["gping"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tom Forbes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gping.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH gping 1 "gping " 4 | .SH NAME 5 | gping \- Ping, but with a graph. 6 | .SH SYNOPSIS 7 | \fBgping\fR [\fB\-\-cmd\fR] [\fB\-n\fR|\fB\-\-watch\-interval\fR] [\fB\-b\fR|\fB\-\-buffer\fR] [\fB\-4 \fR] [\fB\-6 \fR] [\fB\-i\fR|\fB\-\-interface\fR] [\fB\-s\fR|\fB\-\-simple\-graphics\fR] [\fB\-\-vertical\-margin\fR] [\fB\-\-horizontal\-margin\fR] [\fB\-c\fR|\fB\-\-color\fR] [\fB\-\-clear\fR] [\fB\-\-ping\-args\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIHOSTS_OR_COMMANDS\fR] 8 | .SH DESCRIPTION 9 | Ping, but with a graph. 10 | .SH OPTIONS 11 | .TP 12 | \fB\-\-cmd\fR 13 | Graph the execution time for a list of commands rather than pinging hosts 14 | .TP 15 | \fB\-n\fR, \fB\-\-watch\-interval\fR=\fIWATCH_INTERVAL\fR 16 | Watch interval seconds (provide partial seconds like \*(Aq0.5\*(Aq). Default for ping is 0.2, default for cmd is 0.5 17 | .TP 18 | \fB\-b\fR, \fB\-\-buffer\fR=\fIBUFFER\fR [default: 30] 19 | Determines the number of seconds to display in the graph 20 | .TP 21 | \fB\-4\fR 22 | Resolve ping targets to IPv4 address 23 | .TP 24 | \fB\-6\fR 25 | Resolve ping targets to IPv6 address 26 | .TP 27 | \fB\-i\fR, \fB\-\-interface\fR=\fIINTERFACE\fR 28 | Interface to use when pinging 29 | .TP 30 | \fB\-s\fR, \fB\-\-simple\-graphics\fR 31 | 32 | .TP 33 | \fB\-\-vertical\-margin\fR=\fIVERTICAL_MARGIN\fR [default: 1] 34 | Vertical margin around the graph (top and bottom) 35 | .TP 36 | \fB\-\-horizontal\-margin\fR=\fIHORIZONTAL_MARGIN\fR [default: 0] 37 | Horizontal margin around the graph (left and right) 38 | .TP 39 | \fB\-c\fR, \fB\-\-color\fR=\fIcolor\fR 40 | Assign color to a graph entry. 41 | 42 | This option can be defined more than once as a comma separated string, and the 43 | order which the colors are provided will be matched against the hosts or 44 | commands passed to gping. 45 | 46 | Hexadecimal RGB color codes are accepted in the form of \*(Aq#RRGGBB\*(Aq or the 47 | following color names: \*(Aqblack\*(Aq, \*(Aqred\*(Aq, \*(Aqgreen\*(Aq, \*(Aqyellow\*(Aq, \*(Aqblue\*(Aq, \*(Aqmagenta\*(Aq, 48 | \*(Aqcyan\*(Aq, \*(Aqgray\*(Aq, \*(Aqdark\-gray\*(Aq, \*(Aqlight\-red\*(Aq, \*(Aqlight\-green\*(Aq, \*(Aqlight\-yellow\*(Aq, 49 | \*(Aqlight\-blue\*(Aq, \*(Aqlight\-magenta\*(Aq, \*(Aqlight\-cyan\*(Aq, and \*(Aqwhite\*(Aq 50 | .TP 51 | \fB\-\-clear\fR 52 | Clear the graph from the terminal after closing the program 53 | .TP 54 | \fB\-\-ping\-args\fR=\fIPING_ARGS\fR 55 | Extra arguments to pass to `ping`. These are platform dependent 56 | .TP 57 | \fB\-h\fR, \fB\-\-help\fR 58 | Print help 59 | .TP 60 | [\fIHOSTS_OR_COMMANDS\fR] 61 | Hosts or IPs to ping, or commands to run if \-\-cmd is provided. Can use cloud shorthands like aws:eu\-west\-1 62 | .SH AUTHORS 63 | Tom Forbes 64 | -------------------------------------------------------------------------------- /gping/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gping" 3 | version = "1.19.0" 4 | authors = ["Tom Forbes "] 5 | edition = "2018" 6 | repository = "https://github.com/orf/gping" 7 | license = "MIT" 8 | description = "Ping, but with a graph." 9 | build = "build.rs" 10 | readme = "../readme.md" 11 | 12 | [dependencies] 13 | pinger = { version = "^2.0.0", path = "../pinger" } 14 | tui = { package = "ratatui", version = "0.29.0", features = ["crossterm"], default-features = false } 15 | crossterm = "0.28.1" 16 | anyhow = "1.0.95" 17 | chrono = "0.4.39" 18 | itertools = "0.14.0" 19 | shadow-rs = { version = "0.38.0", default-features = false } 20 | const_format = "0.2.34" 21 | clap = { version = "4.5.27", features = ["derive"] } 22 | clap_mangen = "0.2.26" 23 | idna = "1.0.3" 24 | 25 | [build-dependencies] 26 | shadow-rs = { version = "0.38.0", default-features = false } 27 | -------------------------------------------------------------------------------- /gping/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | shadow_rs::ShadowBuilder::builder().build().unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /gping/src/colors.rs: -------------------------------------------------------------------------------- 1 | use std::{iter::Iterator, ops::RangeFrom, str::FromStr}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | use tui::style::Color; 5 | 6 | pub struct Colors { 7 | already_used: Vec, 8 | color_names: T, 9 | indices: RangeFrom, 10 | } 11 | 12 | impl From for Colors { 13 | fn from(color_names: T) -> Self { 14 | Self { 15 | already_used: Vec::new(), 16 | color_names, 17 | indices: 2.., 18 | } 19 | } 20 | } 21 | 22 | impl<'a, T> Iterator for Colors 23 | where 24 | T: Iterator, 25 | { 26 | type Item = Result; 27 | 28 | fn next(&mut self) -> Option { 29 | match self.color_names.next() { 30 | Some(name) => match Color::from_str(name) { 31 | Ok(color) => { 32 | if !self.already_used.contains(&color) { 33 | self.already_used.push(color); 34 | } 35 | Some(Ok(color)) 36 | } 37 | error => Some(error.map_err(|err| { 38 | anyhow!(err).context(format!("Invalid color code: `{}`", name)) 39 | })), 40 | }, 41 | None => loop { 42 | let index = unsafe { self.indices.next().unwrap_unchecked() }; 43 | let color = Color::Indexed(index); 44 | if !self.already_used.contains(&color) { 45 | self.already_used.push(color); 46 | break Some(Ok(color)); 47 | } 48 | }, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /gping/src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::plot_data::PlotData; 2 | use anyhow::{anyhow, bail, Context, Result}; 3 | use chrono::prelude::*; 4 | use clap::{CommandFactory, Parser}; 5 | use crossterm::event::KeyModifiers; 6 | use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; 7 | use crossterm::{ 8 | event::{self, Event as CEvent, KeyCode}, 9 | execute, 10 | terminal::{disable_raw_mode, enable_raw_mode, SetSize}, 11 | }; 12 | use itertools::{Itertools, MinMaxResult}; 13 | use pinger::{ping, PingOptions, PingResult}; 14 | use std::io; 15 | use std::io::BufWriter; 16 | use std::iter; 17 | use std::net::{IpAddr, ToSocketAddrs}; 18 | use std::ops::Add; 19 | use std::path::Path; 20 | use std::process::{Command, ExitStatus, Stdio}; 21 | use std::sync::atomic::{AtomicBool, Ordering}; 22 | use std::sync::mpsc::Sender; 23 | use std::sync::{mpsc, Arc}; 24 | use std::thread; 25 | use std::thread::{sleep, JoinHandle}; 26 | use std::time::{Duration, Instant}; 27 | use tui::backend::{Backend, CrosstermBackend}; 28 | use tui::layout::{Constraint, Direction, Flex, Layout}; 29 | use tui::style::{Color, Style}; 30 | use tui::text::Span; 31 | use tui::widgets::{Axis, Block, Borders, Chart, Dataset}; 32 | use tui::Terminal; 33 | 34 | mod colors; 35 | mod plot_data; 36 | mod region_map; 37 | 38 | use colors::Colors; 39 | use shadow_rs::{formatcp, shadow}; 40 | use tui::prelude::Position; 41 | 42 | shadow!(build); 43 | 44 | const VERSION_INFO: &str = formatcp!( 45 | r#"{} 46 | commit_hash: {} 47 | build_time: {} 48 | build_env: {},{}"#, 49 | build::PKG_VERSION, 50 | build::SHORT_COMMIT, 51 | build::BUILD_TIME, 52 | build::RUST_VERSION, 53 | build::RUST_CHANNEL 54 | ); 55 | 56 | #[derive(Parser, Debug)] 57 | #[command(author, version=build::PKG_VERSION, name = "gping", about = "Ping, but with a graph.", long_version = VERSION_INFO 58 | )] 59 | struct Args { 60 | /// Graph the execution time for a list of commands rather than pinging hosts 61 | #[arg(long)] 62 | cmd: bool, 63 | 64 | /// Watch interval seconds (provide partial seconds like '0.5'). Default for ping is 0.2, default for cmd is 0.5. 65 | #[arg(short = 'n', long)] 66 | watch_interval: Option, 67 | 68 | /// Hosts or IPs to ping, or commands to run if --cmd is provided. Can use cloud shorthands like aws:eu-west-1. 69 | #[arg(allow_hyphen_values = false)] 70 | hosts_or_commands: Vec, 71 | 72 | /// Determines the number of seconds to display in the graph. 73 | #[arg(short, long, default_value = "30")] 74 | buffer: u64, 75 | /// Resolve ping targets to IPv4 address 76 | #[arg(short = '4', conflicts_with = "ipv6")] 77 | ipv4: bool, 78 | /// Resolve ping targets to IPv6 address 79 | #[arg(short = '6', conflicts_with = "ipv4")] 80 | ipv6: bool, 81 | 82 | #[cfg(not(target_os = "windows"))] 83 | /// Interface to use when pinging. 84 | #[arg(short = 'i', long)] 85 | interface: Option, 86 | 87 | /// Uses dot characters instead of braille 88 | #[arg(short = 's', long, help = "")] 89 | simple_graphics: bool, 90 | 91 | /// Vertical margin around the graph (top and bottom) 92 | #[arg(long, default_value = "1")] 93 | vertical_margin: u16, 94 | 95 | /// Horizontal margin around the graph (left and right) 96 | #[arg(long, default_value = "0")] 97 | horizontal_margin: u16, 98 | 99 | #[arg( 100 | name = "color", 101 | short = 'c', 102 | long = "color", 103 | use_value_delimiter = true, 104 | value_delimiter = ',', 105 | help = r#"Assign color to a graph entry. 106 | 107 | This option can be defined more than once as a comma separated string, and the 108 | order which the colors are provided will be matched against the hosts or 109 | commands passed to gping. 110 | 111 | Hexadecimal RGB color codes are accepted in the form of '#RRGGBB' or the 112 | following color names: 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 113 | 'cyan', 'gray', 'dark-gray', 'light-red', 'light-green', 'light-yellow', 114 | 'light-blue', 'light-magenta', 'light-cyan', and 'white'"# 115 | )] 116 | color_codes_or_names: Vec, 117 | 118 | /// Clear the graph from the terminal after closing the program 119 | #[arg(name = "clear", long = "clear", action)] 120 | clear: bool, 121 | 122 | #[cfg(not(target_os = "windows"))] 123 | /// Extra arguments to pass to `ping`. These are platform dependent. 124 | #[arg(long, allow_hyphen_values = true, num_args = 0.., conflicts_with="cmd")] 125 | ping_args: Option>, 126 | } 127 | 128 | struct App { 129 | data: Vec, 130 | display_interval: chrono::Duration, 131 | started: chrono::DateTime, 132 | } 133 | 134 | impl App { 135 | fn new(data: Vec, buffer: u64) -> Self { 136 | App { 137 | data, 138 | display_interval: chrono::Duration::from_std(Duration::from_secs(buffer)).unwrap(), 139 | started: Local::now(), 140 | } 141 | } 142 | 143 | fn update(&mut self, host_idx: usize, item: Option) { 144 | let host = &mut self.data[host_idx]; 145 | host.update(item); 146 | } 147 | 148 | fn y_axis_bounds(&self) -> [f64; 2] { 149 | // Find the Y axis bounds for our chart. 150 | // This is trickier than the x-axis. We iterate through all our PlotData structs 151 | // and find the min/max of all the values. Then we add a 10% buffer to them. 152 | let (min, max) = match self 153 | .data 154 | .iter() 155 | .flat_map(|b| b.data.as_slice()) 156 | .map(|v| v.1) 157 | .filter(|v| !v.is_nan()) 158 | .minmax() 159 | { 160 | MinMaxResult::NoElements => (f64::INFINITY, 0_f64), 161 | MinMaxResult::OneElement(elm) => (elm, elm), 162 | MinMaxResult::MinMax(min, max) => (min, max), 163 | }; 164 | 165 | // Add a 10% buffer to the top and bottom 166 | let max_10_percent = (max * 10_f64) / 100_f64; 167 | let min_10_percent = (min * 10_f64) / 100_f64; 168 | [min - min_10_percent, max + max_10_percent] 169 | } 170 | 171 | fn x_axis_bounds(&self) -> [f64; 2] { 172 | let now = Local::now(); 173 | let now_idx; 174 | let before_idx; 175 | if (now - self.started) < self.display_interval { 176 | now_idx = (self.started + self.display_interval).timestamp_millis() as f64 / 1_000f64; 177 | before_idx = self.started.timestamp_millis() as f64 / 1_000f64; 178 | } else { 179 | now_idx = now.timestamp_millis() as f64 / 1_000f64; 180 | let before = now - self.display_interval; 181 | before_idx = before.timestamp_millis() as f64 / 1_000f64; 182 | } 183 | 184 | [before_idx, now_idx] 185 | } 186 | 187 | fn x_axis_labels(&self, bounds: [f64; 2]) -> Vec { 188 | let lower_utc = DateTime::::from_timestamp(bounds[0] as i64, 0) 189 | .expect("Error parsing x-axis bounds 0"); 190 | let upper_utc = DateTime::::from_timestamp(bounds[1] as i64, 0) 191 | .expect("Error parsing x-asis bounds 1"); 192 | let lower: DateTime = DateTime::from(lower_utc); 193 | let upper: DateTime = DateTime::from(upper_utc); 194 | let diff = (upper - lower) / 2; 195 | let midpoint = lower + diff; 196 | vec![ 197 | Span::raw(format!("{:?}", lower.time())), 198 | Span::raw(format!("{:?}", midpoint.time())), 199 | Span::raw(format!("{:?}", upper.time())), 200 | ] 201 | } 202 | 203 | fn y_axis_labels(&self, bounds: [f64; 2]) -> Vec { 204 | // Create 7 labels for our y axis, based on the y-axis bounds we computed above. 205 | let min = bounds[0]; 206 | let max = bounds[1]; 207 | 208 | let difference = max - min; 209 | let num_labels = 7; 210 | // Split difference into one chunk for each of the 7 labels 211 | let increment = Duration::from_micros((difference / num_labels as f64) as u64); 212 | let duration = Duration::from_micros(min as u64); 213 | 214 | (0..num_labels) 215 | .map(|i| Span::raw(format!("{:?}", duration.add(increment * i)))) 216 | .collect() 217 | } 218 | } 219 | 220 | #[derive(Debug)] 221 | enum Update { 222 | Result(Duration), 223 | Timeout, 224 | Unknown, 225 | Terminated(ExitStatus, String), 226 | } 227 | 228 | impl From for Update { 229 | fn from(result: PingResult) -> Self { 230 | match result { 231 | PingResult::Pong(duration, _) => Update::Result(duration), 232 | PingResult::Timeout(_) => Update::Timeout, 233 | PingResult::Unknown(_) => Update::Unknown, 234 | PingResult::PingExited(e, stderr) => Update::Terminated(e, stderr), 235 | } 236 | } 237 | } 238 | 239 | #[derive(Debug)] 240 | enum Event { 241 | Update(usize, Update), 242 | Terminate, 243 | Render, 244 | } 245 | 246 | fn start_render_thread( 247 | kill_event: Arc, 248 | cmd_tx: Sender, 249 | ) -> JoinHandle> { 250 | thread::spawn(move || { 251 | while !kill_event.load(Ordering::Acquire) { 252 | sleep(Duration::from_millis(250)); 253 | cmd_tx.send(Event::Render)?; 254 | } 255 | Ok(()) 256 | }) 257 | } 258 | 259 | fn start_cmd_thread( 260 | watch_cmd: &str, 261 | host_id: usize, 262 | watch_interval: Option, 263 | cmd_tx: Sender, 264 | kill_event: Arc, 265 | ) -> JoinHandle> { 266 | let mut words = watch_cmd.split_ascii_whitespace(); 267 | let cmd = words 268 | .next() 269 | .expect("Must specify a command to watch") 270 | .to_string(); 271 | let cmd_args = words.map(|w| w.to_string()).collect::>(); 272 | 273 | let interval = Duration::from_millis((watch_interval.unwrap_or(0.5) * 1000.0) as u64); 274 | 275 | // Pump cmd watches into the queue 276 | thread::spawn(move || -> Result<()> { 277 | while !kill_event.load(Ordering::Acquire) { 278 | let start = Instant::now(); 279 | let mut child = Command::new(&cmd) 280 | .args(&cmd_args) 281 | .stderr(Stdio::null()) 282 | .stdout(Stdio::null()) 283 | .spawn()?; 284 | let status = child.wait()?; 285 | let duration = start.elapsed(); 286 | let update = if status.success() { 287 | Update::Result(duration) 288 | } else { 289 | Update::Timeout 290 | }; 291 | cmd_tx.send(Event::Update(host_id, update))?; 292 | sleep(interval); 293 | } 294 | Ok(()) 295 | }) 296 | } 297 | 298 | fn start_ping_thread( 299 | options: PingOptions, 300 | host_id: usize, 301 | ping_tx: Sender, 302 | kill_event: Arc, 303 | ) -> Result>> { 304 | let stream = ping(options)?; 305 | // Pump ping messages into the queue 306 | Ok(thread::spawn(move || -> Result<()> { 307 | while !kill_event.load(Ordering::Acquire) { 308 | match stream.recv() { 309 | Ok(v) => { 310 | ping_tx.send(Event::Update(host_id, v.into()))?; 311 | } 312 | Err(_) => { 313 | // Stream closed, just break 314 | return Ok(()); 315 | } 316 | } 317 | } 318 | Ok(()) 319 | })) 320 | } 321 | 322 | fn get_host_ipaddr(host: &str, force_ipv4: bool, force_ipv6: bool) -> Result { 323 | let mut host = host.to_string(); 324 | if !host.is_ascii() { 325 | let Ok(encoded_host) = idna::domain_to_ascii(&host) else { 326 | bail!("Could not encode host {host} to punycode") 327 | }; 328 | host = encoded_host; 329 | } 330 | let ipaddr: Vec<_> = (host.as_str(), 80) 331 | .to_socket_addrs() 332 | .with_context(|| format!("Resolving {host}"))? 333 | .map(|s| s.ip()) 334 | .collect(); 335 | if ipaddr.is_empty() { 336 | bail!("Could not resolve hostname {}", host) 337 | } 338 | let ipaddr = if force_ipv4 { 339 | ipaddr 340 | .iter() 341 | .find(|ip| matches!(ip, IpAddr::V4(_))) 342 | .ok_or_else(|| anyhow!("Could not resolve '{}' to IPv4", host)) 343 | } else if force_ipv6 { 344 | ipaddr 345 | .iter() 346 | .find(|ip| matches!(ip, IpAddr::V6(_))) 347 | .ok_or_else(|| anyhow!("Could not resolve '{}' to IPv6", host)) 348 | } else { 349 | ipaddr 350 | .first() 351 | .ok_or_else(|| anyhow!("Could not resolve '{}' to IP", host)) 352 | }; 353 | Ok(ipaddr?.to_string()) 354 | } 355 | 356 | fn generate_man_page(path: &Path) -> anyhow::Result<()> { 357 | let man = clap_mangen::Man::new(Args::command().version(None).long_version(None)); 358 | let mut buffer: Vec = Default::default(); 359 | man.render(&mut buffer)?; 360 | 361 | std::fs::write(path, buffer)?; 362 | Ok(()) 363 | } 364 | 365 | fn main() -> Result<()> { 366 | if let Some(path) = std::env::var_os("GENERATE_MANPAGE") { 367 | return generate_man_page(Path::new(&path)); 368 | }; 369 | let args: Args = Args::parse(); 370 | 371 | if args.hosts_or_commands.is_empty() { 372 | return Err(anyhow!("At least one host or command must be given (i.e gping google.com). Use --help for a full list of arguments.")); 373 | } 374 | 375 | let mut data = vec![]; 376 | 377 | let colors = Colors::from(args.color_codes_or_names.iter()); 378 | let hosts_or_commands: Vec = args 379 | .hosts_or_commands 380 | .clone() 381 | .into_iter() 382 | .map(|s| match region_map::try_host_from_cloud_region(&s) { 383 | None => s, 384 | Some(new_domain) => new_domain, 385 | }) 386 | .collect(); 387 | 388 | for (host_or_cmd, color) in hosts_or_commands.iter().zip(colors) { 389 | let color = color?; 390 | let display = match args.cmd { 391 | true => host_or_cmd.to_string(), 392 | false => format!( 393 | "{} ({})", 394 | host_or_cmd, 395 | get_host_ipaddr(host_or_cmd, args.ipv4, args.ipv6)? 396 | ), 397 | }; 398 | data.push(PlotData::new( 399 | display, 400 | args.buffer, 401 | Style::default().fg(color), 402 | args.simple_graphics, 403 | )); 404 | } 405 | 406 | #[cfg(not(target_os = "windows"))] 407 | let interface: Option = args.interface.clone(); 408 | #[cfg(target_os = "windows")] 409 | let interface: Option = None; 410 | 411 | #[cfg(not(target_os = "windows"))] 412 | let ping_args: Option> = args.ping_args.clone(); 413 | #[cfg(target_os = "windows")] 414 | let ping_args: Option> = None; 415 | 416 | let (key_tx, rx) = mpsc::channel(); 417 | 418 | let mut threads = vec![]; 419 | 420 | let killed = Arc::new(AtomicBool::new(false)); 421 | 422 | for (host_id, host_or_cmd) in hosts_or_commands.iter().cloned().enumerate() { 423 | if args.cmd { 424 | let cmd_thread = start_cmd_thread( 425 | &host_or_cmd, 426 | host_id, 427 | args.watch_interval, 428 | key_tx.clone(), 429 | std::sync::Arc::clone(&killed), 430 | ); 431 | threads.push(cmd_thread); 432 | } else { 433 | let interval = 434 | Duration::from_millis((args.watch_interval.unwrap_or(0.2) * 1000.0) as u64); 435 | 436 | let mut ping_opts = if args.ipv4 { 437 | PingOptions::new_ipv4(host_or_cmd, interval, interface.clone()) 438 | } else if args.ipv6 { 439 | PingOptions::new_ipv6(host_or_cmd, interval, interface.clone()) 440 | } else { 441 | PingOptions::new(host_or_cmd, interval, interface.clone()) 442 | }; 443 | if let Some(ping_args) = &ping_args { 444 | ping_opts = ping_opts.with_raw_arguments(ping_args.clone()); 445 | } 446 | 447 | threads.push(start_ping_thread( 448 | ping_opts, 449 | host_id, 450 | key_tx.clone(), 451 | std::sync::Arc::clone(&killed), 452 | )?); 453 | } 454 | } 455 | threads.push(start_render_thread( 456 | std::sync::Arc::clone(&killed), 457 | key_tx.clone(), 458 | )); 459 | 460 | let mut app = App::new(data, args.buffer); 461 | enable_raw_mode()?; 462 | let stdout = io::stdout(); 463 | let mut backend = CrosstermBackend::new(BufWriter::with_capacity(1024 * 1024 * 4, stdout)); 464 | let rect = backend.size()?; 465 | 466 | if args.clear { 467 | execute!( 468 | backend, 469 | SetSize(rect.width, rect.height), 470 | EnterAlternateScreen, 471 | )?; 472 | } else { 473 | execute!(backend, SetSize(rect.width, rect.height),)?; 474 | } 475 | 476 | let mut terminal = Terminal::new(backend)?; 477 | terminal.clear()?; 478 | 479 | // Pump keyboard messages into the queue 480 | let killed_thread = std::sync::Arc::clone(&killed); 481 | thread::spawn(move || -> Result<()> { 482 | while !killed_thread.load(Ordering::Acquire) { 483 | if event::poll(Duration::from_secs(5))? { 484 | if let CEvent::Key(key) = event::read()? { 485 | match key.code { 486 | KeyCode::Char('q') | KeyCode::Esc => { 487 | key_tx.send(Event::Terminate)?; 488 | break; 489 | } 490 | KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { 491 | key_tx.send(Event::Terminate)?; 492 | break; 493 | } 494 | _ => {} 495 | } 496 | } 497 | } 498 | } 499 | Ok(()) 500 | }); 501 | 502 | loop { 503 | match rx.recv()? { 504 | Event::Update(host_id, update) => { 505 | match update { 506 | Update::Result(duration) => app.update(host_id, Some(duration)), 507 | Update::Timeout => app.update(host_id, None), 508 | Update::Unknown => (), 509 | Update::Terminated(e, _) if e.success() => { 510 | break; 511 | } 512 | Update::Terminated(e, stderr) => { 513 | eprintln!("There was an error running ping: {e}\nStderr: {stderr}\n"); 514 | break; 515 | } 516 | }; 517 | } 518 | Event::Render => { 519 | terminal.draw(|f| { 520 | let chunks = Layout::default() 521 | .flex(Flex::Legacy) 522 | .direction(Direction::Vertical) 523 | .vertical_margin(args.vertical_margin) 524 | .horizontal_margin(args.horizontal_margin) 525 | .constraints( 526 | iter::repeat(Constraint::Length(1)) 527 | .take(app.data.len()) 528 | .chain(iter::once(Constraint::Percentage(10))) 529 | .collect::>(), 530 | ) 531 | .split(f.area()); 532 | 533 | let total_chunks = chunks.len(); 534 | 535 | let header_chunks = &chunks[0..total_chunks - 1]; 536 | let chart_chunk = &chunks[total_chunks - 1]; 537 | 538 | for (plot_data, chunk) in app.data.iter().zip(header_chunks) { 539 | let header_layout = Layout::default() 540 | .direction(Direction::Horizontal) 541 | .constraints( 542 | [ 543 | Constraint::Percentage(30), 544 | Constraint::Percentage(10), 545 | Constraint::Percentage(10), 546 | Constraint::Percentage(10), 547 | Constraint::Percentage(10), 548 | Constraint::Percentage(10), 549 | Constraint::Percentage(10), 550 | Constraint::Percentage(10), 551 | ] 552 | .as_ref(), 553 | ) 554 | .split(*chunk); 555 | 556 | for (area, paragraph) in header_layout.iter().zip(plot_data.header_stats()) 557 | { 558 | f.render_widget(paragraph, *area); 559 | } 560 | } 561 | 562 | let datasets: Vec = app.data.iter().map(|d| d.into()).collect(); 563 | 564 | let y_axis_bounds = app.y_axis_bounds(); 565 | let x_axis_bounds = app.x_axis_bounds(); 566 | 567 | let chart = Chart::new(datasets) 568 | .block(Block::default().borders(Borders::NONE)) 569 | .x_axis( 570 | Axis::default() 571 | .style(Style::default().fg(Color::Gray)) 572 | .bounds(x_axis_bounds) 573 | .labels(app.x_axis_labels(x_axis_bounds)), 574 | ) 575 | .y_axis( 576 | Axis::default() 577 | .style(Style::default().fg(Color::Gray)) 578 | .bounds(y_axis_bounds) 579 | .labels(app.y_axis_labels(y_axis_bounds)), 580 | ); 581 | 582 | f.render_widget(chart, *chart_chunk) 583 | })?; 584 | } 585 | Event::Terminate => { 586 | killed.store(true, Ordering::Release); 587 | break; 588 | } 589 | } 590 | } 591 | killed.store(true, Ordering::Relaxed); 592 | 593 | disable_raw_mode()?; 594 | execute!(terminal.backend_mut())?; 595 | terminal.show_cursor()?; 596 | 597 | let new_size = terminal.size()?; 598 | terminal.set_cursor_position(Position { 599 | x: new_size.width, 600 | y: new_size.height, 601 | })?; 602 | for thread in threads { 603 | thread.join().unwrap()?; 604 | } 605 | 606 | if args.clear { 607 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 608 | }; 609 | 610 | Ok(()) 611 | } 612 | -------------------------------------------------------------------------------- /gping/src/plot_data.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use chrono::prelude::*; 3 | use core::option::Option; 4 | use core::option::Option::{None, Some}; 5 | use core::time::Duration; 6 | use itertools::Itertools; 7 | use tui::style::Style; 8 | use tui::symbols; 9 | use tui::widgets::{Dataset, GraphType, Paragraph}; 10 | 11 | pub struct PlotData { 12 | pub display: String, 13 | pub data: Vec<(f64, f64)>, 14 | pub style: Style, 15 | buffer: chrono::Duration, 16 | simple_graphics: bool, 17 | } 18 | 19 | impl PlotData { 20 | pub fn new(display: String, buffer: u64, style: Style, simple_graphics: bool) -> PlotData { 21 | PlotData { 22 | display, 23 | data: Vec::with_capacity(150), 24 | style, 25 | buffer: chrono::Duration::try_seconds(buffer as i64) 26 | .with_context(|| format!("Error converting {buffer} to seconds")) 27 | .unwrap(), 28 | simple_graphics, 29 | } 30 | } 31 | pub fn update(&mut self, item: Option) { 32 | let now = Local::now(); 33 | let idx = now.timestamp_millis() as f64 / 1_000f64; 34 | match item { 35 | Some(dur) => self.data.push((idx, dur.as_micros() as f64)), 36 | None => self.data.push((idx, f64::NAN)), 37 | } 38 | // Find the last index that we should remove. 39 | let earliest_timestamp = (now - self.buffer).timestamp_millis() as f64 / 1_000f64; 40 | let last_idx = self 41 | .data 42 | .iter() 43 | .enumerate() 44 | .filter(|(_, (timestamp, _))| *timestamp < earliest_timestamp) 45 | .map(|(idx, _)| idx) 46 | .last(); 47 | if let Some(idx) = last_idx { 48 | self.data.drain(0..idx).for_each(drop) 49 | } 50 | } 51 | 52 | pub fn header_stats(&self) -> Vec { 53 | let ping_header = Paragraph::new(self.display.clone()).style(self.style); 54 | let items: Vec<&f64> = self 55 | .data 56 | .iter() 57 | .filter(|(_, x)| !x.is_nan()) 58 | .map(|(_, v)| v) 59 | .sorted_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) 60 | .collect(); 61 | if items.is_empty() { 62 | return vec![ping_header]; 63 | } 64 | 65 | let min = **items.first().unwrap(); 66 | let max = **items.last().unwrap(); 67 | let avg = items.iter().copied().sum::() / items.len() as f64; 68 | let jtr = items 69 | .iter() 70 | .zip(items.iter().skip(1)) 71 | .map(|(&prev, &curr)| (curr - prev).abs()) 72 | .sum::() 73 | / (items.len() - 1) as f64; 74 | 75 | let percentile_position = 0.95 * items.len() as f32; 76 | let rounded_position = percentile_position.round() as usize; 77 | let p95 = items.get(rounded_position).map(|i| **i).unwrap_or(0f64); 78 | 79 | // count timeouts 80 | let to = self.data.iter().filter(|(_, x)| x.is_nan()).count(); 81 | 82 | let last = self.data.last().unwrap_or(&(0f64, 0f64)).1; 83 | 84 | vec![ 85 | ping_header, 86 | Paragraph::new(format!("last {:?}", Duration::from_micros(last as u64))) 87 | .style(self.style), 88 | Paragraph::new(format!("min {:?}", Duration::from_micros(min as u64))) 89 | .style(self.style), 90 | Paragraph::new(format!("max {:?}", Duration::from_micros(max as u64))) 91 | .style(self.style), 92 | Paragraph::new(format!("avg {:?}", Duration::from_micros(avg as u64))) 93 | .style(self.style), 94 | Paragraph::new(format!("jtr {:?}", Duration::from_micros(jtr as u64))) 95 | .style(self.style), 96 | Paragraph::new(format!("p95 {:?}", Duration::from_micros(p95 as u64))) 97 | .style(self.style), 98 | Paragraph::new(format!("t/o {to:?}")).style(self.style), 99 | ] 100 | } 101 | } 102 | 103 | impl<'a> From<&'a PlotData> for Dataset<'a> { 104 | fn from(plot: &'a PlotData) -> Self { 105 | let slice = plot.data.as_slice(); 106 | Dataset::default() 107 | .marker(if plot.simple_graphics { 108 | symbols::Marker::Dot 109 | } else { 110 | symbols::Marker::Braille 111 | }) 112 | .style(plot.style) 113 | .graph_type(GraphType::Line) 114 | .data(slice) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /gping/src/region_map.rs: -------------------------------------------------------------------------------- 1 | type Host = String; 2 | 3 | pub fn try_host_from_cloud_region(query: &str) -> Option { 4 | match query.split_once(':') { 5 | Some(("aws", region)) => Some(format!("ec2.{region}.amazonaws.com")), 6 | Some(("gcp", "")) => Some("cloud.google.com".to_string()), 7 | Some(("gcp", region)) => Some(format!("storage.{region}.rep.googleapis.com")), 8 | _ => None, 9 | } 10 | } 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | 16 | #[test] 17 | fn test_host_from_aws() { 18 | assert_eq!( 19 | try_host_from_cloud_region("aws:eu-west-1"), 20 | Some("ec2.eu-west-1.amazonaws.com".to_string()) 21 | ); 22 | } 23 | 24 | #[test] 25 | fn test_host_from_gcp() { 26 | assert_eq!( 27 | try_host_from_cloud_region("gcp:me-central2"), 28 | Some("storage.me-central2.rep.googleapis.com".to_string()) 29 | ); 30 | assert_eq!( 31 | try_host_from_cloud_region("gcp:"), 32 | Some("cloud.google.com".to_string()) 33 | ); 34 | } 35 | 36 | #[test] 37 | fn test_host_from_foo() { 38 | assert_eq!(try_host_from_cloud_region("foo:bar"), None); 39 | } 40 | 41 | #[test] 42 | fn test_invalid_input() { 43 | assert_eq!(try_host_from_cloud_region("foo"), None); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /images/readme-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orf/gping/688932a2374f104320960d4b1fcdb29084f9a486/images/readme-example.gif -------------------------------------------------------------------------------- /pinger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pinger" 3 | version = "2.0.0" 4 | authors = ["Tom Forbes "] 5 | edition = "2018" 6 | license = "MIT" 7 | description = "A small cross-platform library to execute the ping command and parse the output" 8 | repository = "https://github.com/orf/pinger/" 9 | 10 | [dependencies] 11 | thiserror = "2.0.11" 12 | lazy-regex = "3.4.1" 13 | rand = { version = "0.9.0", optional = true } 14 | 15 | [target.'cfg(windows)'.dependencies] 16 | winping = "0.10.1" 17 | 18 | [dev-dependencies] 19 | os_info = "3.9.2" 20 | ntest = "0.9.3" 21 | anyhow = "1.0.95" 22 | 23 | [features] 24 | default = [] 25 | fake-ping = ["rand"] 26 | -------------------------------------------------------------------------------- /pinger/README.md: -------------------------------------------------------------------------------- 1 | # pinger 2 | 3 | > A small cross-platform library to execute the ping command and parse the output. 4 | 5 | This crate is primarily built for use with `gping`, but it can also be used as a 6 | standalone library. 7 | 8 | This allows you to reliably ping hosts without having to worry about process permissions, 9 | in a cross-platform manner on Windows, Linux and macOS. 10 | 11 | ## Usage 12 | 13 | A full example of using the library can be found in the `examples/` directory, but the 14 | interface is quite simple: 15 | 16 | ```rust 17 | use std::time::Duration; 18 | use pinger::{ping, PingOptions}; 19 | 20 | fn ping_google() { 21 | let options = PingOptions::new("google.com", Duration::from_secs(1), None); 22 | let stream = ping(options).expect("Error pinging"); 23 | for message in stream { 24 | match message { 25 | pinger::PingResult::Pong(duration, _) => { 26 | println!("Duration: {:?}", duration) 27 | } 28 | _ => {} // Handle errors, log ping timeouts, etc. 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | ## Adding pinger to your project. 35 | 36 | `cargo add pinger` 37 | -------------------------------------------------------------------------------- /pinger/examples/simple-ping.rs: -------------------------------------------------------------------------------- 1 | use pinger::{ping, PingOptions}; 2 | 3 | const LIMIT: usize = 3; 4 | 5 | pub fn main() { 6 | let target = "tomforb.es".to_string(); 7 | let interval = std::time::Duration::from_millis(500); 8 | let options = PingOptions::new(target, interval, None); 9 | let stream = ping(options).expect("Error pinging"); 10 | for message in stream.into_iter().take(LIMIT) { 11 | match message { 12 | pinger::PingResult::Pong(duration, line) => { 13 | println!("Duration: {:?}\t\t(raw: {:?})", duration, line) 14 | } 15 | pinger::PingResult::Timeout(line) => println!("Timeout! (raw: {line:?})"), 16 | pinger::PingResult::Unknown(line) => println!("Unknown line: {:?}", line), 17 | pinger::PingResult::PingExited(code, stderr) => { 18 | panic!("Ping exited! Code: {:?}. Stderr: {:?}", code, stderr) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pinger/src/bsd.rs: -------------------------------------------------------------------------------- 1 | use crate::{extract_regex, PingCreationError, PingOptions, PingResult, Pinger}; 2 | use lazy_regex::*; 3 | 4 | pub static RE: Lazy = lazy_regex!(r"time=(?:(?P[0-9]+).(?P[0-9]+)\s+ms)"); 5 | 6 | pub struct BSDPinger { 7 | options: PingOptions, 8 | } 9 | 10 | pub(crate) fn parse_bsd(line: String) -> Option { 11 | if line.starts_with("PING ") { 12 | return None; 13 | } 14 | if line.starts_with("Request timeout") { 15 | return Some(PingResult::Timeout(line)); 16 | } 17 | extract_regex(&RE, line) 18 | } 19 | 20 | impl Pinger for BSDPinger { 21 | fn from_options(options: PingOptions) -> Result 22 | where 23 | Self: Sized, 24 | { 25 | Ok(Self { options }) 26 | } 27 | 28 | fn parse_fn(&self) -> fn(String) -> Option { 29 | parse_bsd 30 | } 31 | 32 | fn ping_args(&self) -> (&str, Vec) { 33 | let mut args = vec![format!( 34 | "-i{:.1}", 35 | self.options.interval.as_millis() as f32 / 1_000_f32 36 | )]; 37 | if let Some(interface) = &self.options.interface { 38 | args.push("-I".into()); 39 | args.push(interface.clone()); 40 | } 41 | if let Some(raw_args) = &self.options.raw_arguments { 42 | args.extend(raw_args.iter().cloned()); 43 | } 44 | args.push(self.options.target.to_string()); 45 | ("ping", args) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pinger/src/fake.rs: -------------------------------------------------------------------------------- 1 | use crate::{PingCreationError, PingOptions, PingResult, Pinger}; 2 | use rand::prelude::*; 3 | use rand::rng; 4 | use std::sync::mpsc; 5 | use std::sync::mpsc::Receiver; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | pub struct FakePinger { 10 | options: PingOptions, 11 | } 12 | 13 | impl Pinger for FakePinger { 14 | fn from_options(options: PingOptions) -> Result 15 | where 16 | Self: Sized, 17 | { 18 | Ok(Self { options }) 19 | } 20 | 21 | fn parse_fn(&self) -> fn(String) -> Option { 22 | unimplemented!("parse for FakeParser not implemented") 23 | } 24 | 25 | fn ping_args(&self) -> (&str, Vec) { 26 | unimplemented!("ping_args not implemented for FakePinger") 27 | } 28 | 29 | fn start(&self) -> Result, PingCreationError> { 30 | let (tx, rx) = mpsc::channel(); 31 | let sleep_time = self.options.interval; 32 | 33 | thread::spawn(move || { 34 | let mut random = rng(); 35 | loop { 36 | let fake_seconds = random.random_range(50..150); 37 | let ping_result = PingResult::Pong( 38 | Duration::from_millis(fake_seconds), 39 | format!("Fake ping line: {fake_seconds} ms"), 40 | ); 41 | if tx.send(ping_result).is_err() { 42 | break; 43 | } 44 | 45 | std::thread::sleep(sleep_time); 46 | } 47 | }); 48 | 49 | Ok(rx) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pinger/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use crate::linux::LinuxPinger; 3 | /// Pinger 4 | /// This crate exposes a simple function to ping remote hosts across different operating systems. 5 | /// Example: 6 | /// ```no_run 7 | /// use std::time::Duration; 8 | /// use pinger::{ping, PingResult, PingOptions}; 9 | /// let options = PingOptions::new("tomforb.es".to_string(), Duration::from_secs(1), None); 10 | /// let stream = ping(options).expect("Error pinging"); 11 | /// for message in stream { 12 | /// match message { 13 | /// PingResult::Pong(duration, line) => println!("{:?} (line: {})", duration, line), 14 | /// PingResult::Timeout(_) => println!("Timeout!"), 15 | /// PingResult::Unknown(line) => println!("Unknown line: {}", line), 16 | /// PingResult::PingExited(_code, _stderr) => {} 17 | /// } 18 | /// } 19 | /// ``` 20 | use lazy_regex::Regex; 21 | use std::ffi::OsStr; 22 | use std::fmt::{Debug, Formatter}; 23 | use std::io::{BufRead, BufReader}; 24 | use std::process::{Child, Command, ExitStatus, Stdio}; 25 | use std::sync::{mpsc, Arc}; 26 | use std::time::Duration; 27 | use std::{fmt, io, thread}; 28 | use target::Target; 29 | use thiserror::Error; 30 | 31 | pub mod linux; 32 | pub mod macos; 33 | #[cfg(windows)] 34 | pub mod windows; 35 | 36 | mod bsd; 37 | #[cfg(feature = "fake-ping")] 38 | mod fake; 39 | mod target; 40 | #[cfg(test)] 41 | mod test; 42 | 43 | #[derive(Debug, Clone)] 44 | pub struct PingOptions { 45 | pub target: Target, 46 | pub interval: Duration, 47 | pub interface: Option, 48 | pub raw_arguments: Option>, 49 | } 50 | 51 | impl PingOptions { 52 | pub fn with_raw_arguments(mut self, raw_arguments: Vec) -> Self { 53 | self.raw_arguments = Some( 54 | raw_arguments 55 | .into_iter() 56 | .map(|item| item.to_string()) 57 | .collect(), 58 | ); 59 | self 60 | } 61 | } 62 | 63 | impl PingOptions { 64 | pub fn from_target(target: Target, interval: Duration, interface: Option) -> Self { 65 | Self { 66 | target, 67 | interval, 68 | interface, 69 | raw_arguments: None, 70 | } 71 | } 72 | pub fn new(target: impl ToString, interval: Duration, interface: Option) -> Self { 73 | Self::from_target(Target::new_any(target), interval, interface) 74 | } 75 | 76 | pub fn new_ipv4(target: impl ToString, interval: Duration, interface: Option) -> Self { 77 | Self::from_target(Target::new_ipv4(target), interval, interface) 78 | } 79 | 80 | pub fn new_ipv6(target: impl ToString, interval: Duration, interface: Option) -> Self { 81 | Self::from_target(Target::new_ipv6(target), interval, interface) 82 | } 83 | } 84 | 85 | pub fn run_ping( 86 | cmd: impl AsRef + Debug, 87 | args: Vec + Debug>, 88 | ) -> Result { 89 | Ok(Command::new(cmd.as_ref()) 90 | .args(&args) 91 | .stdout(Stdio::piped()) 92 | .stderr(Stdio::piped()) 93 | // Required to ensure that the output is formatted in the way we expect, not 94 | // using locale specific delimiters. 95 | .env("LANG", "C") 96 | .env("LC_ALL", "C") 97 | .spawn()?) 98 | } 99 | 100 | pub(crate) fn extract_regex(regex: &Regex, line: String) -> Option { 101 | let cap = regex.captures(&line)?; 102 | let ms = cap 103 | .name("ms") 104 | .expect("No capture group named 'ms'") 105 | .as_str() 106 | .parse::() 107 | .ok()?; 108 | let ns = match cap.name("ns") { 109 | None => 0, 110 | Some(cap) => { 111 | let matched_str = cap.as_str(); 112 | let number_of_digits = matched_str.len() as u32; 113 | let fractional_ms = matched_str.parse::().ok()?; 114 | fractional_ms * (10u64.pow(6 - number_of_digits)) 115 | } 116 | }; 117 | let duration = Duration::from_millis(ms) + Duration::from_nanos(ns); 118 | Some(PingResult::Pong(duration, line)) 119 | } 120 | 121 | pub trait Pinger: Send + Sync { 122 | fn from_options(options: PingOptions) -> std::result::Result 123 | where 124 | Self: Sized; 125 | 126 | fn parse_fn(&self) -> fn(String) -> Option; 127 | 128 | fn ping_args(&self) -> (&str, Vec); 129 | 130 | fn start(&self) -> Result, PingCreationError> { 131 | let (tx, rx) = mpsc::channel(); 132 | let (cmd, args) = self.ping_args(); 133 | 134 | let mut child = run_ping(cmd, args)?; 135 | let stdout = child.stdout.take().expect("child did not have a stdout"); 136 | 137 | let parse_fn = self.parse_fn(); 138 | 139 | thread::spawn(move || { 140 | let reader = BufReader::new(stdout).lines(); 141 | for line in reader { 142 | match line { 143 | Ok(msg) => { 144 | if let Some(result) = parse_fn(msg) { 145 | if tx.send(result).is_err() { 146 | break; 147 | } 148 | } 149 | } 150 | Err(_) => break, 151 | } 152 | } 153 | let result = child.wait_with_output().expect("Child wasn't started?"); 154 | let decoded_stderr = String::from_utf8(result.stderr).expect("Error decoding stderr"); 155 | let _ = tx.send(PingResult::PingExited(result.status, decoded_stderr)); 156 | }); 157 | 158 | Ok(rx) 159 | } 160 | } 161 | 162 | #[derive(Debug)] 163 | pub enum PingResult { 164 | Pong(Duration, String), 165 | Timeout(String), 166 | Unknown(String), 167 | PingExited(ExitStatus, String), 168 | } 169 | 170 | impl fmt::Display for PingResult { 171 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 172 | match &self { 173 | PingResult::Pong(duration, _) => write!(f, "{duration:?}"), 174 | PingResult::Timeout(_) => write!(f, "Timeout"), 175 | PingResult::Unknown(_) => write!(f, "Unknown"), 176 | PingResult::PingExited(status, stderr) => write!(f, "Exited({status}, {stderr})"), 177 | } 178 | } 179 | } 180 | 181 | #[derive(Error, Debug)] 182 | pub enum PingCreationError { 183 | #[error("Could not detect ping. Stderr: {stderr:?}\nStdout: {stdout:?}")] 184 | UnknownPing { 185 | stderr: Vec, 186 | stdout: Vec, 187 | }, 188 | #[error("Error spawning ping: {0}")] 189 | SpawnError(#[from] io::Error), 190 | 191 | #[error("Installed ping is not supported: {alternative}")] 192 | NotSupported { alternative: String }, 193 | 194 | #[error("Invalid or unresolvable hostname {0}")] 195 | HostnameError(String), 196 | } 197 | 198 | pub fn get_pinger(options: PingOptions) -> std::result::Result, PingCreationError> { 199 | #[cfg(feature = "fake-ping")] 200 | if std::env::var("PINGER_FAKE_PING") 201 | .map(|e| e == "1") 202 | .unwrap_or_default() 203 | { 204 | return Ok(Arc::new(fake::FakePinger::from_options(options)?)); 205 | } 206 | 207 | #[cfg(windows)] 208 | { 209 | return Ok(Arc::new(windows::WindowsPinger::from_options(options)?)); 210 | } 211 | #[cfg(unix)] 212 | { 213 | if cfg!(target_os = "freebsd") 214 | || cfg!(target_os = "dragonfly") 215 | || cfg!(target_os = "openbsd") 216 | || cfg!(target_os = "netbsd") 217 | { 218 | Ok(Arc::new(bsd::BSDPinger::from_options(options)?)) 219 | } else if cfg!(target_os = "macos") { 220 | Ok(Arc::new(macos::MacOSPinger::from_options(options)?)) 221 | } else { 222 | Ok(Arc::new(LinuxPinger::from_options(options)?)) 223 | } 224 | } 225 | } 226 | 227 | /// Start pinging a an address. The address can be either a hostname or an IP address. 228 | pub fn ping( 229 | options: PingOptions, 230 | ) -> std::result::Result, PingCreationError> { 231 | let pinger = get_pinger(options)?; 232 | pinger.start() 233 | } 234 | -------------------------------------------------------------------------------- /pinger/src/linux.rs: -------------------------------------------------------------------------------- 1 | use crate::{extract_regex, run_ping, PingCreationError, PingOptions, PingResult, Pinger}; 2 | use lazy_regex::*; 3 | 4 | pub static UBUNTU_RE: Lazy = lazy_regex!(r"(?i-u)time=(?P\d+)(?:\.(?P\d+))? *ms"); 5 | 6 | #[derive(Debug)] 7 | pub enum LinuxPinger { 8 | // Alpine 9 | BusyBox(PingOptions), 10 | // Debian, Ubuntu, etc 11 | IPTools(PingOptions), 12 | } 13 | 14 | impl LinuxPinger { 15 | pub fn detect_platform_ping(options: PingOptions) -> Result { 16 | let child = run_ping("ping", vec!["-V".to_string()])?; 17 | let output = child.wait_with_output()?; 18 | let stdout = String::from_utf8(output.stdout).expect("Error decoding ping stdout"); 19 | let stderr = String::from_utf8(output.stderr).expect("Error decoding ping stderr"); 20 | 21 | if stderr.contains("BusyBox") { 22 | Ok(LinuxPinger::BusyBox(options)) 23 | } else if stdout.contains("iputils") { 24 | Ok(LinuxPinger::IPTools(options)) 25 | } else if stdout.contains("inetutils") { 26 | Err(PingCreationError::NotSupported { 27 | alternative: "Please use iputils ping, not inetutils.".to_string(), 28 | }) 29 | } else { 30 | let first_two_lines_stderr: Vec = 31 | stderr.lines().take(2).map(str::to_string).collect(); 32 | let first_two_lines_stout: Vec = 33 | stdout.lines().take(2).map(str::to_string).collect(); 34 | Err(PingCreationError::UnknownPing { 35 | stdout: first_two_lines_stout, 36 | stderr: first_two_lines_stderr, 37 | }) 38 | } 39 | } 40 | } 41 | 42 | impl Pinger for LinuxPinger { 43 | fn from_options(options: PingOptions) -> Result 44 | where 45 | Self: Sized, 46 | { 47 | Self::detect_platform_ping(options) 48 | } 49 | 50 | fn parse_fn(&self) -> fn(String) -> Option { 51 | |line| { 52 | #[cfg(test)] 53 | eprintln!("Got line {line}"); 54 | if line.starts_with("64 bytes from") { 55 | return extract_regex(&UBUNTU_RE, line); 56 | } else if line.starts_with("no answer yet") { 57 | return Some(PingResult::Timeout(line)); 58 | } 59 | None 60 | } 61 | } 62 | 63 | fn ping_args(&self) -> (&str, Vec) { 64 | match self { 65 | // Alpine doesn't support timeout notifications, so we don't add the -O flag here. 66 | LinuxPinger::BusyBox(options) => { 67 | let cmd = if options.target.is_ipv6() { 68 | "ping6" 69 | } else { 70 | "ping" 71 | }; 72 | 73 | let mut args = vec![ 74 | options.target.to_string(), 75 | format!("-i{:.1}", options.interval.as_millis() as f32 / 1_000_f32), 76 | ]; 77 | 78 | if let Some(raw_args) = &options.raw_arguments { 79 | args.extend(raw_args.iter().cloned()); 80 | } 81 | 82 | (cmd, args) 83 | } 84 | LinuxPinger::IPTools(options) => { 85 | let cmd = if options.target.is_ipv6() { 86 | "ping6" 87 | } else { 88 | "ping" 89 | }; 90 | 91 | // The -O flag ensures we "no answer yet" messages from ping 92 | // See https://superuser.com/questions/270083/linux-ping-show-time-out 93 | let mut args = vec![ 94 | "-O".to_string(), 95 | format!("-i{:.1}", options.interval.as_millis() as f32 / 1_000_f32), 96 | ]; 97 | if let Some(interface) = &options.interface { 98 | args.push("-I".into()); 99 | args.push(interface.clone()); 100 | } 101 | if let Some(raw_args) = &options.raw_arguments { 102 | args.extend(raw_args.iter().cloned()); 103 | } 104 | 105 | args.push(options.target.to_string()); 106 | (cmd, args) 107 | } 108 | } 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | #[test] 115 | #[cfg(target_os = "linux")] 116 | fn test_linux_detection() { 117 | use super::*; 118 | use os_info::Type; 119 | use std::time::Duration; 120 | 121 | let platform = LinuxPinger::detect_platform_ping(PingOptions::new( 122 | "foo.com".to_string(), 123 | Duration::from_secs(1), 124 | None, 125 | )) 126 | .unwrap(); 127 | match os_info::get().os_type() { 128 | Type::Alpine => { 129 | assert!(matches!(platform, LinuxPinger::BusyBox(_))) 130 | } 131 | Type::Ubuntu => { 132 | assert!(matches!(platform, LinuxPinger::IPTools(_))) 133 | } 134 | _ => {} 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /pinger/src/macos.rs: -------------------------------------------------------------------------------- 1 | use crate::bsd::parse_bsd; 2 | use crate::{PingCreationError, PingOptions, PingResult, Pinger}; 3 | use lazy_regex::*; 4 | 5 | pub static RE: Lazy = lazy_regex!(r"time=(?:(?P[0-9]+).(?P[0-9]+)\s+ms)"); 6 | 7 | pub struct MacOSPinger { 8 | options: PingOptions, 9 | } 10 | 11 | impl Pinger for MacOSPinger { 12 | fn from_options(options: PingOptions) -> Result 13 | where 14 | Self: Sized, 15 | { 16 | Ok(Self { options }) 17 | } 18 | 19 | fn parse_fn(&self) -> fn(String) -> Option { 20 | parse_bsd 21 | } 22 | 23 | fn ping_args(&self) -> (&str, Vec) { 24 | let cmd = if self.options.target.is_ipv6() { 25 | "ping6" 26 | } else { 27 | "ping" 28 | }; 29 | let mut args = vec![ 30 | format!( 31 | "-i{:.1}", 32 | self.options.interval.as_millis() as f32 / 1_000_f32 33 | ), 34 | self.options.target.to_string(), 35 | ]; 36 | if let Some(interface) = &self.options.interface { 37 | args.push("-b".into()); 38 | args.push(interface.clone()); 39 | } 40 | 41 | if let Some(raw_args) = &self.options.raw_arguments { 42 | args.extend(raw_args.iter().cloned()); 43 | } 44 | 45 | (cmd, args) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pinger/src/target.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::{Display, Formatter}; 3 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 4 | 5 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 6 | pub enum IPVersion { 7 | V4, 8 | V6, 9 | Any, 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub enum Target { 14 | IP(IpAddr), 15 | Hostname { domain: String, version: IPVersion }, 16 | } 17 | 18 | impl Target { 19 | pub fn is_ipv6(&self) -> bool { 20 | match self { 21 | Target::IP(ip) => ip.is_ipv6(), 22 | Target::Hostname { version, .. } => *version == IPVersion::V6, 23 | } 24 | } 25 | 26 | pub fn new_any(value: impl ToString) -> Self { 27 | let value = value.to_string(); 28 | if let Ok(ip) = value.parse::() { 29 | return Self::IP(ip); 30 | } 31 | Self::Hostname { 32 | domain: value, 33 | version: IPVersion::Any, 34 | } 35 | } 36 | 37 | pub fn new_ipv4(value: impl ToString) -> Self { 38 | let value = value.to_string(); 39 | if let Ok(ip) = value.parse::() { 40 | return Self::IP(IpAddr::V4(ip)); 41 | } 42 | Self::Hostname { 43 | domain: value.to_string(), 44 | version: IPVersion::V4, 45 | } 46 | } 47 | 48 | pub fn new_ipv6(value: impl ToString) -> Self { 49 | let value = value.to_string(); 50 | if let Ok(ip) = value.parse::() { 51 | return Self::IP(IpAddr::V6(ip)); 52 | } 53 | Self::Hostname { 54 | domain: value.to_string(), 55 | version: IPVersion::V6, 56 | } 57 | } 58 | } 59 | 60 | impl Display for Target { 61 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 62 | match self { 63 | Target::IP(v) => Display::fmt(&v, f), 64 | Target::Hostname { domain, .. } => Display::fmt(&domain, f), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pinger/src/test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use crate::bsd::BSDPinger; 4 | use crate::linux::LinuxPinger; 5 | use crate::macos::MacOSPinger; 6 | #[cfg(windows)] 7 | use crate::windows::WindowsPinger; 8 | use crate::{PingOptions, PingResult, Pinger}; 9 | use anyhow::bail; 10 | use ntest::timeout; 11 | use std::time::Duration; 12 | 13 | const IS_GHA: bool = option_env!("GITHUB_ACTIONS").is_some(); 14 | 15 | #[test] 16 | #[timeout(20_000)] 17 | fn test_integration_any() { 18 | run_integration_test(PingOptions::new( 19 | "tomforb.es", 20 | Duration::from_millis(500), 21 | None, 22 | )) 23 | .unwrap(); 24 | } 25 | #[test] 26 | #[timeout(20_000)] 27 | fn test_integration_ipv4() { 28 | run_integration_test(PingOptions::new_ipv4( 29 | "tomforb.es", 30 | Duration::from_millis(500), 31 | None, 32 | )) 33 | .unwrap(); 34 | } 35 | #[test] 36 | #[timeout(20_000)] 37 | fn test_integration_ip6() { 38 | let res = run_integration_test(PingOptions::new_ipv6( 39 | "tomforb.es", 40 | Duration::from_millis(500), 41 | None, 42 | )); 43 | // ipv6 tests are allowed to fail on Gitlab CI, as it doesn't support ipv6, apparently. 44 | if !IS_GHA { 45 | res.unwrap(); 46 | } 47 | } 48 | 49 | fn run_integration_test(options: PingOptions) -> anyhow::Result<()> { 50 | let stream = crate::ping(options.clone())?; 51 | 52 | let mut success = 0; 53 | let mut errors = 0; 54 | 55 | for message in stream.into_iter().take(3) { 56 | match message { 57 | PingResult::Pong(_, m) | PingResult::Timeout(m) => { 58 | eprintln!("Message: {}", m); 59 | success += 1; 60 | } 61 | PingResult::Unknown(line) => { 62 | eprintln!("Unknown line: {}", line); 63 | errors += 1; 64 | } 65 | PingResult::PingExited(code, stderr) => { 66 | bail!("Ping exited with code: {}, stderr: {}", code, stderr); 67 | } 68 | } 69 | } 70 | assert_eq!(success, 3, "Success != 3 with opts {options:?}"); 71 | assert_eq!(errors, 0, "Errors != 0 with opts {options:?}"); 72 | Ok(()) 73 | } 74 | 75 | fn opts() -> PingOptions { 76 | PingOptions::new("foo".to_string(), Duration::from_secs(1), None) 77 | } 78 | 79 | fn test_parser(contents: &str) { 80 | let pinger = T::from_options(opts()).unwrap(); 81 | run_parser_test(contents, &pinger); 82 | } 83 | 84 | fn run_parser_test(contents: &str, pinger: &impl Pinger) { 85 | let parser = pinger.parse_fn(); 86 | let test_file: Vec<&str> = contents.split("-----").collect(); 87 | let input = test_file[0].trim().split('\n'); 88 | let expected: Vec<&str> = test_file[1].trim().split('\n').collect(); 89 | let parsed: Vec> = input.map(|l| parser(l.to_string())).collect(); 90 | 91 | assert_eq!( 92 | parsed.len(), 93 | expected.len(), 94 | "Parsed: {:?}, Expected: {:?}", 95 | &parsed, 96 | &expected 97 | ); 98 | 99 | for (idx, (output, expected)) in parsed.into_iter().zip(expected).enumerate() { 100 | if let Some(value) = output { 101 | assert_eq!( 102 | format!("{value}").trim(), 103 | expected.trim(), 104 | "Failed at idx {idx}" 105 | ) 106 | } else { 107 | assert_eq!("None", expected.trim(), "Failed at idx {idx}") 108 | } 109 | } 110 | } 111 | 112 | #[test] 113 | fn macos() { 114 | test_parser::(include_str!("tests/macos.txt")); 115 | } 116 | 117 | #[test] 118 | fn freebsd() { 119 | test_parser::(include_str!("tests/bsd.txt")); 120 | } 121 | 122 | #[test] 123 | fn dragonfly() { 124 | test_parser::(include_str!("tests/bsd.txt")); 125 | } 126 | 127 | #[test] 128 | fn openbsd() { 129 | test_parser::(include_str!("tests/bsd.txt")); 130 | } 131 | 132 | #[test] 133 | fn netbsd() { 134 | test_parser::(include_str!("tests/bsd.txt")); 135 | } 136 | 137 | #[test] 138 | fn ubuntu() { 139 | run_parser_test( 140 | include_str!("tests/ubuntu.txt"), 141 | &LinuxPinger::IPTools(opts()), 142 | ); 143 | } 144 | 145 | #[test] 146 | fn debian() { 147 | run_parser_test( 148 | include_str!("tests/debian.txt"), 149 | &LinuxPinger::IPTools(opts()), 150 | ); 151 | } 152 | 153 | #[cfg(windows)] 154 | #[test] 155 | fn windows() { 156 | test_parser::(include_str!("tests/windows.txt")); 157 | } 158 | 159 | #[test] 160 | fn android() { 161 | run_parser_test( 162 | include_str!("tests/android.txt"), 163 | &LinuxPinger::BusyBox(opts()), 164 | ); 165 | } 166 | 167 | #[test] 168 | fn alpine() { 169 | run_parser_test( 170 | include_str!("tests/alpine.txt"), 171 | &LinuxPinger::BusyBox(opts()), 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /pinger/src/tests/alpine.txt: -------------------------------------------------------------------------------- 1 | PING google.com (142.250.178.14): 56 data bytes 2 | 64 bytes from 142.250.178.14: seq=0 ttl=37 time=19.236 ms 3 | 64 bytes from 142.250.178.14: seq=1 ttl=37 time=19.319 ms 4 | 64 bytes from 142.250.178.14: seq=2 ttl=37 time=17.944 ms 5 | ping: sendto: Network unreachable 6 | ----- 7 | 8 | None 9 | 19.236ms 10 | 19.319ms 11 | 17.944ms 12 | None 13 | -------------------------------------------------------------------------------- /pinger/src/tests/android.txt: -------------------------------------------------------------------------------- 1 | PING google.com (172.217.173.46) 56(84) bytes of data. 2 | 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=1 ttl=110 time=106 ms 3 | 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=2 ttl=110 time=142 ms 4 | 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=3 ttl=110 time=244 ms 5 | 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=4 ttl=110 time=120 ms 6 | 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=5 ttl=110 time=122 ms 7 | 64 bytes from 172.217.173.46: icmp_seq=6 ttl=110 time=246 ms 8 | 9 | --- google.com ping statistics --- 10 | 6 packets transmitted, 6 received, 0% packet loss, time 5018ms 11 | rtt min/avg/max/mdev = 106.252/163.821/246.851/58.823 ms 12 | 13 | ----- 14 | 15 | None 16 | 106ms 17 | 142ms 18 | 244ms 19 | 120ms 20 | 122ms 21 | 246ms 22 | None 23 | None 24 | None 25 | None 26 | -------------------------------------------------------------------------------- /pinger/src/tests/bsd.txt: -------------------------------------------------------------------------------- 1 | PING google.com (216.58.198.174): 56 data bytes 2 | 64 bytes from 96.47.72.84: icmp_seq=0 ttl=50 time=111.525 ms 3 | ping: sendto: Host is down 4 | 64 bytes from 96.47.72.84: icmp_seq=1 ttl=50 time=110.395 ms 5 | ping: sendto: No route to host 6 | 7 | ----- 8 | 9 | None 10 | 111.525ms 11 | None 12 | 110.395ms 13 | None 14 | -------------------------------------------------------------------------------- /pinger/src/tests/debian.txt: -------------------------------------------------------------------------------- 1 | PING google.com (216.58.209.78): 56 data bytes 2 | 64 bytes from 216.58.209.78: icmp_seq=0 ttl=37 time=21.308 ms 3 | 64 bytes from 216.58.209.78: icmp_seq=1 ttl=37 time=15.769 ms 4 | ^C--- google.com ping statistics --- 5 | 8 packets transmitted, 8 packets received, 0% packet loss 6 | round-trip min/avg/max/stddev = 15.282/20.347/41.775/8.344 ms 7 | 8 | ----- 9 | 10 | None 11 | 21.308ms 12 | 15.769ms 13 | None 14 | None 15 | None 16 | -------------------------------------------------------------------------------- /pinger/src/tests/macos.txt: -------------------------------------------------------------------------------- 1 | PING google.com (216.58.209.78): 56 data bytes 2 | 64 bytes from 216.58.209.78: icmp_seq=0 ttl=119 time=14.621 ms 3 | 64 bytes from 216.58.209.78: icmp_seq=1 ttl=119 time=33.898 ms 4 | 64 bytes from 216.58.209.78: icmp_seq=2 ttl=119 time=17.305 ms 5 | 64 bytes from 216.58.209.78: icmp_seq=3 ttl=119 time=24.235 ms 6 | 64 bytes from 216.58.209.78: icmp_seq=4 ttl=119 time=15.242 ms 7 | 64 bytes from 216.58.209.78: icmp_seq=5 ttl=119 time=16.639 ms 8 | Request timeout for icmp_seq 19 9 | Request timeout for icmp_seq 20 10 | Request timeout for icmp_seq 21 11 | 64 bytes from 216.58.209.78: icmp_seq=30 ttl=119 time=16.943 ms 12 | 13 | ----- 14 | 15 | None 16 | 14.621ms 17 | 33.898ms 18 | 17.305ms 19 | 24.235ms 20 | 15.242ms 21 | 16.639ms 22 | Timeout 23 | Timeout 24 | Timeout 25 | 16.943ms 26 | -------------------------------------------------------------------------------- /pinger/src/tests/ubuntu.txt: -------------------------------------------------------------------------------- 1 | PING google.com (216.58.209.78) 56(84) bytes of data. 2 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=1 ttl=37 time=25.1 ms 3 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=2 ttl=37 time=19.4 ms 4 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=3 ttl=37 time=14.9 ms 5 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=4 ttl=37 time=22.8 ms 6 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=5 ttl=37 time=13.9 ms 7 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=6 ttl=37 time=77.6 ms 8 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=7 ttl=37 time=158 ms 9 | no answer yet for icmp_seq=8 10 | no answer yet for icmp_seq=9 11 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=18 ttl=37 time=357 ms 12 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=19 ttl=37 time=85.2 ms 13 | 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=20 ttl=37 time=17.8 ms 14 | 15 | ----- 16 | 17 | None 18 | 25.1ms 19 | 19.4ms 20 | 14.9ms 21 | 22.8ms 22 | 13.9ms 23 | 77.6ms 24 | 158ms 25 | Timeout 26 | Timeout 27 | 357ms 28 | 85.2ms 29 | 17.8ms 30 | -------------------------------------------------------------------------------- /pinger/src/tests/windows.txt: -------------------------------------------------------------------------------- 1 | pinging example.microsoft.com [192.168.239.132] with 32 bytes of data: 2 | Reply from 192.168.239.132: bytes=32 time=101ms TTL=124 3 | Reply from 192.168.239.132: bytes=32 time=100ms TTL=124 4 | Reply from 192.168.239.132: bytes=32 time=120ms TTL=124 5 | Reply from 192.168.239.132: bytes=32 time=120ms TTL=124 6 | Request timed out. 7 | Request timed out. 8 | Reply from 192.168.239.132: bytes=32 time=120ms TTL=124 9 | 10 | ----- 11 | 12 | None 13 | 101ms 14 | 100ms 15 | 120ms 16 | 120ms 17 | Timeout 18 | Timeout 19 | 120ms 20 | -------------------------------------------------------------------------------- /pinger/src/windows.rs: -------------------------------------------------------------------------------- 1 | use crate::target::{IPVersion, Target}; 2 | use crate::PingCreationError; 3 | use crate::{extract_regex, PingOptions, PingResult, Pinger}; 4 | use lazy_regex::*; 5 | use std::net::{IpAddr, ToSocketAddrs}; 6 | use std::sync::mpsc; 7 | use std::thread; 8 | use std::time::Duration; 9 | use winping::{Buffer, Pinger as WinPinger}; 10 | 11 | pub static RE: Lazy = lazy_regex!(r"(?ix-u)time=(?P\d+)(?:\.(?P\d+))?"); 12 | 13 | pub struct WindowsPinger { 14 | options: PingOptions, 15 | } 16 | 17 | impl Pinger for WindowsPinger { 18 | fn from_options(options: PingOptions) -> Result { 19 | Ok(Self { options }) 20 | } 21 | 22 | fn parse_fn(&self) -> fn(String) -> Option { 23 | |line| { 24 | if line.contains("timed out") || line.contains("failure") { 25 | return Some(PingResult::Timeout(line)); 26 | } 27 | extract_regex(&RE, line) 28 | } 29 | } 30 | 31 | fn ping_args(&self) -> (&str, Vec) { 32 | unimplemented!("ping_args for WindowsPinger is not implemented") 33 | } 34 | 35 | fn start(&self) -> Result, PingCreationError> { 36 | let interval = self.options.interval; 37 | let parsed_ip = match &self.options.target { 38 | Target::IP(ip) => ip.clone(), 39 | Target::Hostname { domain, version } => { 40 | let ips = (domain.as_str(), 0).to_socket_addrs()?; 41 | let selected_ips: Vec<_> = if *version == IPVersion::Any { 42 | ips.collect() 43 | } else { 44 | ips.into_iter() 45 | .filter(|addr| { 46 | if *version == IPVersion::V6 { 47 | matches!(addr.ip(), IpAddr::V6(_)) 48 | } else { 49 | matches!(addr.ip(), IpAddr::V4(_)) 50 | } 51 | }) 52 | .collect() 53 | }; 54 | if selected_ips.is_empty() { 55 | return Err(PingCreationError::HostnameError(domain.clone()).into()); 56 | } 57 | selected_ips[0].ip() 58 | } 59 | }; 60 | 61 | let (tx, rx) = mpsc::channel(); 62 | 63 | thread::spawn(move || { 64 | let pinger = WinPinger::new().expect("Failed to create a WinPinger instance"); 65 | let mut buffer = Buffer::new(); 66 | loop { 67 | match pinger.send(parsed_ip.clone(), &mut buffer) { 68 | Ok(rtt) => { 69 | if tx 70 | .send(PingResult::Pong( 71 | Duration::from_millis(rtt as u64), 72 | "".to_string(), 73 | )) 74 | .is_err() 75 | { 76 | break; 77 | } 78 | } 79 | Err(_) => { 80 | // Fuck it. All errors are timeouts. Why not. 81 | if tx.send(PingResult::Timeout("".to_string())).is_err() { 82 | break; 83 | } 84 | } 85 | } 86 | thread::sleep(interval); 87 | } 88 | }); 89 | 90 | Ok(rx) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gping 🚀 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/gping.svg)](https://crates.io/crates/gping) 4 | [![Actions Status](https://github.com/orf/gping/workflows/CI/badge.svg)](https://github.com/orf/gping/actions) 5 | 6 | Ping, but with a graph. 7 | 8 | ![](./images/readme-example.gif) 9 | 10 | Comes with the following super-powers: 11 | * Graph the ping time for multiple hosts 12 | * Graph the _execution time_ for commands via the `--cmd` flag 13 | * Custom colours 14 | * Windows, Mac and Linux support 15 | 16 | Table of Contents 17 | ================= 18 | 19 | * [Install :cd:](#install-cd) 20 | * [Usage :saxophone:](#usage-saxophone) 21 | 22 | 23 | Packaging status 24 | 25 | 26 | # Install :cd: 27 | 28 | * macOS 29 | * [Homebrew](https://formulae.brew.sh/formula/gping#default): `brew install gping` 30 | * [MacPorts](https://ports.macports.org/port/gping/): `sudo port install gping` 31 | * Linux (Homebrew): `brew install gping` 32 | * CentOS (and other distributions with an old glibc): Download the MUSL build from the latest release 33 | * Windows/ARM: 34 | * Scoop: `scoop install gping` 35 | * Chocolatey: `choco install gping` 36 | * Download the latest release from [the github releases page](https://github.com/orf/gping/releases) 37 | * Fedora ([COPR](https://copr.fedorainfracloud.org/coprs/atim/gping/)): `sudo dnf copr enable atim/gping -y && sudo dnf install gping` 38 | * Cargo (**This requires `rustc` version 1.67.0 or greater**): `cargo install gping` 39 | * Arch Linux: `pacman -S gping` 40 | * Alpine linux: `apk add gping` 41 | * Ubuntu >23.10/Debian >13: `apt install gping` 42 | * Ubuntu/Debian ([Azlux's repo](https://packages.azlux.fr/)): 43 | ```bash 44 | echo 'deb [signed-by=/usr/share/keyrings/azlux.gpg] https://packages.azlux.fr/debian/ bookworm main' | sudo tee /etc/apt/sources.list.d/azlux.list 45 | sudo apt install gpg 46 | curl -s https://azlux.fr/repo.gpg.key | gpg --dearmor | sudo tee /usr/share/keyrings/azlux.gpg > /dev/null 47 | sudo apt update 48 | sudo apt install gping 49 | ``` 50 | * Gentoo ([dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)): 51 | ```sh 52 | sudo eselect repository enable dm9pZCAq 53 | sudo emerge --sync dm9pZCAq 54 | sudo emerge net-misc/gping::dm9pZCAq 55 | ``` 56 | * FreeBSD: 57 | * [pkg](https://www.freshports.org/net-mgmt/gping/): `pkg install gping` 58 | * [ports](https://cgit.freebsd.org/ports/tree/net-mgmt/gping) `cd /usr/ports/net-mgmt/gping; make install clean` 59 | * Docker: 60 | ```sh 61 | # Check all options 62 | docker run --rm -ti --network host ghcr.io/orf/gping:gping-v1.15.1 --help 63 | # Ping google.com 64 | docker run --rm -ti --network host ghcr.io/orf/gping:gping-v1.15.1 google.com 65 | ``` 66 | * Flox: 67 | ```sh 68 | # Inside of a Flox environment 69 | flox install gping 70 | ``` 71 | 72 | # Usage :saxophone: 73 | 74 | Just run `gping [host]`. `host` can be a command like `curl google.com` if the `--cmd` flag is used. You can also use 75 | shorthands like `aws:eu-west-1` or `aws:ca-central-1` to ping specific cloud regions. Only `aws` is currently supported. 76 | 77 | ```bash 78 | $ gping --help 79 | Ping, but with a graph. 80 | 81 | Usage: gping [OPTIONS] [HOSTS_OR_COMMANDS]... 82 | 83 | Arguments: 84 | [HOSTS_OR_COMMANDS]... Hosts or IPs to ping, or commands to run if --cmd is provided. Can use cloud shorthands like aws:eu-west-1. 85 | 86 | Options: 87 | --cmd 88 | Graph the execution time for a list of commands rather than pinging hosts 89 | -n, --watch-interval 90 | Watch interval seconds (provide partial seconds like '0.5'). Default for ping is 0.2, default for cmd is 0.5. 91 | -b, --buffer 92 | Determines the number of seconds to display in the graph. [default: 30] 93 | -4 94 | Resolve ping targets to IPv4 address 95 | -6 96 | Resolve ping targets to IPv6 address 97 | -i, --interface 98 | Interface to use when pinging 99 | -s, --simple-graphics 100 | Uses dot characters instead of braille 101 | --vertical-margin 102 | Vertical margin around the graph (top and bottom) [default: 1] 103 | --horizontal-margin 104 | Horizontal margin around the graph (left and right) [default: 0] 105 | -c, --color 106 | Assign color to a graph entry. This option can be defined more than once as a comma separated string, and the order which the colors are provided will be matched against the hosts or commands passed to gping. Hexadecimal RGB color codes are accepted in the form of '#RRGGBB' or the following color names: 'black', 'red', 'green', 'yellow', 'blue', 'magenta','cyan', 'gray', 'dark-gray', 'light-red', 'light-green', 'light-yellow', 'light-blue', 'light-magenta', 'light-cyan', and 'white' 107 | -h, --help 108 | Print help information 109 | -V, --version 110 | Print version information 111 | --clear 112 | Clear the graph from the terminal after closing the program 113 | ``` 114 | --------------------------------------------------------------------------------