Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17d2885b41 |
No files matched your search
@@ -1,313 +0,0 @@
|
||||
|
||||
name: Build binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Measure before stripping; package stripped copies separately.
|
||||
CARGO_PROFILE_RELEASE_STRIP: none
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.target }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-musl
|
||||
- i686-musl
|
||||
- arm-musleabi
|
||||
- arm-musleabihf
|
||||
- armv7-musleabi
|
||||
- armv7-musleabihf
|
||||
- aarch64-musl
|
||||
- mips-musl
|
||||
- mipsel-musl
|
||||
- mips64-muslabi64
|
||||
- mips64el-muslabi64
|
||||
- riscv64gc-musl
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: builtin:checkout
|
||||
|
||||
- name: Configure Rust directories
|
||||
# Cross remote does not traverse hidden directories under /root.
|
||||
run: |
|
||||
echo "CARGO_HOME=$RUNNER_TEMP/cargo" >> "$GITHUB_ENV"
|
||||
echo "RUSTUP_HOME=$RUNNER_TEMP/rustup" >> "$GITHUB_ENV"
|
||||
echo "$RUNNER_TEMP/cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install standard Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
toolchain: ${{ startsWith(matrix.target, 'mips') && 'nightly' || 'stable' }}
|
||||
cache: false
|
||||
build-warnings: warn
|
||||
|
||||
- name: Install nightly toolchain and standard library sources
|
||||
run: |
|
||||
rustup toolchain install nightly \
|
||||
--profile minimal \
|
||||
--component rust-src
|
||||
|
||||
- name: Install cross and binary tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends binutils llvm
|
||||
cargo install --git https://github.com/cross-rs/cross --locked cross
|
||||
echo "RUST_TARGET=${TARGET%-*}-unknown-linux-${TARGET#*-}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Detect statistics feature
|
||||
run: |
|
||||
if grep -Eq '^[[:space:]]*stats[[:space:]]*=' crates/irtt-app/Cargo.toml; then
|
||||
echo "HAS_STATS_FEATURE=true" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "HAS_STATS_FEATURE=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Build, verify, package, and compare
|
||||
env:
|
||||
# MIPS build-std needs cross GCC's CRT objects.
|
||||
RUSTFLAGS: ${{ startsWith(matrix.target, 'mips') && '-C target-feature=+crt-static -C link-self-contained=no' || '-C target-feature=+crt-static' }}
|
||||
CROSS_REMOTE: "true"
|
||||
CROSS_REMOTE_COPY_FULL_TARGET_DIR: ${{ startsWith(matrix.target, 'mips') && 'true' || 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
declare -A raw_size
|
||||
declare -A stripped_size
|
||||
declare -A compressed_size
|
||||
|
||||
mkdir -p dist
|
||||
|
||||
# MIPS requires nightly build-std via Cross.toml.
|
||||
# Every other standard build uses stable.
|
||||
standard_toolchain=stable
|
||||
if [[ "$TARGET" == mips* ]]; then
|
||||
standard_toolchain=nightly
|
||||
fi
|
||||
|
||||
build_variant() {
|
||||
local variant="$1"
|
||||
local toolchain="$2"
|
||||
local features="$3"
|
||||
shift 3
|
||||
|
||||
local binary="target/${RUST_TARGET}/release/irtt-client"
|
||||
local directory="dist/${variant}"
|
||||
local artifact="dist/irtt-client-${TARGET}-${variant}.tar.gz"
|
||||
|
||||
echo "::group::Build ${variant} (${toolchain}, ${features})"
|
||||
|
||||
cross "+${toolchain}" build \
|
||||
--release \
|
||||
--locked \
|
||||
--target "$RUST_TARGET" \
|
||||
-p irtt-rs \
|
||||
--bin irtt-client \
|
||||
--no-default-features \
|
||||
--features "$features" \
|
||||
"$@"
|
||||
|
||||
# Reject dynamically linked executables.
|
||||
readelf --program-headers "$binary" > /tmp/program-headers.txt
|
||||
readelf --dynamic "$binary" > /tmp/dynamic-section.txt
|
||||
|
||||
if grep -q INTERP /tmp/program-headers.txt ||
|
||||
grep -q NEEDED /tmp/dynamic-section.txt; then
|
||||
echo "Binary is not statically linked: ${variant}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$directory"
|
||||
cp "$binary" "$directory/irtt-client"
|
||||
|
||||
raw_size[$variant]=$(stat -c %s "$binary")
|
||||
|
||||
llvm-strip --strip-all "$directory/irtt-client"
|
||||
stripped_size[$variant]=$(
|
||||
stat -c %s "$directory/irtt-client"
|
||||
)
|
||||
|
||||
# Each archive contains an executable named irtt-client.
|
||||
tar -czf "$artifact" -C "$directory" irtt-client
|
||||
compressed_size[$variant]=$(stat -c %s "$artifact")
|
||||
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
human() {
|
||||
numfmt --to=iec-i --suffix=B --format='%.1f' "$1"
|
||||
}
|
||||
|
||||
# Negative means smaller than the reference binary.
|
||||
delta() {
|
||||
awk -v base="$1" -v current="$2" 'BEGIN {
|
||||
if (base == 0) {
|
||||
printf "N/A"
|
||||
} else {
|
||||
printf "%+.1f%%", 100 * (current - base) / base
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
print_row() {
|
||||
local variant="$1"
|
||||
local label="$2"
|
||||
local baseline="$3"
|
||||
local comparison
|
||||
|
||||
if [[ "$variant" == "$baseline" ]]; then
|
||||
comparison="Baseline"
|
||||
else
|
||||
comparison=$(delta \
|
||||
"${compressed_size[$baseline]}" \
|
||||
"${compressed_size[$variant]}")
|
||||
fi
|
||||
|
||||
printf '| %s | %s | %s | %s | %s |\n' \
|
||||
"$label" \
|
||||
"$(human "${raw_size[$variant]}")" \
|
||||
"$(human "${stripped_size[$variant]}")" \
|
||||
"$(human "${compressed_size[$variant]}")" \
|
||||
"$comparison"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Standard builds: stable except MIPS.
|
||||
# ---------------------------------------------------------
|
||||
|
||||
full_features=client
|
||||
if [[ "$HAS_STATS_FEATURE" == "true" ]]; then
|
||||
full_features="client,stats"
|
||||
fi
|
||||
|
||||
build_variant full "$standard_toolchain" "$full_features"
|
||||
|
||||
standard_baseline=full
|
||||
if [[ "$HAS_STATS_FEATURE" == "true" ]]; then
|
||||
build_variant minimal "$standard_toolchain" client
|
||||
standard_baseline=minimal
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Nightly baseline: identical compiler for all experiments.
|
||||
#
|
||||
# MIPS already uses nightly for standard builds, so reuse
|
||||
# those binaries rather than compiling them twice.
|
||||
# ---------------------------------------------------------
|
||||
|
||||
if [[ "$standard_toolchain" == "nightly" ]]; then
|
||||
nightly_full=full
|
||||
nightly_baseline="$standard_baseline"
|
||||
else
|
||||
nightly_full=nightly-full
|
||||
build_variant nightly-full nightly "$full_features"
|
||||
|
||||
nightly_baseline=nightly-full
|
||||
if [[ "$HAS_STATS_FEATURE" == "true" ]]; then
|
||||
build_variant nightly-minimal nightly client
|
||||
nightly_baseline=nightly-minimal
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Nightly size experiments, always enabled.
|
||||
# ---------------------------------------------------------
|
||||
|
||||
# Rebuild std with the ordinary std feature set.
|
||||
build_variant rebuilt-std nightly client \
|
||||
-Z build-std
|
||||
|
||||
# Rebuild std without backtrace support.
|
||||
# Retain normal panic unwinding.
|
||||
build_variant no-backtrace nightly client \
|
||||
-Z build-std \
|
||||
-Z build-std-features=panic-unwind
|
||||
|
||||
# Aggressive option: terminate immediately on panic.
|
||||
build_variant immediate-abort nightly client \
|
||||
-Z panic-immediate-abort \
|
||||
-Z build-std \
|
||||
-Z build-std-features= \
|
||||
--config 'profile.release.panic="immediate-abort"'
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Job summary.
|
||||
# ---------------------------------------------------------
|
||||
|
||||
{
|
||||
echo "### irtt-client: ${TARGET}"
|
||||
echo
|
||||
|
||||
echo "#### Standard builds"
|
||||
echo
|
||||
echo "Toolchain: \`${standard_toolchain}\`"
|
||||
echo
|
||||
echo '| Variant | Unstripped | Stripped | Compressed | Compressed vs baseline |'
|
||||
echo '|:--|--:|--:|--:|--:|'
|
||||
|
||||
print_row full "Full" "$standard_baseline"
|
||||
|
||||
if [[ "$HAS_STATS_FEATURE" == "true" ]]; then
|
||||
print_row minimal "Minimal" "$standard_baseline"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "#### Nightly experiments"
|
||||
echo
|
||||
echo '| Variant | Unstripped | Stripped | Compressed | Compressed vs baseline |'
|
||||
echo '|:--|--:|--:|--:|--:|'
|
||||
|
||||
print_row "$nightly_full" "Nightly full" "$nightly_baseline"
|
||||
|
||||
if [[ "$HAS_STATS_FEATURE" == "true" ]]; then
|
||||
print_row "$nightly_baseline" "Nightly minimal" "$nightly_baseline"
|
||||
fi
|
||||
|
||||
print_row rebuilt-std "Rebuilt std" "$nightly_baseline"
|
||||
print_row no-backtrace "No backtrace" "$nightly_baseline"
|
||||
print_row immediate-abort "Immediate abort" "$nightly_baseline"
|
||||
|
||||
if [[ "$standard_toolchain" == "stable" ]]; then
|
||||
echo
|
||||
echo "Nightly vs stable baseline (compressed): $(delta \
|
||||
"${compressed_size[$standard_baseline]}" \
|
||||
"${compressed_size[$nightly_baseline]}")"
|
||||
else
|
||||
echo
|
||||
echo "MIPS: standard and nightly baselines are identical."
|
||||
echo "Cross.toml already enables build-std for these targets."
|
||||
fi
|
||||
|
||||
if [[ "$HAS_STATS_FEATURE" != "true" ]]; then
|
||||
echo
|
||||
echo "Statistics feature not detected; client includes statistics."
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload binaries
|
||||
# Gitea's v3 artifact protocol.
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: irtt-client-${{ matrix.target }}
|
||||
path: dist/irtt-client-${{ matrix.target }}-*.tar.gz
|
||||
if-no-files-found: error
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
if: matrix.musl
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends musl-tools
|
||||
sudo apt-get install --no-install-recommends musl-tools
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
registry: ${{ env.GITEA_REGISTRY }}
|
||||
# The automatic per-job token comes back unauthorized for package
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
registry: ${{ env.GHCR_REGISTRY }}
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
|
||||
@@ -34,35 +34,12 @@ jobs:
|
||||
with:
|
||||
cache: false
|
||||
|
||||
- name: Install Go toolchain
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
||||
with:
|
||||
go-version: '1.27.3'
|
||||
cache: false
|
||||
|
||||
- name: Install netem dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends iproute2 python3 util-linux
|
||||
|
||||
- name: Check host tools and network administration access
|
||||
run: |
|
||||
ip -V
|
||||
tc -V
|
||||
nsenter --version
|
||||
go version
|
||||
# Exercise the namespace, veth, and qdisc operations the harness needs.
|
||||
# sudo/NET_ADMIN alone do not establish mount/namespace permissions.
|
||||
sudo -n bash -euo pipefail <<'SH'
|
||||
probe_ns="irtt-preflight-$$"
|
||||
ip netns add "$probe_ns"
|
||||
trap 'ip netns delete "$probe_ns"' EXIT
|
||||
nsenter --net="/run/netns/$probe_ns" -- sh -ec '
|
||||
ip link add probe0 type veth peer name probe1
|
||||
tc qdisc add dev probe0 root netem delay 1ms
|
||||
tc qdisc show dev probe0
|
||||
'
|
||||
SH
|
||||
sudo -n true
|
||||
|
||||
- name: Install upstream irtt
|
||||
run: |
|
||||
|
||||
@@ -47,11 +47,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
with:
|
||||
cache: false
|
||||
|
||||
- name: Install cargo-deny
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
# It remains here while portable actionlint runs with Gitea CI, but scans
|
||||
# both workflow directories through its GitHub Actions rule set.
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
with:
|
||||
persona: regular
|
||||
inputs: |
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
if: matrix.musl
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends musl-tools
|
||||
sudo apt-get install --no-install-recommends musl-tools
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
# the old --skip-git flag is no longer supported.
|
||||
scan-scheduled:
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@b1d267aea5211fa648d9d442fa64b90dbdcbdd36" # v2.7.0
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
||||
scan-pr:
|
||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@b1d267aea5211fa648d9d442fa64b90dbdcbdd36" # v2.7.0
|
||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
exit 0
|
||||
|
||||
- name: Upload Clippy SARIF
|
||||
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
|
||||
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: rust-clippy-results.sarif
|
||||
|
||||
@@ -192,24 +192,6 @@ release. The format is loosely based on
|
||||
|
||||
## irtt-rs
|
||||
|
||||
### 0.9.0
|
||||
|
||||
#### Added
|
||||
|
||||
- Client and TUI `--bind-addr IP[:PORT]` selects a local source address and optional UDP port. Omitted ports and port `0` use an ephemeral port; IPv6 addresses with an explicit port use `[ADDR]:PORT`. The bind address must match the target's address family, and fixed ports can conflict across multiple target sockets.
|
||||
- Client and TUI `--bind-device DEVICE` binds sockets to a network interface on Linux, Android, and Fuchsia.
|
||||
- Client and TUI `--mark MARK` sets a socket firewall mark for policy routing or firewall rules on Linux, Android, and Fuchsia. It accepts decimal and `0x`-prefixed hexadecimal unsigned 32-bit values.
|
||||
- Client and TUI `--fib FIB` selects a routing table on FreeBSD, accepting a decimal unsigned 32-bit value.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Restored CLI compatibility with upstream IRTT for hexadecimal HMAC keys: client, TUI, and server `--hmac`, server `IRTT_SERVER_HMAC`, and client/TUI per-target `@hmac=KEY` accept a lowercase `0x` prefix followed by hexadecimal bytes, with either case allowed for digits. Literal UTF-8 keys remain supported; the HMAC-MD5 algorithm is unchanged.
|
||||
- Server argument debug output now redacts HMAC keys, matching the client and TUI. Server help hides the `IRTT_SERVER_HMAC` value, and invalid global HMAC keys are rejected during configuration preparation without echoing key material.
|
||||
|
||||
#### Compatibility
|
||||
|
||||
- Keys beginning with literal `0x` are now interpreted as hexadecimal. Empty keys, empty hexadecimal keys, odd-length hexadecimal strings, and invalid hexadecimal digits are rejected. Per-target `@hmac=` continues to disable authentication explicitly.
|
||||
|
||||
### 0.8.0
|
||||
|
||||
#### Added
|
||||
|
||||
Generated
+137
-233
@@ -123,11 +123,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.11.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c"
|
||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||
dependencies = [
|
||||
"bit-vec 0.10.1",
|
||||
"bit-vec 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -138,13 +138,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.10.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"serde",
|
||||
]
|
||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@@ -185,30 +181,6 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a"
|
||||
dependencies = [
|
||||
"borsh-derive",
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
@@ -227,12 +199,6 @@ version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
@@ -244,9 +210,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.9.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -264,17 +230,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.1",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -296,15 +251,6 @@ dependencies = [
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap-num"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822c4000301ac390e65995c62207501e3ef800a1fc441df913a5e8e4dc374816"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.7"
|
||||
@@ -326,7 +272,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -382,12 +328,6 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core_detect"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -397,15 +337,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
@@ -482,9 +413,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.4"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865"
|
||||
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
@@ -509,7 +440,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -520,7 +451,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -531,9 +462,9 @@ checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.9"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
@@ -608,9 +539,9 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.3"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
@@ -660,9 +591,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.15"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "finl_unicode"
|
||||
@@ -743,7 +674,17 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -778,12 +719,6 @@ dependencies = [
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
|
||||
|
||||
[[package]]
|
||||
name = "hybrid-array"
|
||||
version = "0.4.15"
|
||||
@@ -823,21 +758,11 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.8"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
@@ -852,7 +777,7 @@ dependencies = [
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -890,16 +815,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "irtt-rs"
|
||||
version = "0.9.0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap-num",
|
||||
"crossterm",
|
||||
"ctrlc",
|
||||
"futures-core",
|
||||
"hex",
|
||||
"humantime",
|
||||
"irtt-client",
|
||||
"irtt-proto",
|
||||
"irtt-server",
|
||||
@@ -964,11 +886,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kasuari"
|
||||
version = "0.4.13"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f14c90e40a6133f4805b83542a7ed7364f56070b447f56e7f4e90297122209"
|
||||
checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"portable-atomic",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -987,9 +909,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -1014,9 +936,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.1"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -1039,16 +961,16 @@ version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac_address"
|
||||
version = "1.2.1"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381"
|
||||
checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a"
|
||||
dependencies = [
|
||||
"nix 0.31.3",
|
||||
"nix 0.30.1",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
@@ -1119,6 +1041,19 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
@@ -1170,9 +1105,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.8"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cabcc654086861f9e1bc5115258de62ef92c79b20f4615a1b0cc57c4e82027d4"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -1271,21 +1206,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.9.4"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd47a460a9cbc3b3429c7e22bf0d76653e779319bae45a499ca010c9a990bb23"
|
||||
checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"psm",
|
||||
"stacker",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.9.4"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "203e46697708f7519ab1608393cf89239cafb46b8e6d08a8ffe359359b11ef44"
|
||||
checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
@@ -1293,22 +1226,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.9.4"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e84ae6340a458d3d6cfc73cf328cf3cfe90b2484cd0de25eb66aa954615ed4d5"
|
||||
checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.9.4"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b57f92f32b3dbce6598fd8f5cdf1f06a6072570b8103fa21728fa9036923e710"
|
||||
checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
@@ -1384,12 +1317,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1403,17 +1336,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proptest"
|
||||
version = "1.12.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0"
|
||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||
dependencies = [
|
||||
"bit-set 0.11.1",
|
||||
"bit-vec 0.10.1",
|
||||
"bit-set 0.8.0",
|
||||
"bit-vec 0.8.0",
|
||||
"bitflags 2.13.2",
|
||||
"chacha20",
|
||||
"core_detect",
|
||||
"num-traits",
|
||||
"rand 0.10.3",
|
||||
"rand 0.9.5",
|
||||
"rand_chacha",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
@@ -1421,15 +1353,6 @@ dependencies = [
|
||||
"unarray",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
@@ -1468,12 +1391,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.3"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
"rand_chacha",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1484,17 +1417,20 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_xorshift"
|
||||
version = "0.5.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf"
|
||||
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
||||
dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1522,7 +1458,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"compact_str",
|
||||
"critical-section",
|
||||
"hashbrown",
|
||||
"hashbrown 0.17.1",
|
||||
"itertools",
|
||||
"kasuari",
|
||||
"lru",
|
||||
@@ -1585,7 +1521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"hashbrown",
|
||||
"hashbrown 0.17.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools",
|
||||
@@ -1721,7 +1657,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1731,7 +1667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"cpufeatures",
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
@@ -1786,9 +1722,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.3"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
@@ -1800,19 +1736,6 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
@@ -1876,9 +1799,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2011,7 +1934,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2037,9 +1960,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.54.1"
|
||||
version = "1.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
@@ -2057,37 +1980,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.17+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.5+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2110,9 +2003,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
@@ -2145,9 +2038,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.28.0"
|
||||
version = "1.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"getrandom 0.4.3",
|
||||
@@ -2226,7 +2119,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"syn 3.0.6",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -2401,17 +2294,28 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
@@ -14,6 +14,7 @@ edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.88"
|
||||
repository = "https://github.com/Lochnair/irtt-rs"
|
||||
homepage = "https://github.com/Lochnair/irtt-rs"
|
||||
|
||||
[workspace.dependencies]
|
||||
irtt-proto = { version = "0.5.0", path = "crates/irtt-proto" }
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
|
||||
[build]
|
||||
# MIPS musl std expects libunwind; cross GCC provides libgcc_eh.
|
||||
pre-build = [
|
||||
'''
|
||||
for cc in /usr/local/bin/mips*-gcc; do
|
||||
[ -x "$cc" ] || continue
|
||||
archive=$("$cc" -print-file-name=libgcc_eh.a)
|
||||
test -f "$archive" || exit 1
|
||||
ln -sf libgcc_eh.a "$(dirname "$archive")/libunwind.a"
|
||||
done
|
||||
''',
|
||||
]
|
||||
|
||||
[target.riscv64gc-unknown-linux-musl]
|
||||
image = "ghcr.io/cross-rs/riscv64gc-unknown-linux-musl:main"
|
||||
|
||||
[target.mips-unknown-linux-musl]
|
||||
image = "ghcr.io/cross-rs/mips-unknown-linux-musl:main"
|
||||
build-std = ["std", "panic_abort"]
|
||||
|
||||
[target.mipsel-unknown-linux-musl]
|
||||
image = "ghcr.io/cross-rs/mipsel-unknown-linux-musl:main"
|
||||
build-std = ["std", "panic_abort"]
|
||||
|
||||
[target.mips64-unknown-linux-muslabi64]
|
||||
image = "ghcr.io/cross-rs/mips64-unknown-linux-muslabi64:main"
|
||||
build-std = ["std", "panic_abort"]
|
||||
|
||||
[target.mips64el-unknown-linux-muslabi64]
|
||||
image = "ghcr.io/cross-rs/mips64el-unknown-linux-muslabi64:main"
|
||||
build-std = ["std", "panic_abort"]
|
||||
@@ -84,15 +84,12 @@ cargo install irtt-rs --locked
|
||||
This installs the `irtt-rs` dispatcher plus dedicated `irtt-client`,
|
||||
`irtt-tui`, and `irtt-server` binaries. Each applet can also be built in
|
||||
isolation with `--no-default-features` plus the applet's own feature, for
|
||||
example a lightweight client-only install without statistics:
|
||||
example a lightweight client-only install:
|
||||
|
||||
```sh
|
||||
cargo install irtt-rs --locked --no-default-features --features client
|
||||
```
|
||||
|
||||
Add `stats` (`--features client,stats`) to retain statistics in a client-only
|
||||
build. Statistics are enabled by default; the TUI always enables them.
|
||||
|
||||
For development, or to install directly from a checkout:
|
||||
|
||||
```sh
|
||||
@@ -107,9 +104,6 @@ Probe a server:
|
||||
|
||||
```sh
|
||||
irtt-client netperf-eu.bufferbloat.net:2112 --duration 30s --interval 100ms
|
||||
|
||||
# Negotiate parameters without sending measurement probes
|
||||
irtt-client --no-test netperf-eu.bufferbloat.net:2112
|
||||
```
|
||||
|
||||
Probe continuously until interrupted (`Ctrl-C`):
|
||||
@@ -168,7 +162,6 @@ when to use it instead of `irtt-client`/`irtt-server` directly.
|
||||
| --- | --- |
|
||||
| `--no-default-features` | `irtt-rs` |
|
||||
| `--no-default-features --features client` | `irtt-rs`, `irtt-client` |
|
||||
| `--no-default-features --features client,stats` | `irtt-rs`, `irtt-client` (with statistics) |
|
||||
| `--no-default-features --features server` | `irtt-rs`, `irtt-server` |
|
||||
| `--no-default-features --features tui` | `irtt-rs`, `irtt-tui` |
|
||||
| Default features (or `--all-features`) | `irtt-rs`, `irtt-client`, `irtt-tui`, `irtt-server` |
|
||||
|
||||
+14
-11
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "irtt-rs"
|
||||
version = "0.9.0"
|
||||
version = "0.8.0"
|
||||
description = "IRTT-compatible client, TUI, and server applications"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords = ["irtt", "networking", "latency", "rtt"]
|
||||
categories = ["command-line-utilities", "network-programming"]
|
||||
@@ -40,31 +41,33 @@ required-features = ["server"]
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
default = ["client", "server", "tui", "stats"]
|
||||
client = ["dep:humantime", "dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto"]
|
||||
stats = ["dep:irtt-stats"]
|
||||
server = ["dep:irtt-server", "dep:irtt-proto", "dep:tokio"]
|
||||
tui =["dep:humantime", "dep:tokio", "dep:futures-core", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "stats", "dep:ratatui", "dep:crossterm", "dep:chrono"]
|
||||
default = ["client", "server", "tui"]
|
||||
client = ["dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats"]
|
||||
server = ["dep:irtt-server", "dep:tokio"]
|
||||
tui =["dep:tokio", "dep:futures-core", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm", "dep:chrono"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.45", default-features = false, features = ["clock"], optional = true }
|
||||
clap = { version = "4.6.7", features = ["derive", "env"] }
|
||||
clap-num = "=1.2.0"
|
||||
crossterm = { version = "0.29.0", features = ["event-stream"], optional = true }
|
||||
futures-core = { version = "0.3.34", default-features = false, optional = true }
|
||||
hex = "0.4.3"
|
||||
humantime = { version = "2.4.0", optional = true }
|
||||
ctrlc = { version = "3.5.2", features = ["termination"] }
|
||||
irtt-client = { workspace = true, optional = true }
|
||||
irtt-proto = { workspace = true, optional = true }
|
||||
irtt-server = { workspace = true, optional = true }
|
||||
irtt-stats = { workspace = true, optional = true }
|
||||
ratatui = { version = "0.30.2", optional = true }
|
||||
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync", "macros"], optional = true }
|
||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync", "macros"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
irtt-server.workspace = true
|
||||
tokio = { version = "1.54.1", default-features = false, features = ["rt", "sync"] }
|
||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "sync"] }
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
nix = { version = "0.31.3", features = ["term", "poll", "signal", "process"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = "symbols"
|
||||
@@ -11,13 +11,8 @@ Installing this crate builds:
|
||||
- `irtt-tui` — the interactive terminal UI (feature `tui`).
|
||||
- `irtt-server` — the dedicated server binary (feature `server`).
|
||||
|
||||
The `client`, `server`, `tui`, and `stats` features are enabled by default, so a
|
||||
plain `cargo install irtt-rs` installs everything. The TUI requires statistics
|
||||
and enables `stats` automatically.
|
||||
|
||||
For a client without statistics, use
|
||||
`cargo install irtt-rs --locked --no-default-features --features client --bin irtt-client`.
|
||||
Use `--features client,stats` for a client-only build with statistics.
|
||||
All three features are enabled by default, so a plain `cargo install irtt-rs`
|
||||
installs everything.
|
||||
|
||||
## This is an application, not a library
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
|
||||
};
|
||||
|
||||
match command {
|
||||
"-V" | "--version" => Ok(AppletDispatch::Help(crate::version::app())),
|
||||
"-h" | "--help" => Ok(AppletDispatch::Help(dispatcher_help())),
|
||||
"client" => Ok(AppletDispatch::Run {
|
||||
applet: RequestedApplet::Client,
|
||||
@@ -79,7 +78,6 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
|
||||
|
||||
pub fn dispatcher_help() -> String {
|
||||
let mut command = Command::new("irtt-rs")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("IRTT-compatible multi-applet dispatcher")
|
||||
.subcommand(Command::new("client").about(client_applet_about()))
|
||||
.subcommand(Command::new("tui").about(tui_applet_about()))
|
||||
|
||||
@@ -9,18 +9,14 @@ use clap::{Parser, ValueEnum};
|
||||
pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(
|
||||
name = "irtt-client",
|
||||
version,
|
||||
about = "Minimal IRTT-compatible stream client"
|
||||
)]
|
||||
#[command(name = "irtt-client", about = "Minimal IRTT-compatible stream client")]
|
||||
pub struct ClientArgs {
|
||||
/// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.
|
||||
#[arg(
|
||||
value_name = "TARGET",
|
||||
num_args = 0..,
|
||||
value_parser = parse_target,
|
||||
long_help = "Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode. Keys use literal UTF-8 or lowercase 0x followed by hexadecimal bytes.\n\nExamples:\n irtt-client host-a:2112 host-b:2112\n irtt-client eu=host-a:2112 us=host-b:2112\n irtt-client ams=host-a:2112@hmac=secret public=host-b:2112@hmac=\n\nA target without @hmac= inherits --hmac. @hmac= explicitly disables it for that target."
|
||||
long_help = "Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.\n\nExamples:\n irtt-client host-a:2112 host-b:2112\n irtt-client eu=host-a:2112 us=host-b:2112\n irtt-client ams=host-a:2112@hmac=secret public=host-b:2112@hmac=\n\nA target without @hmac= inherits --hmac. @hmac= explicitly disables it for that target."
|
||||
)]
|
||||
pub targets: Vec<TargetArg>,
|
||||
|
||||
@@ -31,15 +27,6 @@ pub struct ClientArgs {
|
||||
)]
|
||||
pub targets_stdin: bool,
|
||||
|
||||
/// Negotiate parameters and exit without sending measurement probes.
|
||||
#[arg(
|
||||
short = 'n',
|
||||
long,
|
||||
conflicts_with = "targets_stdin",
|
||||
long_help = "Negotiate parameters with each target and exit without sending measurement probes. Duration options still set the requested duration, but do not keep this mode running. Reports negotiated parameters using the selected output format, without a measurement summary."
|
||||
)]
|
||||
pub no_test: bool,
|
||||
|
||||
/// Managed group pacing for multi-target mode.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -51,43 +38,13 @@ pub struct ClientArgs {
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
short = 'd',
|
||||
default_value = "10s",
|
||||
value_parser = parse_test_duration,
|
||||
help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)",
|
||||
long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use --continuous for continuous mode (0 is also accepted).\n\nWhen statistics support is compiled in and --no-stats is not selected, finite runs retain exact statistics for final summaries. Continuous mode uses bounded-memory running statistics and prints a final summary when interrupted; --targets-stdin also prints the final retained summary on EOF."
|
||||
help = "Test duration; use 0 for continuous mode",
|
||||
long_help = "Test duration; use 0 for continuous mode.\n\nFinite runs retain exact statistics for final summaries. Continuous mode uses bounded-memory running statistics and prints a final summary when interrupted; --targets-stdin also prints the final retained summary on EOF."
|
||||
)]
|
||||
pub duration: Duration,
|
||||
|
||||
/// Run continuously until interrupted (equivalent to --duration 0).
|
||||
#[arg(long, conflicts_with = "duration")]
|
||||
pub continuous: bool,
|
||||
|
||||
/// Disable statistics collection and final summaries; retain measurement event output.
|
||||
/// Accepted even when statistics support is not compiled in.
|
||||
#[arg(long)]
|
||||
pub no_stats: bool,
|
||||
|
||||
/// Retry failed, closed, or silent sessions (requires continuous mode).
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with = "no_test",
|
||||
long_help = "Retry failed initial opens and closed, failed, or silent sessions independently per target. Requires --continuous or --duration 0. Unsuccessful connections use exponential backoff (defaults: 250ms initial, 10s maximum), configurable with --reconnect-initial-delay and --reconnect-max-delay. Use --reconnect-timeout to configure silence detection and --no-stats when only measurement events are needed."
|
||||
)]
|
||||
pub reconnect: bool,
|
||||
|
||||
/// Reconnect after this long without valid replies while sending probes.
|
||||
#[arg(long, default_value = "10s", value_parser = crate::shared::client::parse_duration, requires = "reconnect")]
|
||||
pub reconnect_timeout: Duration,
|
||||
|
||||
/// Initial retry delay, restored after a valid reply.
|
||||
#[arg(long, default_value = "250ms", value_parser = crate::shared::client::parse_duration, requires = "reconnect")]
|
||||
pub reconnect_initial_delay: Duration,
|
||||
|
||||
/// Maximum retry delay (must be at least twice the initial delay).
|
||||
#[arg(long, default_value = "10s", value_parser = crate::shared::client::parse_duration, requires = "reconnect")]
|
||||
pub reconnect_max_delay: Duration,
|
||||
|
||||
#[command(flatten)]
|
||||
pub common: CommonClientArgs,
|
||||
|
||||
@@ -95,7 +52,6 @@ pub struct ClientArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_enum,
|
||||
short = 'f',
|
||||
default_value_t = OutputFormat::Table,
|
||||
long_help = "Event row output format.\n\nTable is the default interactive format. CSV, TSV, and JSON Lines default to all columns for structured export."
|
||||
)]
|
||||
@@ -127,7 +83,7 @@ pub struct ClientArgs {
|
||||
pub header: HeaderMode,
|
||||
|
||||
/// Include extra fields in table output and final summaries.
|
||||
#[arg(short = 'v', long)]
|
||||
#[arg(long)]
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
@@ -137,43 +93,23 @@ impl ClientArgs {
|
||||
/// Targets are only required here. `--list-columns` returns before this is
|
||||
/// called, so listing columns still needs no target.
|
||||
pub async fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
||||
if self.reconnect && !self.is_continuous() {
|
||||
return Err("--reconnect requires --continuous or --duration 0".to_owned());
|
||||
}
|
||||
if self
|
||||
.reconnect_initial_delay
|
||||
.checked_mul(2)
|
||||
.is_none_or(|minimum| self.reconnect_max_delay < minimum)
|
||||
{
|
||||
return Err(
|
||||
"--reconnect-max-delay must be at least twice --reconnect-initial-delay".to_owned(),
|
||||
);
|
||||
}
|
||||
if self.targets_stdin && !self.is_continuous() {
|
||||
return Err("--targets-stdin requires --continuous or --duration 0".to_owned());
|
||||
return Err("--targets-stdin requires --duration 0".to_owned());
|
||||
}
|
||||
let mut setup = prepare_managed_run(
|
||||
prepare_managed_run(
|
||||
&self.common,
|
||||
if self.continuous {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
self.duration
|
||||
},
|
||||
self.duration,
|
||||
TargetSelection {
|
||||
targets: &self.targets,
|
||||
pacing: self.pacing,
|
||||
stdin_controlled: self.targets_stdin,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if self.no_test {
|
||||
setup.client.request.run_mode = irtt_client::RunMode::NoTest;
|
||||
}
|
||||
Ok(setup)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn is_continuous(&self) -> bool {
|
||||
self.continuous || self.duration == Duration::ZERO
|
||||
self.duration == Duration::ZERO
|
||||
}
|
||||
|
||||
pub fn timestamp_mode(&self) -> TimestampArg {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod args;
|
||||
pub mod output;
|
||||
pub mod run;
|
||||
#[cfg(feature = "stats")]
|
||||
pub mod summary;
|
||||
|
||||
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
||||
|
||||
@@ -9,7 +9,6 @@ use irtt_client::{
|
||||
ServerTiming, SignedDuration, WarningKind,
|
||||
};
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate};
|
||||
|
||||
use super::args::{HeaderMode, OutputFormat};
|
||||
@@ -49,12 +48,10 @@ impl OutputConfig {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
pub(super) fn prints_summary(&self) -> bool {
|
||||
self.format.prints_summary()
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
pub(super) fn summary_verbose(&self) -> bool {
|
||||
self.verbose
|
||||
}
|
||||
@@ -99,22 +96,15 @@ impl OutputConfig {
|
||||
&self,
|
||||
event: &ClientEvent,
|
||||
target: Option<&str>,
|
||||
#[cfg(feature = "stats")] stats: Option<&EventStatsUpdate>,
|
||||
stats: Option<&EventStatsUpdate>,
|
||||
) -> Option<String> {
|
||||
let row = OutputRow::from_event(event);
|
||||
if self.default_table_rows && row.is_default_table_hidden() {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
let ipdv_ns = row
|
||||
.seq()
|
||||
.and_then(|seq| ipdv_pair(stats, seq))
|
||||
.map(|pair| pair.rtt_ipdv_ns);
|
||||
#[cfg(not(feature = "stats"))]
|
||||
let ipdv_ns = None;
|
||||
let context = RenderContext {
|
||||
ipdv_ns,
|
||||
stats,
|
||||
target,
|
||||
verbose: self.verbose,
|
||||
};
|
||||
@@ -165,7 +155,7 @@ impl OutputConfig {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct RenderContext<'a> {
|
||||
ipdv_ns: Option<i128>,
|
||||
stats: Option<&'a EventStatsUpdate>,
|
||||
target: Option<&'a str>,
|
||||
verbose: bool,
|
||||
}
|
||||
@@ -551,7 +541,7 @@ columns! {
|
||||
SendDelay("sd", ["send_delay"], 9, right, "human-readable client-to-server delay");
|
||||
SendDelayUs("sd_us", ["send_delay_us"], 10, right, "client-to-server delay in signed microseconds");
|
||||
Ipdv("ipdv", [], 9, right, "human-readable round-trip IPDV for adjacent samples");
|
||||
IpdvUs("ipdv_us", [], 10, right, "round-trip IPDV in signed microseconds");
|
||||
IpdvUs("ipdv_us", [], 10, right, "round-trip IPDV in microseconds");
|
||||
ServerProcessing("proc", ["server_processing"], 9, right, "human-readable server processing time");
|
||||
ServerProcessingUs("server_processing_us", [], 20, right, "server processing time in microseconds");
|
||||
Bytes("bytes", [], 10, right, "packet bytes for packet events");
|
||||
@@ -827,12 +817,14 @@ fn cell_for(row: &OutputRow, column: Column, context: RenderContext<'_>) -> Opti
|
||||
.reply()
|
||||
.and_then(|reply| reply.one_way.and_then(|one_way| one_way.client_to_server))
|
||||
.map(|value| CellValue::Integer(signed_duration_us(value))),
|
||||
Column::Ipdv => context.ipdv_ns.map(|value| {
|
||||
CellValue::text(format_signed_duration(SignedDuration::from_nanos(value)))
|
||||
}),
|
||||
Column::IpdvUs => context
|
||||
.ipdv_ns
|
||||
.map(|value| CellValue::Integer(value / 1_000)),
|
||||
Column::Ipdv => row
|
||||
.seq()
|
||||
.and_then(|seq| ipdv_pair(context.stats, seq))
|
||||
.map(|pair| CellValue::text(format_duration(pair.rtt_ipdv))),
|
||||
Column::IpdvUs => row
|
||||
.seq()
|
||||
.and_then(|seq| ipdv_pair(context.stats, seq))
|
||||
.map(|pair| CellValue::Unsigned(duration_us(pair.rtt_ipdv))),
|
||||
Column::ServerProcessing => row
|
||||
.reply()
|
||||
.and_then(|reply| reply.server_timing.and_then(|timing| timing.processing))
|
||||
@@ -939,7 +931,6 @@ fn server_timing_i64(
|
||||
.map(|value| CellValue::Integer(i128::from(value)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
fn ipdv_pair(stats: Option<&EventStatsUpdate>, seq: u32) -> Option<&IpdvPairUpdate> {
|
||||
let stats = stats?;
|
||||
stats
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
|
||||
io::{self, BufRead, Write},
|
||||
sync::Arc,
|
||||
thread,
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
use irtt_client::{
|
||||
managed::{
|
||||
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent,
|
||||
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent, ManagedStatus,
|
||||
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
||||
},
|
||||
ClientEvent,
|
||||
@@ -16,25 +16,26 @@ use irtt_client::{
|
||||
use super::{args::ClientArgs, output::OutputConfig};
|
||||
|
||||
use crate::shared::client::{
|
||||
parse_stdin_target_set,
|
||||
expected_probe_count, parse_stdin_target_set,
|
||||
session::{
|
||||
drain_final_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
||||
request_managed_stop_once,
|
||||
request_managed_stop_once, should_print_final_summary,
|
||||
},
|
||||
worker::ManagedWorker,
|
||||
ManagedRunSetup, TargetSpec, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
||||
};
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
use irtt_stats::{EventStatsUpdate, StatsCollector, StatsConfig};
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
use crate::shared::client::{expected_probe_count, session::should_print_final_summary};
|
||||
#[cfg(feature = "stats")]
|
||||
use irtt_client::managed::ManagedStatus;
|
||||
#[cfg(feature = "stats")]
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
const MIB: u64 = 1024 * 1024;
|
||||
|
||||
const GIB: u64 = 1024 * MIB;
|
||||
|
||||
const FINITE_STATS_MEMORY_WARNING_BYTES: u64 = 128 * MIB;
|
||||
|
||||
const FINITE_STATS_MEMORY_STRONG_WARNING_BYTES: u64 = 512 * MIB;
|
||||
|
||||
const FINITE_STATS_MEMORY_VERY_STRONG_WARNING_BYTES: u64 = GIB;
|
||||
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
||||
|
||||
pub async fn run_stream(
|
||||
@@ -49,7 +50,6 @@ pub async fn run_stream(
|
||||
result = args.prepare() => result.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?,
|
||||
_ = shutdown.wait_for(|requested| *requested) => return Ok(()),
|
||||
};
|
||||
#[cfg(feature = "stats")]
|
||||
let multi_target = setup.is_multi_target();
|
||||
let output_config = OutputConfig::new(
|
||||
args.format,
|
||||
@@ -58,55 +58,34 @@ pub async fn run_stream(
|
||||
args.verbose,
|
||||
)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
||||
#[cfg(feature = "stats")]
|
||||
if let Some(warning) = reconnect_stats_warning(&args) {
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
if setup.stdin_controlled {
|
||||
return run_stdin_stream(setup, output_config, shutdown, &args).await;
|
||||
return run_stdin_stream(setup, output_config, shutdown).await;
|
||||
}
|
||||
let continuous = args.is_continuous();
|
||||
let target_count = setup.target_count();
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
if let Some(warning) = finite_stats_memory_warning(&args, setup.target_count()) {
|
||||
if let Some(warning) = finite_stats_memory_warning(&args, target_count) {
|
||||
eprintln!("{warning}");
|
||||
}
|
||||
if *shutdown.borrow() {
|
||||
return Ok(());
|
||||
}
|
||||
let (task, handle) = ManagedClient::task(
|
||||
{
|
||||
let mut config = setup.managed_config();
|
||||
config.reconnect_timeout = args.reconnect.then_some(args.reconnect_timeout);
|
||||
config.reconnect_initial_delay = args.reconnect_initial_delay;
|
||||
config.reconnect_max_delay = args.reconnect_max_delay;
|
||||
config
|
||||
},
|
||||
setup.managed_targets(),
|
||||
)?;
|
||||
let (task, handle) = ManagedClient::task(setup.managed_config(), setup.managed_targets())?;
|
||||
let mut events = handle.subscribe()?;
|
||||
let mut status = handle.subscribe_status();
|
||||
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||
let mut stream_output = StreamOutput {
|
||||
#[cfg(feature = "stats")]
|
||||
continuous: args.is_continuous(),
|
||||
#[cfg(feature = "stats")]
|
||||
collect_stats: !args.no_stats,
|
||||
config: output_config,
|
||||
header_printed: false,
|
||||
#[cfg(feature = "stats")]
|
||||
print_final_summary: false,
|
||||
#[cfg(feature = "stats")]
|
||||
show_running_only_summary_note: false,
|
||||
out: &mut stdout,
|
||||
};
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
let mut stats = setup
|
||||
.targets
|
||||
.iter()
|
||||
.filter(|_| !args.no_stats)
|
||||
.map(|target| {
|
||||
(
|
||||
target.label.clone(),
|
||||
@@ -141,7 +120,7 @@ pub async fn run_stream(
|
||||
_ = shutdown.changed(), if !interrupted => {}
|
||||
changed = status.changed() => { if changed.is_err() { break; } }
|
||||
event = events.recv() => match event {
|
||||
Ok(event) => process_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?,
|
||||
Ok(event) => process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||
dropped_events = dropped_events.saturating_add(count);
|
||||
}
|
||||
@@ -154,13 +133,7 @@ pub async fn run_stream(
|
||||
}
|
||||
let outcome = owner.join().await?;
|
||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||
process_event(
|
||||
event,
|
||||
&mut stream_output,
|
||||
#[cfg(feature = "stats")]
|
||||
&mut stats,
|
||||
&mut terminal_targets,
|
||||
)
|
||||
process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
||||
})?;
|
||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||
eprintln!("{warning}");
|
||||
@@ -194,25 +167,21 @@ pub async fn run_stream(
|
||||
})
|
||||
}),
|
||||
};
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
stream_output.print_final_summary =
|
||||
!args.no_stats && !args.no_test && should_print_final_summary(continuous, interrupted);
|
||||
stream_output.show_running_only_summary_note =
|
||||
continuous && interrupted && stream_output.print_final_summary;
|
||||
for target in &setup.targets {
|
||||
let Some(target_stats) = stats.get(&target.label) else {
|
||||
continue;
|
||||
};
|
||||
if multi_target
|
||||
&& stream_output.print_final_summary
|
||||
&& stream_output.config.prints_summary()
|
||||
{
|
||||
writeln!(stream_output.out)?;
|
||||
writeln!(stream_output.out, "target: {}", target.label)?;
|
||||
}
|
||||
stream_output.print_summary(target_stats)?;
|
||||
stream_output.print_final_summary = should_print_final_summary(continuous, interrupted);
|
||||
stream_output.show_running_only_summary_note =
|
||||
continuous && interrupted && stream_output.print_final_summary;
|
||||
for target in &setup.targets {
|
||||
let target_stats = stats.get(&target.label).expect(
|
||||
"every prepared target must have a stats collector inserted before the run begins",
|
||||
);
|
||||
if multi_target
|
||||
&& stream_output.print_final_summary
|
||||
&& stream_output.config.prints_summary()
|
||||
{
|
||||
writeln!(stream_output.out)?;
|
||||
writeln!(stream_output.out, "target: {}", target.label)?;
|
||||
}
|
||||
stream_output.print_summary(target_stats)?;
|
||||
}
|
||||
stream_output.out.flush()?;
|
||||
if let Some(error) = terminal_error {
|
||||
@@ -387,33 +356,21 @@ impl BoundedTargetSet {
|
||||
fn process_stdin_event<W: Write>(
|
||||
event: ManagedEvent,
|
||||
stream_output: &mut StreamOutput<'_, W>,
|
||||
#[cfg(feature = "stats")] stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||
terminal_targets: &mut BoundedTargetSet,
|
||||
) -> io::Result<()> {
|
||||
match event {
|
||||
#[cfg(feature = "stats")]
|
||||
ManagedEvent::TargetStateChanged { target, .. } if stream_output.collect_stats => {
|
||||
ManagedEvent::TargetStateChanged { target, .. } => {
|
||||
stats
|
||||
.entry(target)
|
||||
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
||||
}
|
||||
ManagedEvent::Client { target, event } => {
|
||||
#[cfg(feature = "stats")]
|
||||
let update = stream_output.collect_stats.then(|| {
|
||||
let collector = stats
|
||||
.entry(target.clone())
|
||||
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
||||
if matches!(event, ClientEvent::SessionStarted(_)) {
|
||||
*collector = StatsCollector::new(stats_config(true));
|
||||
}
|
||||
collector.process(&event)
|
||||
});
|
||||
stream_output.print_event(
|
||||
&event,
|
||||
Some(target.id.as_str()),
|
||||
#[cfg(feature = "stats")]
|
||||
update.as_ref(),
|
||||
)?;
|
||||
let collector = stats
|
||||
.entry(target.clone())
|
||||
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
||||
let update = collector.process(&event);
|
||||
stream_output.print_event(&event, Some(target.id.as_str()), &update)?;
|
||||
}
|
||||
ManagedEvent::TargetFinished { outcome }
|
||||
if terminal_targets.insert(outcome.target.clone()) =>
|
||||
@@ -425,7 +382,6 @@ fn process_stdin_event<W: Write>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
fn reconcile_stdin_stats(
|
||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||
status: &ManagedStatus,
|
||||
@@ -442,15 +398,10 @@ fn reconcile_stdin_stats(
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
fn snapshot_stdin_summary_targets(
|
||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||
status: &ManagedStatus,
|
||||
enabled: bool,
|
||||
) -> BTreeSet<TargetInstance> {
|
||||
if !enabled {
|
||||
return BTreeSet::new();
|
||||
}
|
||||
let targets = status
|
||||
.targets
|
||||
.iter()
|
||||
@@ -475,21 +426,11 @@ async fn run_stdin_stream(
|
||||
setup: ManagedRunSetup,
|
||||
output_config: OutputConfig,
|
||||
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
||||
args: &ClientArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if *shutdown.borrow() {
|
||||
return Ok(());
|
||||
}
|
||||
let (task, handle) = ManagedClient::task(
|
||||
{
|
||||
let mut config = setup.managed_config();
|
||||
config.reconnect_timeout = args.reconnect.then_some(args.reconnect_timeout);
|
||||
config.reconnect_initial_delay = args.reconnect_initial_delay;
|
||||
config.reconnect_max_delay = args.reconnect_max_delay;
|
||||
config
|
||||
},
|
||||
setup.managed_targets(),
|
||||
)?;
|
||||
let (task, handle) = ManagedClient::task(setup.managed_config(), setup.managed_targets())?;
|
||||
let mut events = handle.subscribe()?;
|
||||
let mut status = handle.subscribe_status();
|
||||
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||
@@ -498,19 +439,12 @@ async fn run_stdin_stream(
|
||||
|
||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||
let mut stream_output = StreamOutput {
|
||||
#[cfg(feature = "stats")]
|
||||
continuous: args.is_continuous(),
|
||||
#[cfg(feature = "stats")]
|
||||
collect_stats: !args.no_stats,
|
||||
config: output_config,
|
||||
header_printed: false,
|
||||
#[cfg(feature = "stats")]
|
||||
print_final_summary: false,
|
||||
#[cfg(feature = "stats")]
|
||||
show_running_only_summary_note: false,
|
||||
out: &mut stdout,
|
||||
};
|
||||
#[cfg(feature = "stats")]
|
||||
let mut stats = BTreeMap::new();
|
||||
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
||||
let mut stdin_changed = false;
|
||||
@@ -524,7 +458,6 @@ async fn run_stdin_stream(
|
||||
let mut dropped_events = 0_u64;
|
||||
let mut stop_requested = false;
|
||||
let mut stop = None;
|
||||
#[cfg(feature = "stats")]
|
||||
let mut summary_targets = None;
|
||||
loop {
|
||||
let snapshot = status.borrow_and_update().clone();
|
||||
@@ -576,20 +509,12 @@ async fn run_stdin_stream(
|
||||
}
|
||||
if stop.is_some() {
|
||||
preparing = None;
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
summary_targets = Some(snapshot_stdin_summary_targets(
|
||||
&mut stats,
|
||||
&snapshot,
|
||||
!args.no_stats,
|
||||
));
|
||||
}
|
||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &snapshot));
|
||||
if request_managed_stop_once(&mut stop_requested) {
|
||||
drop(handle.stop());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stats")]
|
||||
reconcile_stdin_stats(&mut stats, &snapshot, summary_targets.as_ref());
|
||||
if snapshot.final_outcome.is_some() {
|
||||
break;
|
||||
@@ -611,10 +536,7 @@ async fn run_stdin_stream(
|
||||
}
|
||||
Err(error) => {
|
||||
stop = Some(StdinStop::Fatal(format!("invalid --targets-stdin target set: {error}")));
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
||||
}
|
||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow()));
|
||||
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||
}
|
||||
}
|
||||
@@ -635,18 +557,14 @@ async fn run_stdin_stream(
|
||||
}
|
||||
Err(_) => {
|
||||
stop = Some(StdinStop::Fatal("--targets-stdin update was rejected".to_owned()));
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
||||
}
|
||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow()));
|
||||
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
event = events.recv() => match event {
|
||||
Ok(event) => {
|
||||
process_stdin_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?;
|
||||
#[cfg(feature = "stats")]
|
||||
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?;
|
||||
reconcile_stdin_stats(&mut stats, &status.borrow(), summary_targets.as_ref());
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||
@@ -659,13 +577,7 @@ async fn run_stdin_stream(
|
||||
|
||||
let outcome = owner.join().await?;
|
||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||
process_stdin_event(
|
||||
event,
|
||||
&mut stream_output,
|
||||
#[cfg(feature = "stats")]
|
||||
&mut stats,
|
||||
&mut terminal_targets,
|
||||
)
|
||||
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
||||
})?;
|
||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||
eprintln!("{warning}");
|
||||
@@ -682,28 +594,24 @@ async fn run_stdin_stream(
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
{
|
||||
let print_summary =
|
||||
!args.no_stats && matches!(stop, Some(StdinStop::Interrupted | StdinStop::Eof));
|
||||
stream_output.print_final_summary = print_summary;
|
||||
stream_output.show_running_only_summary_note = print_summary;
|
||||
if let Some(summary_targets) = summary_targets {
|
||||
let multi_target = summary_targets.len() > 1;
|
||||
for target in summary_targets {
|
||||
let stats = stats
|
||||
.get(&target)
|
||||
.expect("summary snapshot retains every selected target collector");
|
||||
if multi_target && stream_output.config.prints_summary() {
|
||||
writeln!(stream_output.out)?;
|
||||
writeln!(
|
||||
stream_output.out,
|
||||
"target: {} (generation {})",
|
||||
target.id, target.generation
|
||||
)?;
|
||||
}
|
||||
stream_output.print_summary(stats)?;
|
||||
let print_summary = matches!(stop, Some(StdinStop::Interrupted | StdinStop::Eof));
|
||||
stream_output.print_final_summary = print_summary;
|
||||
stream_output.show_running_only_summary_note = print_summary;
|
||||
if let Some(summary_targets) = summary_targets {
|
||||
let multi_target = summary_targets.len() > 1;
|
||||
for target in summary_targets {
|
||||
let stats = stats
|
||||
.get(&target)
|
||||
.expect("summary snapshot retains every selected target collector");
|
||||
if multi_target && stream_output.config.prints_summary() {
|
||||
writeln!(stream_output.out)?;
|
||||
writeln!(
|
||||
stream_output.out,
|
||||
"target: {} (generation {})",
|
||||
target.id, target.generation
|
||||
)?;
|
||||
}
|
||||
stream_output.print_summary(stats)?;
|
||||
}
|
||||
}
|
||||
stream_output.out.flush()?;
|
||||
@@ -722,28 +630,16 @@ async fn run_stdin_stream(
|
||||
fn process_event<W: Write>(
|
||||
event: ManagedEvent,
|
||||
stream_output: &mut StreamOutput<'_, W>,
|
||||
#[cfg(feature = "stats")] stats: &mut BTreeMap<String, StatsCollector>,
|
||||
stats: &mut BTreeMap<String, StatsCollector>,
|
||||
terminal_targets: &mut HashSet<TargetInstance>,
|
||||
) -> io::Result<()> {
|
||||
match event {
|
||||
ManagedEvent::Client { target, event } => {
|
||||
#[cfg(feature = "stats")]
|
||||
let update = stream_output.collect_stats.then(|| {
|
||||
let collector = stats
|
||||
.get_mut(target.id.as_str())
|
||||
.expect("prepared target has a statistics collector");
|
||||
if matches!(event, ClientEvent::SessionStarted(_)) {
|
||||
let config = stats_config(stream_output.continuous);
|
||||
*collector = StatsCollector::new(config);
|
||||
}
|
||||
collector.process(&event)
|
||||
});
|
||||
stream_output.print_event(
|
||||
&event,
|
||||
Some(target.id.as_str()),
|
||||
#[cfg(feature = "stats")]
|
||||
update.as_ref(),
|
||||
)?;
|
||||
let collector = stats
|
||||
.entry(target.id.as_str().to_owned())
|
||||
.or_insert_with(|| StatsCollector::new(stats_config(false)));
|
||||
let update = collector.process(&event);
|
||||
stream_output.print_event(&event, Some(target.id.as_str()), &update)?;
|
||||
}
|
||||
ManagedEvent::TargetFinished { outcome } => {
|
||||
terminal_targets.insert(outcome.target.clone());
|
||||
@@ -777,24 +673,13 @@ fn target_failure_messages(target: &ManagedTargetOutcome) -> Vec<String> {
|
||||
messages
|
||||
}
|
||||
fn dropped_event_warning(dropped_events: u64) -> Option<String> {
|
||||
let affected = if cfg!(feature = "stats") {
|
||||
"output and statistics"
|
||||
} else {
|
||||
"output"
|
||||
};
|
||||
(dropped_events > 0).then(|| format!("irtt-rs: warning: dropped {dropped_events} managed run event{}; {affected} may be incomplete", if dropped_events == 1 { "" } else { "s" }))
|
||||
(dropped_events > 0).then(|| format!("irtt-rs: warning: dropped {dropped_events} managed run event{}; output and statistics may be incomplete", if dropped_events == 1 { "" } else { "s" }))
|
||||
}
|
||||
|
||||
struct StreamOutput<'a, W: Write> {
|
||||
#[cfg(feature = "stats")]
|
||||
continuous: bool,
|
||||
#[cfg(feature = "stats")]
|
||||
collect_stats: bool,
|
||||
config: OutputConfig,
|
||||
header_printed: bool,
|
||||
#[cfg(feature = "stats")]
|
||||
print_final_summary: bool,
|
||||
#[cfg(feature = "stats")]
|
||||
show_running_only_summary_note: bool,
|
||||
out: &'a mut W,
|
||||
}
|
||||
@@ -803,15 +688,10 @@ impl<W: Write> StreamOutput<'_, W> {
|
||||
&mut self,
|
||||
event: &ClientEvent,
|
||||
target: Option<&str>,
|
||||
#[cfg(feature = "stats")] update: Option<&EventStatsUpdate>,
|
||||
update: &EventStatsUpdate,
|
||||
) -> io::Result<()> {
|
||||
self.print_header()?;
|
||||
if let Some(line) = self.config.render_event(
|
||||
event,
|
||||
target,
|
||||
#[cfg(feature = "stats")]
|
||||
update,
|
||||
) {
|
||||
if let Some(line) = self.config.render_event(event, target, Some(update)) {
|
||||
writeln!(self.out, "{line}")?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -827,7 +707,6 @@ impl<W: Write> StreamOutput<'_, W> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
fn print_summary(&mut self, stats: &StatsCollector) -> io::Result<()> {
|
||||
if self.print_final_summary && self.config.prints_summary() {
|
||||
write!(
|
||||
@@ -846,7 +725,6 @@ impl<W: Write> StreamOutput<'_, W> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
fn stats_config(continuous: bool) -> StatsConfig {
|
||||
if continuous {
|
||||
StatsConfig::continuous()
|
||||
@@ -854,18 +732,8 @@ fn stats_config(continuous: bool) -> StatsConfig {
|
||||
StatsConfig::finite()
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "stats")]
|
||||
fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option<String> {
|
||||
const MIB: u64 = 1024 * 1024;
|
||||
|
||||
const GIB: u64 = 1024 * MIB;
|
||||
|
||||
const FINITE_STATS_MEMORY_WARNING_BYTES: u64 = 128 * MIB;
|
||||
|
||||
const FINITE_STATS_MEMORY_STRONG_WARNING_BYTES: u64 = 512 * MIB;
|
||||
|
||||
const FINITE_STATS_MEMORY_VERY_STRONG_WARNING_BYTES: u64 = GIB;
|
||||
if args.no_stats || args.no_test || args.is_continuous() || args.duration.is_zero() {
|
||||
if args.is_continuous() || args.duration.is_zero() {
|
||||
return None;
|
||||
}
|
||||
let target_count = u64::try_from(target_count).unwrap_or(u64::MAX);
|
||||
@@ -892,8 +760,3 @@ fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option
|
||||
};
|
||||
Some(format!("irtt-rs: warning: finite exact statistics may retain about {formatted} for this run; {guidance}"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "stats")]
|
||||
fn reconnect_stats_warning(args: &ClientArgs) -> Option<&'static str> {
|
||||
(args.reconnect && !args.no_stats).then_some("irtt-rs: warning: statistics spanning multiple sessions may not accurately represent one uninterrupted measurement run; use --no-stats for measurement events only. Session statistics reset on reconnect.")
|
||||
}
|
||||
@@ -57,13 +57,12 @@ pub(crate) fn format_ns_i128(value: i128) -> String {
|
||||
}
|
||||
|
||||
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
||||
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||
pub(crate) fn format_optional_ns_i128(value: Option<i128>) -> String {
|
||||
value.map(format_ns_i128).unwrap_or_else(absent)
|
||||
}
|
||||
|
||||
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
||||
#[cfg(all(feature = "client", feature = "stats"))]
|
||||
#[cfg(feature = "client")]
|
||||
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> String {
|
||||
value.map(format_ns_f64).unwrap_or_else(absent)
|
||||
}
|
||||
@@ -81,7 +80,6 @@ pub(crate) fn format_signed_duration(value: SignedDuration) -> String {
|
||||
|
||||
/// Format a percentage with two decimals; non-finite values render as
|
||||
/// [`ABSENT`].
|
||||
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||
pub(crate) fn format_percent(value: f64) -> String {
|
||||
if value.is_finite() {
|
||||
format!("{value:.2}%")
|
||||
@@ -114,7 +112,6 @@ pub(crate) fn format_optional_count(value: Option<u64>) -> String {
|
||||
value.map(format_count).unwrap_or_else(absent)
|
||||
}
|
||||
|
||||
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||
fn absent() -> String {
|
||||
ABSENT.to_owned()
|
||||
}
|
||||
@@ -12,10 +12,9 @@ const DEFAULT_PORT: u16 = 2112;
|
||||
// Every policy option is optional. An omitted option leaves the corresponding
|
||||
// `ServerConfig` default in place rather than restating it here, so library and
|
||||
// CLI defaults cannot drift apart.
|
||||
#[derive(Clone, Parser)]
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(
|
||||
name = "irtt-server",
|
||||
version,
|
||||
about = "Minimal IRTT-compatible UDP server",
|
||||
after_help = "With no --bind, the server listens on the wildcard IRTT port for both address families ([::]:2112 and 0.0.0.0:2112) where the platform supports wildcard reply-source selection.\nRepeat --bind to serve multiple addresses in one process; any explicit --bind replaces the default pair entirely. All listeners use the same policy but keep independent session namespaces."
|
||||
)]
|
||||
@@ -29,14 +28,13 @@ pub struct ServerArgs {
|
||||
)]
|
||||
pub bind: Vec<SocketAddr>,
|
||||
|
||||
// Decode during configuration preparation so Clap cannot echo rejected keys.
|
||||
/// HMAC key; every request must then carry a valid MAC.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "KEY",
|
||||
value_parser = parse_hmac_key,
|
||||
env = "IRTT_SERVER_HMAC",
|
||||
hide_env_values = true,
|
||||
long_help = "HMAC key: literal UTF-8 or lowercase 0x followed by hexadecimal bytes (digits may use either case). Empty keys and invalid hex are rejected.\n\nAuthentication is global: with a key configured, every request must carry a valid MAC and every reply is authenticated. Without one, authenticated requests are dropped. The key is visible in the process arguments, or in the environment as IRTT_SERVER_HMAC."
|
||||
long_help = "HMAC key, taken as the UTF-8 bytes of this argument.\n\nAuthentication is global: with a key configured, every request must carry a valid MAC and every reply is authenticated. Without one, authenticated requests are dropped. The key is visible in the process arguments, or in the environment as IRTT_SERVER_HMAC."
|
||||
)]
|
||||
pub hmac: Option<String>,
|
||||
|
||||
@@ -128,23 +126,6 @@ pub struct ServerArgs {
|
||||
pub no_dscp: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ServerArgs {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServerArgs")
|
||||
.field("hmac", &self.hmac.as_ref().map(|_| "[REDACTED]"))
|
||||
.field("bind", &self.bind)
|
||||
.field("max_sessions", &self.max_sessions)
|
||||
.field("max_packet_length", &self.max_packet_length)
|
||||
.field("min_interval", &self.min_interval)
|
||||
.field("burst", &self.burst)
|
||||
.field("idle_timeout", &self.idle_timeout)
|
||||
.field("max_duration", &self.max_duration)
|
||||
.field("timestamp_allowance", &self.timestamp_allowance)
|
||||
.field("no_dscp", &self.no_dscp)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// The command-line spelling of [`TimestampAllowance`].
|
||||
///
|
||||
/// A CLI-local enum rather than a `clap` derive on the library type: `irtt-server`
|
||||
@@ -185,10 +166,11 @@ impl ServerArgs {
|
||||
}
|
||||
|
||||
/// Builds the server configuration this invocation asks for.
|
||||
pub fn server_config(&self) -> Result<ServerConfig, String> {
|
||||
#[must_use]
|
||||
pub fn server_config(&self) -> ServerConfig {
|
||||
let mut config = ServerConfig::default();
|
||||
if let Some(key) = self.hmac.as_ref() {
|
||||
config = config.with_hmac_key(crate::shared::decode_hmac_key(key)?);
|
||||
config = config.with_hmac_key(key.as_bytes());
|
||||
}
|
||||
if let Some(value) = self.max_sessions {
|
||||
config = config.with_max_sessions(value);
|
||||
@@ -216,10 +198,19 @@ impl ServerArgs {
|
||||
if self.no_dscp {
|
||||
config = config.with_dscp_allowed(false);
|
||||
}
|
||||
Ok(config)
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Rejects an empty HMAC key so a blank shell expansion cannot silently
|
||||
/// enable authentication with predictable, empty key material.
|
||||
fn parse_hmac_key(input: &str) -> Result<String, String> {
|
||||
if input.is_empty() {
|
||||
return Err("HMAC key must not be empty".to_owned());
|
||||
}
|
||||
Ok(input.to_owned())
|
||||
}
|
||||
|
||||
/// Parses a duration whose zero is meaningful server policy.
|
||||
fn parse_duration_allow_zero(input: &str) -> Result<Duration, String> {
|
||||
if input == "0" {
|
||||
|
||||
@@ -36,7 +36,7 @@ async fn serve(
|
||||
args: ServerArgs,
|
||||
shutdown_requested: &AtomicBool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = args.server_config()?;
|
||||
let config = args.server_config();
|
||||
let binds = args.resolve_binds();
|
||||
let is_implicit_default = args.bind.is_empty();
|
||||
let set = match ServerSet::bind(binds.clone(), config.clone()).await {
|
||||
|
||||
@@ -10,11 +10,7 @@ use crate::shared::client::{
|
||||
pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(
|
||||
name = "irtt-tui",
|
||||
version,
|
||||
about = "Minimal IRTT-compatible TUI client"
|
||||
)]
|
||||
#[command(name = "irtt-tui", about = "Minimal IRTT-compatible TUI client")]
|
||||
pub struct TuiArgs {
|
||||
/// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.
|
||||
#[arg(
|
||||
@@ -22,7 +18,7 @@ pub struct TuiArgs {
|
||||
num_args = 1..,
|
||||
required = true,
|
||||
value_parser = parse_target,
|
||||
long_help = "Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode. Keys use literal UTF-8 or lowercase 0x followed by hexadecimal bytes.\n\nExplicit labels are used in the legend and status table. A target without @hmac= inherits --hmac; @hmac= disables it for that target.\n\nExamples:\n irtt-tui host.example\n irtt-tui eu=host.example\n irtt-tui eu=host-a.example@hmac=secret us=host-b.example"
|
||||
long_help = "Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.\n\nExplicit labels are used in the legend and status table. A target without @hmac= inherits --hmac; @hmac= disables it for that target.\n\nExamples:\n irtt-tui host.example\n irtt-tui eu=host.example\n irtt-tui eu=host-a.example@hmac=secret us=host-b.example"
|
||||
)]
|
||||
pub targets: Vec<TargetArg>,
|
||||
|
||||
@@ -37,18 +33,13 @@ pub struct TuiArgs {
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
short = 'd',
|
||||
default_value = "0",
|
||||
value_parser = parse_test_duration,
|
||||
help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)",
|
||||
long_help = "Test duration (e.g. 2.5s, 1m30s, 500us, 100ns, 1h); use --continuous for continuous mode (0 is also accepted). The TUI defaults to continuous mode."
|
||||
help = "Test duration; use 0 for continuous mode",
|
||||
long_help = "Test duration; use 0 for continuous mode. The TUI defaults to continuous mode."
|
||||
)]
|
||||
pub duration: Duration,
|
||||
|
||||
/// Run continuously until interrupted (equivalent to --duration 0).
|
||||
#[arg(long, conflicts_with = "duration")]
|
||||
pub continuous: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub common: CommonClientArgs,
|
||||
}
|
||||
@@ -61,11 +52,7 @@ impl TuiArgs {
|
||||
pub async fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
||||
prepare_managed_run(
|
||||
&self.common,
|
||||
if self.continuous {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
self.duration
|
||||
},
|
||||
self.duration,
|
||||
TargetSelection {
|
||||
targets: &self.targets,
|
||||
pacing: self.pacing,
|
||||
@@ -76,7 +63,7 @@ impl TuiArgs {
|
||||
}
|
||||
|
||||
pub fn is_continuous(&self) -> bool {
|
||||
self.continuous || self.duration == Duration::ZERO
|
||||
self.duration == Duration::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,10 +89,11 @@ fn with_shutdown_flag(
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn run_client_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use clap::Parser;
|
||||
|
||||
let shutdown = crate::signal::install_async_signal_handler()
|
||||
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
||||
let args =
|
||||
crate::version::parse::<crate::cmd::client::ClientArgs>(argv, crate::version::client);
|
||||
let args = crate::cmd::client::ClientArgs::parse_from(argv);
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
@@ -106,9 +107,10 @@ fn run_client_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Err
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn run_server_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use clap::Parser;
|
||||
|
||||
with_shutdown_flag(|shutdown_requested| {
|
||||
let args =
|
||||
crate::version::parse::<crate::cmd::server::ServerArgs>(argv, crate::version::server);
|
||||
let args = crate::cmd::server::ServerArgs::parse_from(argv);
|
||||
crate::cmd::server::run_server(args, shutdown_requested)
|
||||
})
|
||||
}
|
||||
@@ -120,9 +122,11 @@ fn run_server_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Err
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
fn run_tui_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use clap::Parser;
|
||||
|
||||
let shutdown = crate::signal::install_async_signal_handler()
|
||||
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
||||
let args = crate::version::parse::<crate::cmd::tui::TuiArgs>(argv, crate::version::client);
|
||||
let args = crate::cmd::tui::TuiArgs::parse_from(argv);
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
|
||||
@@ -6,5 +6,3 @@ pub mod cmd;
|
||||
pub mod entry;
|
||||
pub mod shared;
|
||||
pub mod signal;
|
||||
|
||||
mod version;
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fmt, net::IpAddr, net::SocketAddr, time::Duration};
|
||||
use std::{fmt, time::Duration};
|
||||
|
||||
use clap::ValueEnum;
|
||||
use irtt_client::{MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH};
|
||||
@@ -18,21 +18,16 @@ pub struct CommonClientArgs {
|
||||
#[arg(long, conflicts_with_all = ["ipv4", "ipv6"], long_help = "Probe available IPv4 and IPv6 paths separately for each hostname. Labels gain /v4 or /v6; IP literals remain single targets. Families are discovered when a target is added; reconnects resolve the original hostname again within the chosen family.")]
|
||||
pub dual_stack: bool,
|
||||
|
||||
/// Positive probe interval (e.g. 2.5s, 1m30s, 500us, 100ns, 1h).
|
||||
#[arg(short = 'i', long, default_value = "1s", value_parser = parse_duration)]
|
||||
/// Probe interval.
|
||||
#[arg(long, default_value = "1s", value_parser = parse_duration)]
|
||||
pub interval: Duration,
|
||||
|
||||
/// Comma-separated session-opening timeouts, one per attempt (each >= 200ms).
|
||||
#[arg(long, value_name = "DURATIONS", value_delimiter = ',', value_parser = parse_duration, long_help = "Comma-separated session-opening receive timeouts, one per attempt (each at least 200ms), e.g. 1s,2s,4s,8s. Omit to use the library default retry schedule.")]
|
||||
pub timeouts: Vec<Duration>,
|
||||
|
||||
/// UDP payload length.
|
||||
#[arg(short = 'l', long, default_value_t = 0, value_parser = parse_length)]
|
||||
#[arg(long, default_value_t = 0, value_parser = parse_length)]
|
||||
pub length: u32,
|
||||
|
||||
// Decode during configuration preparation so Clap cannot echo rejected keys.
|
||||
/// HMAC key: literal UTF-8 or lowercase 0x followed by hexadecimal bytes.
|
||||
#[arg(long)]
|
||||
/// HMAC key.
|
||||
#[arg(long, value_parser = parse_hmac_key)]
|
||||
pub hmac: Option<String>,
|
||||
|
||||
/// Clock mode to request.
|
||||
@@ -75,38 +70,6 @@ pub struct CommonClientArgs {
|
||||
#[arg(long, value_name = "1..=255", value_parser = parse_ttl)]
|
||||
pub ttl: Option<u32>,
|
||||
|
||||
#[arg(
|
||||
long = "bind-addr",
|
||||
value_name = "IP[:PORT]",
|
||||
value_parser = parse_bind_addr,
|
||||
long_help = "Local IP address and optional UDP port to bind. \
|
||||
If no port is specified, the operating system selects \
|
||||
an ephemeral port. IPv6 addresses with an explicit \
|
||||
port must use [ADDR]:PORT syntax."
|
||||
)]
|
||||
pub bind_addr: Option<SocketAddr>,
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
#[arg(
|
||||
long = "bind-device",
|
||||
value_name = "DEVICE",
|
||||
help = "Bind the socket to a network device"
|
||||
)]
|
||||
pub bind_to_device: Option<String>,
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "MARK",
|
||||
value_parser = clap_num::maybe_hex::<u32>,
|
||||
help = "Set the outgoing socket firewall mark"
|
||||
)]
|
||||
pub mark: Option<u32>,
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
#[arg(long, value_name = "FIB", help = "Set the socket routing table (FIB)")]
|
||||
pub fib: Option<u32>,
|
||||
|
||||
/// Accept safe server restrictions during negotiation.
|
||||
#[arg(long)]
|
||||
pub loose: bool,
|
||||
@@ -114,13 +77,11 @@ pub struct CommonClientArgs {
|
||||
|
||||
impl fmt::Debug for CommonClientArgs {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut debug = f.debug_struct("CommonClientArgs");
|
||||
debug
|
||||
f.debug_struct("CommonClientArgs")
|
||||
.field("ipv4", &self.ipv4)
|
||||
.field("ipv6", &self.ipv6)
|
||||
.field("dual_stack", &self.dual_stack)
|
||||
.field("interval", &self.interval)
|
||||
.field("timeouts", &self.timeouts)
|
||||
.field("length", &self.length)
|
||||
.field("hmac", &self.hmac.as_ref().map(|_| "[REDACTED]"))
|
||||
.field("clock", &self.clock)
|
||||
@@ -129,14 +90,8 @@ impl fmt::Debug for CommonClientArgs {
|
||||
.field("server_fill", &self.server_fill)
|
||||
.field("dscp", &self.dscp)
|
||||
.field("ttl", &self.ttl)
|
||||
.field("bind_addr", &self.bind_addr);
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
debug
|
||||
.field("bind_to_device", &self.bind_to_device)
|
||||
.field("mark", &self.mark);
|
||||
#[cfg(target_os = "freebsd")]
|
||||
debug.field("fib", &self.fib);
|
||||
debug.field("loose", &self.loose).finish()
|
||||
.field("loose", &self.loose)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,26 +152,63 @@ impl From<ReceivedStatsArg> for ReceivedStats {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bind_addr(input: &str) -> Result<SocketAddr, String> {
|
||||
input
|
||||
.parse::<SocketAddr>()
|
||||
.or_else(|_| input.parse::<IpAddr>().map(|ip| SocketAddr::new(ip, 0)))
|
||||
.map_err(|_| "expected an IP address, optionally with a port".to_owned())
|
||||
}
|
||||
|
||||
pub fn parse_duration(input: &str) -> Result<Duration, String> {
|
||||
let duration = parse_test_duration(input)?;
|
||||
if duration.is_zero() {
|
||||
let (number, unit) = split_duration(input)?;
|
||||
let value: u64 = number
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid duration value {input:?}"))?;
|
||||
if value == 0 {
|
||||
return Err("duration must be greater than zero".to_owned());
|
||||
}
|
||||
Ok(duration)
|
||||
match unit {
|
||||
"ms" => Ok(Duration::from_millis(value)),
|
||||
"s" => Ok(Duration::from_secs(value)),
|
||||
"m" => value
|
||||
.checked_mul(60)
|
||||
.map(Duration::from_secs)
|
||||
.ok_or_else(|| "duration is too large".to_owned()),
|
||||
_ => Err(format!(
|
||||
"unsupported duration unit {unit:?}; use ms, s, or m"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_test_duration(input: &str) -> Result<Duration, String> {
|
||||
if input == "0" {
|
||||
return Ok(Duration::ZERO);
|
||||
}
|
||||
humantime::parse_duration(input).map_err(|error| error.to_string())
|
||||
let (number, unit) = split_duration(input)?;
|
||||
let value: u64 = number
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid duration value {input:?}"))?;
|
||||
if value == 0 {
|
||||
return Ok(Duration::ZERO);
|
||||
}
|
||||
match unit {
|
||||
"ms" => Ok(Duration::from_millis(value)),
|
||||
"s" => Ok(Duration::from_secs(value)),
|
||||
"m" => value
|
||||
.checked_mul(60)
|
||||
.map(Duration::from_secs)
|
||||
.ok_or_else(|| "duration is too large".to_owned()),
|
||||
_ => Err(format!(
|
||||
"unsupported duration unit {unit:?}; use ms, s, or m"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn split_duration(input: &str) -> Result<(&str, &str), String> {
|
||||
let split = input
|
||||
.find(|ch: char| !ch.is_ascii_digit())
|
||||
.ok_or_else(|| "duration must include a unit: ms, s, or m".to_owned())?;
|
||||
let (number, unit) = input.split_at(split);
|
||||
if number.is_empty() || unit.is_empty() {
|
||||
return Err("duration must include a positive value and unit".to_owned());
|
||||
}
|
||||
if number.starts_with('-') {
|
||||
return Err("duration must be greater than zero".to_owned());
|
||||
}
|
||||
Ok((number, unit))
|
||||
}
|
||||
|
||||
pub fn parse_length(input: &str) -> Result<u32, String> {
|
||||
@@ -229,6 +221,13 @@ pub fn parse_length(input: &str) -> Result<u32, String> {
|
||||
Ok(length)
|
||||
}
|
||||
|
||||
pub fn parse_hmac_key(input: &str) -> Result<String, String> {
|
||||
if input.is_empty() {
|
||||
return Err("HMAC key must not be empty".to_owned());
|
||||
}
|
||||
Ok(input.to_owned())
|
||||
}
|
||||
|
||||
pub fn parse_server_fill(input: &str) -> Result<String, String> {
|
||||
if input.is_empty() {
|
||||
return Err("server fill must not be empty".to_owned());
|
||||
|
||||
@@ -21,22 +21,17 @@ impl CommonClientArgs {
|
||||
/// Build the shared client configuration template for a managed run.
|
||||
///
|
||||
/// The config is endpoint-independent; managed targets own their addresses.
|
||||
pub fn to_client_config(&self, duration: Duration) -> Result<ClientConfig, String> {
|
||||
let mut open = OpenPolicy {
|
||||
negotiation: if self.loose {
|
||||
NegotiationPolicy::Loose
|
||||
} else {
|
||||
NegotiationPolicy::Strict
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
if !self.timeouts.is_empty() {
|
||||
open.timeouts.clone_from(&self.timeouts);
|
||||
}
|
||||
|
||||
Ok(ClientConfig {
|
||||
pub fn to_client_config(&self, duration: Duration) -> ClientConfig {
|
||||
ClientConfig {
|
||||
address_family: self.address_family(),
|
||||
open,
|
||||
open: OpenPolicy {
|
||||
negotiation: if self.loose {
|
||||
NegotiationPolicy::Loose
|
||||
} else {
|
||||
NegotiationPolicy::Strict
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
request: SessionRequest {
|
||||
duration: (!duration.is_zero()).then_some(duration),
|
||||
interval: self.interval,
|
||||
@@ -50,24 +45,16 @@ impl CommonClientArgs {
|
||||
},
|
||||
auth: self
|
||||
.hmac
|
||||
.as_deref()
|
||||
.map(crate::shared::decode_hmac_key)
|
||||
.transpose()?
|
||||
.as_ref()
|
||||
.map_or(Authentication::Unauthenticated, |key| {
|
||||
Authentication::Hmac(HmacKey::new(key))
|
||||
Authentication::Hmac(HmacKey::new(key.as_bytes()))
|
||||
}),
|
||||
socket: SocketConfig {
|
||||
bind_addr: self.bind_addr,
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
bind_to_device: self.bind_to_device.clone(),
|
||||
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
|
||||
mark: self.mark,
|
||||
#[cfg(target_os = "freebsd")]
|
||||
fib: self.fib,
|
||||
ttl: self.ttl,
|
||||
..SocketConfig::default()
|
||||
},
|
||||
..ClientConfig::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ pub async fn prepare_managed_run(
|
||||
.await?;
|
||||
Ok(ManagedRunSetup {
|
||||
targets,
|
||||
client: common.to_client_config(duration)?,
|
||||
client: common.to_client_config(duration),
|
||||
pacing: selection.pacing.into(),
|
||||
stdin_controlled: selection.stdin_controlled,
|
||||
preparation,
|
||||
|
||||
@@ -88,13 +88,12 @@ impl From<GroupPacingArg> for ManagedPacing {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum TargetParseError {
|
||||
EmptyLabel,
|
||||
EmptyAddress,
|
||||
InvalidEscape,
|
||||
EmptyTarget,
|
||||
InvalidHmac(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for TargetParseError {
|
||||
@@ -104,7 +103,6 @@ impl fmt::Display for TargetParseError {
|
||||
Self::EmptyAddress => "target address must not be empty",
|
||||
Self::InvalidEscape => "invalid target escape sequence",
|
||||
Self::EmptyTarget => "target must not be empty",
|
||||
Self::InvalidHmac(message) => message,
|
||||
};
|
||||
f.write_str(message)
|
||||
}
|
||||
@@ -199,10 +197,7 @@ fn parse_target_syntax(
|
||||
let auth = if value.is_empty() {
|
||||
TargetAuth::Override(Authentication::Unauthenticated)
|
||||
} else {
|
||||
TargetAuth::Override(Authentication::Hmac(HmacKey::new(
|
||||
crate::shared::decode_hmac_key(&value)
|
||||
.map_err(TargetParseError::InvalidHmac)?,
|
||||
)))
|
||||
TargetAuth::Override(Authentication::Hmac(HmacKey::new(value.into_bytes())))
|
||||
};
|
||||
(&input[..index], auth)
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/// Decode CLI key syntax without ever including key material in diagnostics.
|
||||
pub(crate) fn decode_hmac_key(input: &str) -> Result<Vec<u8>, String> {
|
||||
let key = match input.strip_prefix("0x") {
|
||||
Some(hex) => hex::decode(hex).map_err(|_| {
|
||||
"HMAC hexadecimal key must contain an even number of valid hexadecimal digits"
|
||||
.to_owned()
|
||||
})?,
|
||||
None => input.as_bytes().to_vec(),
|
||||
};
|
||||
if key.is_empty() {
|
||||
return Err("HMAC key must not be empty".to_owned());
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
@@ -1,7 +1,2 @@
|
||||
#[cfg(any(feature = "client", feature = "tui"))]
|
||||
pub mod client;
|
||||
|
||||
#[cfg(any(feature = "client", feature = "tui", feature = "server"))]
|
||||
mod hmac;
|
||||
#[cfg(any(feature = "client", feature = "tui", feature = "server"))]
|
||||
pub(crate) use hmac::decode_hmac_key;
|
||||
@@ -1,44 +0,0 @@
|
||||
#[cfg(any(feature = "client", feature = "tui", feature = "server"))]
|
||||
use std::ffi::OsString;
|
||||
|
||||
pub(crate) fn app() -> String {
|
||||
clap::Command::new("irtt-rs")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.render_version()
|
||||
}
|
||||
|
||||
// Preserve Clap's version banner and append the compiled component versions.
|
||||
#[cfg(any(feature = "client", feature = "tui", feature = "server"))]
|
||||
pub(crate) fn parse<T: clap::Parser>(argv: Vec<OsString>, report: fn() -> String) -> T {
|
||||
match T::try_parse_from(argv) {
|
||||
Ok(args) => args,
|
||||
Err(err) if err.kind() == clap::error::ErrorKind::DisplayVersion => {
|
||||
print!("{}{}", err.render().ansi(), report());
|
||||
std::process::exit(0)
|
||||
}
|
||||
Err(err) => err.exit(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "client", feature = "tui"))]
|
||||
pub(crate) fn client() -> String {
|
||||
#[cfg(feature = "stats")]
|
||||
let stats = format!(" stats: {}\n", irtt_stats::VERSION);
|
||||
#[cfg(not(feature = "stats"))]
|
||||
let stats = " stats: disabled\n";
|
||||
format!(
|
||||
" client: {}\n{} proto: {}\n",
|
||||
irtt_client::VERSION,
|
||||
stats,
|
||||
irtt_proto::VERSION,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
pub(crate) fn server() -> String {
|
||||
format!(
|
||||
" server: {}\n proto: {}\n",
|
||||
irtt_server::VERSION,
|
||||
irtt_proto::VERSION,
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords = ["irtt", "networking", "latency", "rtt"]
|
||||
categories = ["network-programming"]
|
||||
@@ -14,7 +15,7 @@ categories = ["network-programming"]
|
||||
irtt-proto.workspace = true
|
||||
socket2 = { version = "0.6.5", features = ["all"] }
|
||||
thiserror = "2.0.21"
|
||||
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync"], optional = true}
|
||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync"], optional = true}
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
nix = { version = "0.31.3", default-features = false, features = ["net", "uio"], optional = true }
|
||||
|
||||
@@ -93,6 +93,8 @@ pub(crate) struct AsyncOpenState {
|
||||
deadline: Option<Instant>,
|
||||
deadline_timer: Option<Pin<Box<time::Sleep>>>,
|
||||
request_submitted: bool,
|
||||
stop_after_current_attempt: bool,
|
||||
stopped_after_current_attempt: bool,
|
||||
buffer: [u8; MAX_OPEN_PACKET_SIZE],
|
||||
cleanup: Option<AsyncOpenCleanup>,
|
||||
}
|
||||
@@ -104,6 +106,8 @@ impl AsyncOpenState {
|
||||
deadline: None,
|
||||
deadline_timer: None,
|
||||
request_submitted: false,
|
||||
stop_after_current_attempt: false,
|
||||
stopped_after_current_attempt: false,
|
||||
buffer: [0_u8; MAX_OPEN_PACKET_SIZE],
|
||||
cleanup: None,
|
||||
}
|
||||
@@ -124,12 +128,21 @@ impl AsyncOpenState {
|
||||
self.deadline = None;
|
||||
self.deadline_timer = None;
|
||||
self.request_submitted = false;
|
||||
self.stopped_after_current_attempt = self.stop_after_current_attempt;
|
||||
}
|
||||
|
||||
pub(crate) fn has_in_flight_work(&self) -> bool {
|
||||
self.request_submitted || self.cleanup.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn request_stop_after_current_attempt(&mut self) {
|
||||
self.stop_after_current_attempt = true;
|
||||
}
|
||||
|
||||
pub(crate) fn stopped_after_current_attempt(&self) -> bool {
|
||||
self.stopped_after_current_attempt
|
||||
}
|
||||
|
||||
fn deadline(&self) -> Instant {
|
||||
self.deadline
|
||||
.expect("an open attempt has an active deadline")
|
||||
@@ -311,6 +324,10 @@ impl AsyncClient {
|
||||
if state.attempt >= attempt_count {
|
||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
||||
}
|
||||
if state.stop_after_current_attempt && state.deadline.is_none() {
|
||||
state.stopped_after_current_attempt = true;
|
||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
||||
}
|
||||
if state.deadline.is_none() {
|
||||
let timeout = self.machine.config().open.timeouts[state.attempt];
|
||||
if let Err(error) = state.start_attempt(timeout) {
|
||||
@@ -351,6 +368,9 @@ impl AsyncClient {
|
||||
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
||||
Poll::Ready(Ok(false)) => {
|
||||
state.finish_attempt();
|
||||
if state.stopped_after_current_attempt() {
|
||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Poll::Ready(Ok(true)) => {}
|
||||
@@ -376,6 +396,9 @@ impl AsyncClient {
|
||||
};
|
||||
if datagram.received_at.mono > state.deadline() {
|
||||
state.finish_attempt();
|
||||
if state.stopped_after_current_attempt() {
|
||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -755,16 +778,6 @@ impl AsyncClient {
|
||||
.map_err(ClientError::Socket)
|
||||
}
|
||||
|
||||
/// Cancel opening without waiting for a reply or socket writability.
|
||||
/// Retained post-token cleanup gets one best-effort nonblocking send.
|
||||
pub(crate) fn cancel_open(&self, state: &mut AsyncOpenState) -> Option<ClientError> {
|
||||
let cleanup = state.cleanup.take()?;
|
||||
if let Some(packet) = cleanup.packet() {
|
||||
let _ = self.socket.try_send(packet);
|
||||
}
|
||||
Some(cleanup.into_primary())
|
||||
}
|
||||
|
||||
fn poll_open_cleanup(
|
||||
&self,
|
||||
state: &mut AsyncOpenState,
|
||||
|
||||
@@ -47,9 +47,6 @@
|
||||
//!
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
/// Cargo package version of this compiled library.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
mod async_client;
|
||||
mod auth;
|
||||
|
||||
@@ -101,15 +101,6 @@ pub struct ManagedClientConfig {
|
||||
pub outcome_history_limit: usize,
|
||||
pub max_live_target_generations: usize,
|
||||
pub final_drain: Duration,
|
||||
/// Retry failed/closed sessions and detect silence while sending. Only valid
|
||||
/// for continuous measurement runs. None (the default) disables reconnection.
|
||||
pub reconnect_timeout: Option<Duration>,
|
||||
/// First retry delay, also restored after a valid reply (default: 250 ms).
|
||||
/// Must be positive and at most half of `reconnect_max_delay`.
|
||||
pub reconnect_initial_delay: Duration,
|
||||
/// Cap for per-target exponential backoff with a fixed multiplier of two
|
||||
/// (default: 10 seconds). Must be schedulable from the current instant.
|
||||
pub reconnect_max_delay: Duration,
|
||||
}
|
||||
|
||||
impl Default for ManagedClientConfig {
|
||||
@@ -123,9 +114,6 @@ impl Default for ManagedClientConfig {
|
||||
outcome_history_limit: DEFAULT_MANAGED_OUTCOME_HISTORY_LIMIT,
|
||||
max_live_target_generations: DEFAULT_MANAGED_MAX_LIVE_TARGET_GENERATIONS,
|
||||
final_drain: DEFAULT_MANAGED_FINAL_DRAIN,
|
||||
reconnect_timeout: None,
|
||||
reconnect_initial_delay: Duration::from_millis(250),
|
||||
reconnect_max_delay: Duration::from_secs(10),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,13 +364,6 @@ pub struct ManagedTargetFailure {
|
||||
/// Configuration failure detected before any runtime work begins.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ManagedConfigError {
|
||||
#[error("reconnection requires continuous measurement mode and a positive, schedulable silence timeout")]
|
||||
InvalidReconnectPolicy,
|
||||
#[error("reconnection delays must be positive and schedulable, with maximum delay at least twice the initial delay (initial: {initial:?}, maximum: {maximum:?})")]
|
||||
InvalidReconnectDelays {
|
||||
initial: Duration,
|
||||
maximum: Duration,
|
||||
},
|
||||
#[error("FinishWhenQuiescent requires at least one initial target")]
|
||||
EmptyInitialTargets,
|
||||
#[error("duplicate managed target id {id}")]
|
||||
|
||||
@@ -327,14 +327,7 @@ impl ManagedClientTask {
|
||||
let mut non_send_deadline = None;
|
||||
let mut send_deadline = None;
|
||||
for target in &self.targets {
|
||||
non_send_deadline = non_send_deadline
|
||||
.into_iter()
|
||||
.chain(target.silence_deadline(self.config.reconnect_timeout))
|
||||
.min();
|
||||
match target.phase() {
|
||||
TargetPhase::Retrying { deadline } => {
|
||||
non_send_deadline = non_send_deadline.into_iter().chain(Some(*deadline)).min();
|
||||
}
|
||||
TargetPhase::Active {
|
||||
client,
|
||||
schedule,
|
||||
|
||||
@@ -367,35 +367,6 @@ fn build_task(
|
||||
maximum: tokio::sync::Semaphore::MAX_PERMITS,
|
||||
});
|
||||
}
|
||||
if config.reconnect_initial_delay.is_zero()
|
||||
|| config.reconnect_max_delay.is_zero()
|
||||
|| config
|
||||
.reconnect_initial_delay
|
||||
.checked_mul(2)
|
||||
.is_none_or(|minimum| config.reconnect_max_delay < minimum)
|
||||
// Tokio represents timer deadlines in u64 milliseconds, reserving the
|
||||
// top two values for state. Leave room for its millisecond rounding.
|
||||
|| config.reconnect_max_delay.as_millis() >= u128::from(u64::MAX - 2)
|
||||
|| config
|
||||
.reconnect_max_delay
|
||||
.checked_add(std::time::Duration::from_millis(1))
|
||||
.and_then(|delay| Instant::now().checked_add(delay))
|
||||
.is_none()
|
||||
{
|
||||
return Err(ManagedConfigError::InvalidReconnectDelays {
|
||||
initial: config.reconnect_initial_delay,
|
||||
maximum: config.reconnect_max_delay,
|
||||
});
|
||||
}
|
||||
if let Some(timeout) = config.reconnect_timeout {
|
||||
if timeout.is_zero()
|
||||
|| Instant::now().checked_add(timeout).is_none()
|
||||
|| config.client.request.duration.is_some()
|
||||
|| config.client.request.run_mode != crate::RunMode::Normal
|
||||
{
|
||||
return Err(ManagedConfigError::InvalidReconnectPolicy);
|
||||
}
|
||||
}
|
||||
if config.max_live_target_generations == 0 {
|
||||
return Err(ManagedConfigError::ZeroLiveGenerationLimit);
|
||||
}
|
||||
@@ -446,10 +417,6 @@ fn build_task(
|
||||
server_addr: Arc::from(target.server_addr),
|
||||
remote: None,
|
||||
counters: TargetCounters::default(),
|
||||
sent_before_session: 0,
|
||||
retry_delay: config.reconnect_initial_delay,
|
||||
silence_since: None,
|
||||
last_reply_at: None,
|
||||
phase: Some(TargetPhase::Pending { client_config }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,12 +206,6 @@ impl ManagedClientTask {
|
||||
};
|
||||
self.targets[index].sync_packets_sent(&client);
|
||||
send_waiting = result.is_pending();
|
||||
if send_waiting {
|
||||
// A blocked sender is not evidence of an unresponsive peer. Arm
|
||||
// a fresh silence period only after transmission resumes.
|
||||
self.targets[index].silence_since = None;
|
||||
self.targets[index].last_reply_at = None;
|
||||
}
|
||||
self.targets[index].restore_phase(TargetPhase::Active {
|
||||
client,
|
||||
schedule,
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
time::{Duration, Instant},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -27,9 +27,6 @@ type ConnectFuture =
|
||||
Pin<Box<dyn Future<Output = Result<AsyncClient, ClientError>> + Send + 'static>>;
|
||||
|
||||
pub(super) enum TargetPhase {
|
||||
Retrying {
|
||||
deadline: Instant,
|
||||
},
|
||||
Pending {
|
||||
client_config: ClientConfig,
|
||||
},
|
||||
@@ -68,7 +65,6 @@ pub(super) enum TargetPhase {
|
||||
impl TargetPhase {
|
||||
pub(super) fn lifecycle(&self) -> ManagedTargetLifecycle {
|
||||
match self {
|
||||
Self::Retrying { .. } => ManagedTargetLifecycle::Pending,
|
||||
Self::Pending { .. } => ManagedTargetLifecycle::Pending,
|
||||
Self::Connecting { .. } => ManagedTargetLifecycle::Connecting,
|
||||
Self::Opening { .. } => ManagedTargetLifecycle::Opening,
|
||||
@@ -186,30 +182,11 @@ pub(super) struct TargetRuntime {
|
||||
pub(super) server_addr: Arc<str>,
|
||||
pub(super) remote: Option<std::net::SocketAddr>,
|
||||
pub(super) counters: TargetCounters,
|
||||
pub(super) sent_before_session: u64,
|
||||
pub(super) retry_delay: Duration,
|
||||
pub(super) silence_since: Option<Instant>,
|
||||
pub(super) last_reply_at: Option<Instant>,
|
||||
// None only while a synchronous operation owns the phase locally.
|
||||
pub(super) phase: Option<TargetPhase>,
|
||||
}
|
||||
|
||||
impl TargetRuntime {
|
||||
pub(super) fn silence_deadline(&self, timeout: Option<Duration>) -> Option<Instant> {
|
||||
let TargetPhase::Active {
|
||||
schedule,
|
||||
send_waiting,
|
||||
..
|
||||
} = self.phase()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if *send_waiting || schedule.is_finished() {
|
||||
return None;
|
||||
}
|
||||
self.silence_since?.checked_add(timeout?)
|
||||
}
|
||||
|
||||
pub(super) fn phase(&self) -> &TargetPhase {
|
||||
self.phase
|
||||
.as_ref()
|
||||
@@ -241,18 +218,16 @@ impl TargetRuntime {
|
||||
self.membership.is_desired() && matches!(phase, TargetPhase::Active { .. })
|
||||
}
|
||||
|
||||
/// Combine the current client's authoritative sent count with retired
|
||||
/// sessions, so one target generation keeps durable accounting on retries.
|
||||
/// Adopt the underlying client's authoritative sent count.
|
||||
///
|
||||
/// The client owns `packets_sent`; this runtime only mirrors it so a
|
||||
/// terminal outcome can report it after the client is gone.
|
||||
pub(super) fn sync_packets_sent(&mut self, client: &AsyncClient) {
|
||||
self.counters.packets_sent = self
|
||||
.sent_before_session
|
||||
.saturating_add(client.packets_sent());
|
||||
self.counters.packets_sent = client.packets_sent();
|
||||
}
|
||||
|
||||
pub(super) fn sync_live_packets_sent(&mut self) {
|
||||
self.counters.packets_sent = self
|
||||
.sent_before_session
|
||||
.saturating_add(self.phase().client().packets_sent());
|
||||
self.counters.packets_sent = self.phase().client().packets_sent();
|
||||
}
|
||||
|
||||
/// Build this target's durable outcome.
|
||||
@@ -309,30 +284,6 @@ impl ManagedClientTask {
|
||||
|
||||
pub(super) fn publish_client_events(&mut self, index: usize, events: Vec<ClientEvent>) {
|
||||
for event in events {
|
||||
match &event {
|
||||
ClientEvent::EchoSent { sent_at, .. } => {
|
||||
let target = &mut self.targets[index];
|
||||
let anchor = target
|
||||
.last_reply_at
|
||||
.filter(|reply| {
|
||||
self.config.reconnect_timeout.is_some_and(|timeout| {
|
||||
sent_at.mono.duration_since(*reply) < timeout
|
||||
})
|
||||
})
|
||||
.unwrap_or(sent_at.mono);
|
||||
target.silence_since.get_or_insert(anchor);
|
||||
}
|
||||
ClientEvent::EchoReply { received_at, .. }
|
||||
| ClientEvent::LateReply { received_at, .. }
|
||||
| ClientEvent::DuplicateReply { received_at, .. } => {
|
||||
// A reply disarms silence until another probe is committed.
|
||||
// A healthy sender with a long interval can remain idle.
|
||||
self.targets[index].silence_since = None;
|
||||
self.targets[index].last_reply_at = Some(received_at.mono);
|
||||
self.targets[index].retry_delay = self.config.reconnect_initial_delay;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.targets[index].counters.observe(&event);
|
||||
self.publish_event(ManagedEvent::Client {
|
||||
target: self.targets[index].instance.clone(),
|
||||
@@ -344,33 +295,9 @@ impl ManagedClientTask {
|
||||
fn finish_target(
|
||||
&mut self,
|
||||
index: usize,
|
||||
mut end_reason: ManagedTargetEndReason,
|
||||
end_reason: ManagedTargetEndReason,
|
||||
cleanup_failure: Option<ManagedTargetFailure>,
|
||||
) {
|
||||
if self.can_reconnect(index)
|
||||
&& matches!(
|
||||
end_reason,
|
||||
ManagedTargetEndReason::Failed(_)
|
||||
| ManagedTargetEndReason::PeerClosed
|
||||
| ManagedTargetEndReason::TestComplete
|
||||
)
|
||||
{
|
||||
let target = &mut self.targets[index];
|
||||
target.sent_before_session = target.counters.packets_sent;
|
||||
target.silence_since = None;
|
||||
target.last_reply_at = None;
|
||||
if let Some(deadline) = Instant::now().checked_add(target.retry_delay) {
|
||||
target.retry_delay = target
|
||||
.retry_delay
|
||||
.checked_mul(2)
|
||||
.unwrap_or(self.config.reconnect_max_delay)
|
||||
.min(self.config.reconnect_max_delay);
|
||||
self.install_target_phase(index, TargetPhase::Retrying { deadline });
|
||||
return;
|
||||
}
|
||||
// A delay validated at construction may no longer fit after a long run.
|
||||
end_reason = ManagedTargetEndReason::Failed(duration_overflow_failure());
|
||||
}
|
||||
let outcome = Arc::new(self.targets[index].outcome(end_reason, cleanup_failure));
|
||||
// Finalize history before the phase publisher installs Terminal, updates
|
||||
// pacing and publishes one complete status followed by the lifecycle event.
|
||||
@@ -384,12 +311,6 @@ impl ManagedClientTask {
|
||||
self.publish_event(ManagedEvent::TargetFinished { outcome });
|
||||
}
|
||||
|
||||
fn can_reconnect(&self, index: usize) -> bool {
|
||||
self.config.reconnect_timeout.is_some()
|
||||
&& !self.phase.is_stopping()
|
||||
&& self.targets[index].membership.is_desired()
|
||||
}
|
||||
|
||||
fn fail_target(&mut self, index: usize, phase: ManagedTargetFailurePhase, error: ClientError) {
|
||||
let failure = classify_client_error(phase, &error);
|
||||
self.finish_target(index, ManagedTargetEndReason::Failed(failure), None);
|
||||
@@ -411,9 +332,6 @@ impl ManagedClientTask {
|
||||
};
|
||||
client.discard_prepared_probe();
|
||||
self.targets[index].sync_live_packets_sent();
|
||||
if self.can_reconnect(index) {
|
||||
return self.begin_close(index, primary_end, None, now);
|
||||
}
|
||||
match cleanup {
|
||||
OpenSessionFailureCleanup::Drain => self.begin_drain(index, primary_end, now),
|
||||
OpenSessionFailureCleanup::Close => self.begin_close(index, primary_end, None, now),
|
||||
@@ -441,27 +359,6 @@ impl ManagedClientTask {
|
||||
let retirement = self.effective_retirement(index);
|
||||
let stopping = self.phase.is_stopping() || retirement.is_some();
|
||||
match self.targets[index].phase_mut() {
|
||||
TargetPhase::Retrying { deadline } => {
|
||||
if stopping {
|
||||
self.finish_target(
|
||||
index,
|
||||
retirement.unwrap_or(ManagedTargetEndReason::Stopped),
|
||||
None,
|
||||
);
|
||||
false
|
||||
} else if now >= *deadline {
|
||||
let target = &self.targets[index].config;
|
||||
let mut client_config = self.config.client.clone();
|
||||
client_config.address_family = target
|
||||
.address_family
|
||||
.unwrap_or(client_config.address_family);
|
||||
client_config.auth = target.auth.resolve(&client_config.auth);
|
||||
self.install_target_phase(index, TargetPhase::Pending { client_config });
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
TargetPhase::Pending { .. } => {
|
||||
if stopping {
|
||||
self.finish_target(
|
||||
@@ -512,12 +409,12 @@ impl ManagedClientTask {
|
||||
let retirement = retirement
|
||||
.clone()
|
||||
.unwrap_or(ManagedTargetEndReason::Stopped);
|
||||
let cleanup_failure = client.cancel_open(open).map(|error| {
|
||||
classify_client_error(ManagedTargetFailurePhase::Opening, &error)
|
||||
});
|
||||
self.targets[index].sync_live_packets_sent();
|
||||
self.finish_target(index, retirement, cleanup_failure);
|
||||
return false;
|
||||
if !open.has_in_flight_work() {
|
||||
self.targets[index].sync_live_packets_sent();
|
||||
self.finish_target(index, retirement, None);
|
||||
return false;
|
||||
}
|
||||
open.request_stop_after_current_attempt();
|
||||
}
|
||||
match client.poll_open(open, cx) {
|
||||
Poll::Pending => false,
|
||||
@@ -541,25 +438,64 @@ impl ManagedClientTask {
|
||||
index,
|
||||
vec![ClientEvent::SessionStarted(started)],
|
||||
);
|
||||
let previous = self.targets[index].take_phase();
|
||||
self.targets[index].restore_phase(TargetPhase::Active {
|
||||
client: previous.into_client(),
|
||||
schedule,
|
||||
send_waiting: false,
|
||||
});
|
||||
self.publish_target_phase(index, ManagedTargetLifecycle::Opening);
|
||||
true
|
||||
if stopping {
|
||||
self.targets[index].sync_live_packets_sent();
|
||||
self.begin_drain(
|
||||
index,
|
||||
retirement
|
||||
.clone()
|
||||
.unwrap_or(ManagedTargetEndReason::Stopped),
|
||||
now,
|
||||
)
|
||||
} else {
|
||||
let previous = self.targets[index].take_phase();
|
||||
self.targets[index].restore_phase(TargetPhase::Active {
|
||||
client: previous.into_client(),
|
||||
schedule,
|
||||
send_waiting: false,
|
||||
});
|
||||
self.publish_target_phase(index, ManagedTargetLifecycle::Opening);
|
||||
true
|
||||
}
|
||||
}
|
||||
Poll::Ready(Ok(OpenOutcome::NoTestCompleted(completed))) => {
|
||||
self.publish_client_events(
|
||||
index,
|
||||
vec![ClientEvent::NoTestCompleted(completed)],
|
||||
);
|
||||
self.finish_target(index, ManagedTargetEndReason::NoTestComplete, None);
|
||||
let end_reason = if stopping {
|
||||
retirement
|
||||
.clone()
|
||||
.unwrap_or(ManagedTargetEndReason::Stopped)
|
||||
} else {
|
||||
ManagedTargetEndReason::NoTestComplete
|
||||
};
|
||||
self.finish_target(index, end_reason, None);
|
||||
false
|
||||
}
|
||||
Poll::Ready(Err(error)) => {
|
||||
self.fail_target(index, ManagedTargetFailurePhase::Opening, error);
|
||||
if stopping {
|
||||
let TargetPhase::Opening { open, .. } = self.targets[index].phase()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let cleanup_failure =
|
||||
(!open.stopped_after_current_attempt()).then(|| {
|
||||
classify_client_error(
|
||||
ManagedTargetFailurePhase::Opening,
|
||||
&error,
|
||||
)
|
||||
});
|
||||
self.finish_target(
|
||||
index,
|
||||
retirement
|
||||
.clone()
|
||||
.unwrap_or(ManagedTargetEndReason::Stopped),
|
||||
cleanup_failure,
|
||||
);
|
||||
} else {
|
||||
self.fail_target(index, ManagedTargetFailurePhase::Opening, error);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -568,16 +504,6 @@ impl ManagedClientTask {
|
||||
if stopping {
|
||||
client.discard_prepared_probe();
|
||||
self.targets[index].sync_live_packets_sent();
|
||||
if self.config.reconnect_timeout.is_some() {
|
||||
return self.begin_close(
|
||||
index,
|
||||
retirement
|
||||
.clone()
|
||||
.unwrap_or(ManagedTargetEndReason::Stopped),
|
||||
None,
|
||||
now,
|
||||
);
|
||||
}
|
||||
return self.begin_drain(
|
||||
index,
|
||||
retirement
|
||||
@@ -613,22 +539,6 @@ impl ManagedClientTask {
|
||||
self.finish_target(index, ManagedTargetEndReason::PeerClosed, None);
|
||||
return false;
|
||||
}
|
||||
if self.targets[index]
|
||||
.silence_deadline(self.config.reconnect_timeout)
|
||||
.is_some_and(|deadline| deadline <= now)
|
||||
{
|
||||
self.targets[index].sync_live_packets_sent();
|
||||
return self.begin_close(
|
||||
index,
|
||||
ManagedTargetEndReason::Failed(ManagedTargetFailure {
|
||||
phase: ManagedTargetFailurePhase::Receiving,
|
||||
kind: ManagedTargetFailureKind::Timeout,
|
||||
message: Arc::from("session silent while sending probes"),
|
||||
}),
|
||||
None,
|
||||
now,
|
||||
);
|
||||
}
|
||||
let TargetPhase::Active {
|
||||
client, schedule, ..
|
||||
} = self.targets[index].phase_mut()
|
||||
|
||||
@@ -45,8 +45,7 @@ struct UpdatePlan {
|
||||
|
||||
fn synchronously_retireable(phase: &TargetPhase) -> bool {
|
||||
match phase {
|
||||
TargetPhase::Retrying { .. }
|
||||
| TargetPhase::Pending { .. }
|
||||
TargetPhase::Pending { .. }
|
||||
| TargetPhase::Connecting { .. }
|
||||
| TargetPhase::Terminal { .. } => true,
|
||||
TargetPhase::Opening { open, .. } => !open.has_in_flight_work(),
|
||||
@@ -141,10 +140,6 @@ impl ManagedClientTask {
|
||||
membership: TargetMembership::Desired,
|
||||
remote: None,
|
||||
counters: TargetCounters::default(),
|
||||
sent_before_session: 0,
|
||||
retry_delay: self.config.reconnect_initial_delay,
|
||||
silence_since: None,
|
||||
last_reply_at: None,
|
||||
phase: Some(TargetPhase::Pending {
|
||||
client_config: planned.client_config,
|
||||
}),
|
||||
|
||||
@@ -10,4 +10,4 @@ publish = false
|
||||
irtt-client = { workspace = true, features = ["ancillary", "tokio"] }
|
||||
irtt-server.workspace = true
|
||||
irtt-proto.workspace = true
|
||||
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
||||
@@ -6,6 +6,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords = ["irtt", "networking", "latency", "protocol"]
|
||||
categories = ["network-programming"]
|
||||
@@ -17,4 +18,4 @@ subtle = "2.6.1"
|
||||
thiserror = "2.0.21"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.12.0"
|
||||
proptest = "1.11.0"
|
||||
@@ -77,9 +77,6 @@
|
||||
//!
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
/// Cargo package version of this compiled library.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub mod echo;
|
||||
mod envelope;
|
||||
pub mod error;
|
||||
|
||||
@@ -6,6 +6,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords = ["irtt", "networking", "latency", "server"]
|
||||
categories = ["network-programming"]
|
||||
@@ -15,7 +16,7 @@ irtt-proto.workspace = true
|
||||
getrandom = "0.4.3"
|
||||
socket2 = { version = "0.6.5", features = ["all"] }
|
||||
thiserror = "2.0.21"
|
||||
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
||||
|
||||
# Wildcard listeners recover each request's local destination address and send
|
||||
# the reply from it. Only these targets have a safe destination-metadata and
|
||||
|
||||
@@ -108,9 +108,6 @@
|
||||
//! payload bytes never reach a reply under any mode.
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
/// Cargo package version of this compiled library.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
mod clock;
|
||||
mod config;
|
||||
mod core;
|
||||
|
||||
@@ -6,6 +6,7 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords = ["irtt", "networking", "latency", "statistics"]
|
||||
categories = ["network-programming"]
|
||||
@@ -18,4 +19,4 @@ irtt-client.workspace = true
|
||||
name = "finite_vs_continuous"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.12.0"
|
||||
proptest = "1.11.0"
|
||||
@@ -175,31 +175,29 @@ impl CoreStats {
|
||||
let mut updates = Vec::new();
|
||||
|
||||
for pair in self.ipdv_tracker.insert(sample) {
|
||||
let rtt_magnitude_ns = pair.rtt_ipdv_ns.saturating_abs();
|
||||
let send_magnitude_ns = pair.send_ipdv_ns.map(i128::saturating_abs);
|
||||
let receive_magnitude_ns = pair.receive_ipdv_ns.map(i128::saturating_abs);
|
||||
let Some(rtt_ipdv) = duration_from_non_negative_i128_ns(rtt_magnitude_ns) else {
|
||||
let Some(rtt_ipdv) = duration_from_non_negative_i128_ns(pair.rtt_ipdv_ns) else {
|
||||
continue;
|
||||
};
|
||||
let send_ipdv = send_magnitude_ns.and_then(duration_from_non_negative_i128_ns);
|
||||
let receive_ipdv = receive_magnitude_ns.and_then(duration_from_non_negative_i128_ns);
|
||||
let send_ipdv = pair
|
||||
.send_ipdv_ns
|
||||
.and_then(duration_from_non_negative_i128_ns);
|
||||
let receive_ipdv = pair
|
||||
.receive_ipdv_ns
|
||||
.and_then(duration_from_non_negative_i128_ns);
|
||||
|
||||
self.ipdv_round_trip.push_ns(rtt_magnitude_ns);
|
||||
self.ipdv_round_trip.push_ns(pair.rtt_ipdv_ns);
|
||||
|
||||
if let Some(value) = send_magnitude_ns {
|
||||
if let Some(value) = pair.send_ipdv_ns {
|
||||
self.ipdv_send.push_ns(value);
|
||||
}
|
||||
|
||||
if let Some(value) = receive_magnitude_ns {
|
||||
if let Some(value) = pair.receive_ipdv_ns {
|
||||
self.ipdv_receive.push_ns(value);
|
||||
}
|
||||
|
||||
updates.push(IpdvPairUpdate {
|
||||
previous_seq: pair.previous_seq,
|
||||
current_seq: pair.current_seq,
|
||||
rtt_ipdv_ns: pair.rtt_ipdv_ns,
|
||||
send_ipdv_ns: pair.send_ipdv_ns,
|
||||
receive_ipdv_ns: pair.receive_ipdv_ns,
|
||||
rtt_ipdv,
|
||||
send_ipdv,
|
||||
receive_ipdv,
|
||||
|
||||
@@ -89,9 +89,9 @@ impl IpdvTracker {
|
||||
Some(CompletedIpdvPair {
|
||||
previous_seq,
|
||||
current_seq,
|
||||
rtt_ipdv_ns: current.rtt_primary_ns - previous.rtt_primary_ns,
|
||||
send_ipdv_ns: send_ipdv_ns(previous, current),
|
||||
receive_ipdv_ns: receive_ipdv_ns(previous, current),
|
||||
rtt_ipdv_ns: abs_i128_ns(current.rtt_primary_ns - previous.rtt_primary_ns),
|
||||
send_ipdv_ns: send_ipdv_ns(previous, current).map(abs_i128_ns),
|
||||
receive_ipdv_ns: receive_ipdv_ns(previous, current).map(abs_i128_ns),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,10 @@ fn duration_ns_i128(duration: Duration) -> i128 {
|
||||
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
|
||||
}
|
||||
|
||||
fn abs_i128_ns(value: i128) -> i128 {
|
||||
value.saturating_abs()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
#![warn(missing_docs)]
|
||||
#![warn(rustdoc::missing_crate_level_docs)]
|
||||
|
||||
/// Cargo package version of this compiled library.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use irtt_client::ClientEvent;
|
||||
@@ -523,12 +520,6 @@ pub struct IpdvPairUpdate {
|
||||
pub previous_seq: u32,
|
||||
/// Sequence number of the later reply in the adjacent pair.
|
||||
pub current_seq: u32,
|
||||
/// Signed round-trip delay change (current minus previous) in nanoseconds.
|
||||
pub rtt_ipdv_ns: i128,
|
||||
/// Signed send-side delay change in nanoseconds, when available for both replies.
|
||||
pub send_ipdv_ns: Option<i128>,
|
||||
/// Signed receive-side delay change in nanoseconds, when available for both replies.
|
||||
pub receive_ipdv_ns: Option<i128>,
|
||||
/// Absolute round-trip IPDV between the adjacent replies.
|
||||
pub rtt_ipdv: Duration,
|
||||
/// Absolute send-side IPDV when send one-way delay is available for both replies.
|
||||
|
||||
@@ -191,15 +191,10 @@ impl ReplySample {
|
||||
client_receive_mono: received_at.mono,
|
||||
client_send_wall_ns: system_time_ns(sent_at.wall),
|
||||
client_receive_wall_ns: system_time_ns(received_at.wall),
|
||||
// Midpoint timestamps serve as both directional endpoints.
|
||||
server_receive_mono_ns: server_timing
|
||||
.and_then(|timing| timing.receive_mono_ns.or(timing.midpoint_mono_ns)),
|
||||
server_send_mono_ns: server_timing
|
||||
.and_then(|timing| timing.send_mono_ns.or(timing.midpoint_mono_ns)),
|
||||
server_receive_wall_ns: server_timing
|
||||
.and_then(|timing| timing.receive_wall_ns.or(timing.midpoint_wall_ns)),
|
||||
server_send_wall_ns: server_timing
|
||||
.and_then(|timing| timing.send_wall_ns.or(timing.midpoint_wall_ns)),
|
||||
server_receive_mono_ns: server_timing.and_then(|timing| timing.receive_mono_ns),
|
||||
server_send_mono_ns: server_timing.and_then(|timing| timing.send_mono_ns),
|
||||
server_receive_wall_ns: server_timing.and_then(|timing| timing.receive_wall_ns),
|
||||
server_send_wall_ns: server_timing.and_then(|timing| timing.send_wall_ns),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+3
-88
@@ -37,13 +37,12 @@ overrides it for that target, and `@hmac=` explicitly disables HMAC for that
|
||||
target. The modifier uses `@`, not a shell command separator; quote an argument
|
||||
if its key needs shell quoting.
|
||||
|
||||
Global `--hmac KEY` and per-target `@hmac=KEY` accept plain-text keys or hexadecimal keys prefixed with `0x`. For example, `secret` and `0x736563726574` are equivalent.
|
||||
|
||||
Target syntax supports these backslash escapes in labels, addresses, and HMAC
|
||||
values: `\\` for a literal backslash, `\@` for a literal `@`, `\=` for a
|
||||
literal `=`, and `\;` for a literal semicolon. Escaping `@` is necessary in a
|
||||
label or address when it would otherwise begin `@hmac=`; escaping the first
|
||||
`=` is how an unlabeled target or a label can contain that character. An `=` in an HMAC value does not need escaping. Positional
|
||||
`=` is how an unlabeled target or a label can contain that character. An `=` in
|
||||
an HMAC value, including Base64 padding, does not need escaping. Positional
|
||||
target arguments may contain a raw comma; stdin target sets have the additional
|
||||
`\,` escape described below.
|
||||
|
||||
@@ -94,25 +93,6 @@ labels; choose another alias if one collides with an explicit label.
|
||||
ordinary multi-target pacing and each sends its own probes, so dual-stack can
|
||||
double the number of probes per interval compared with one target per hostname.
|
||||
|
||||
## LOCAL SOCKET CONFIGURATION
|
||||
|
||||
The client supports configuring its local UDP sockets using the following options:
|
||||
|
||||
- `--bind-addr IP[:PORT]` binds sockets to the specified local IP address and optional port. If no port is specified, or the port is `0`, the operating system assigns an ephemeral port. IPv6 addresses with an explicit port must use `[ADDR]:PORT` syntax. The address family must match the target's resolved address family.
|
||||
- `--bind-device DEVICE` (Linux, Android, Fuchsia) binds sockets to a network interface using `SO_BINDTODEVICE`.
|
||||
- `--mark MARK` (Linux, Android, Fuchsia) sets the socket's firewall mark using `SO_MARK`, allowing traffic to be selected by policy routing or firewall rules. Accepts a decimal or 0x-prefixed hexadecimal unsigned 32-bit integer.
|
||||
- `--fib FIB` (FreeBSD) selects a routing table using `SO_SETFIB`. The FIB is specified as a decimal unsigned 32-bit integer.
|
||||
|
||||
For example, bind IPv4 probes to a specific source address and interface, with a firewall mark:
|
||||
|
||||
```sh
|
||||
irtt-client -4 --bind-addr 192.0.2.10 --bind-device wan0 --mark 1234 host.example
|
||||
```
|
||||
|
||||
Setting a firewall mark or binding to a network device may require elevated privileges or appropriate capabilities. If the operating system rejects a socket option, socket setup fails.
|
||||
|
||||
A fixed local port may cause binding conflicts when multiple target sockets are created. Use an ephemeral port unless a specific source port is required. A family-specific bind address is also incompatible with targets using the other address family.
|
||||
|
||||
## DYNAMIC TARGET SETS FROM STANDARD INPUT
|
||||
|
||||
`--targets-stdin` is available only with continuous mode (`--duration 0`).
|
||||
@@ -172,39 +152,8 @@ desired set to 128 targets and retains at most 256 live target generations
|
||||
while replacements drain. The 128-target limit applies after dual-stack
|
||||
expansion: a hostname producing both families consumes two target slots.
|
||||
|
||||
## NEGOTIATION ONLY
|
||||
|
||||
`-n` / `--no-test` connects to each target, negotiates the requested parameters,
|
||||
and exits without sending measurement probes. For example:
|
||||
|
||||
```sh
|
||||
irtt-client --no-test host.example:2112 --duration 30s --interval 100ms
|
||||
```
|
||||
|
||||
The `no_test_completed` event reports successful negotiation, including the
|
||||
negotiated duration, interval, and packet length. Output format and column
|
||||
selection work as usual; no measurement summary or statistics-memory warning
|
||||
is printed. Negotiation failures use the normal target diagnostics and exit
|
||||
policy: an uninterrupted run exits nonzero if no target succeeds.
|
||||
|
||||
Duration options still specify the duration to negotiate; even with
|
||||
`--continuous` or `--duration 0`, the client exits after negotiation.
|
||||
`--no-test` cannot be combined with `--targets-stdin`.
|
||||
|
||||
## FINITE VERSUS CONTINUOUS OPERATION
|
||||
|
||||
`--duration` and `--interval` accept fractional and composite durations, such as
|
||||
`2.5s`, `1m30s`, and `1h`, as well as `ms`, `us` (or `µs`), and `ns` units
|
||||
(e.g. `500us` or `100ns`). A nonzero value requires a unit. The interval must
|
||||
be greater than zero; a zero test duration, including `0s`, selects continuous
|
||||
mode.
|
||||
|
||||
`--timeouts 1s,2s,4s,8s` sets the session-opening retry schedule. Each
|
||||
comma-separated duration is the receive timeout for one open attempt; attempts
|
||||
stop when a valid open reply arrives. The list must be nonempty, with each
|
||||
value at least `200ms`, and uses the same duration syntax as `--interval`.
|
||||
Omitting the option preserves the library default schedule.
|
||||
|
||||
`--duration` controls the run length:
|
||||
|
||||
- Any nonzero duration (default `10s`) is a **finite** run: probing stops
|
||||
@@ -220,40 +169,6 @@ See [MEASUREMENTS AND MEMORY](#measurements-and-memory) below for why this
|
||||
distinction also determines what statistics can be computed and how much
|
||||
memory a long run retains.
|
||||
|
||||
### Optional recovery and event-only runs
|
||||
|
||||
`--no-stats` disables statistics collection and final summaries while preserving individual measurement output. Statistics-dependent columns (such as IPDV) are unavailable. This runtime option does not remove the `irtt-stats` dependency.
|
||||
|
||||
To omit statistics at compile time, build with:
|
||||
|
||||
```sh
|
||||
cargo build --release -p irtt-rs --bin irtt-client --no-default-features --features client
|
||||
```
|
||||
|
||||
Use `--features client,stats` to include statistics in a client-only build.
|
||||
Default builds include statistics, and `tui` always enables them. Without
|
||||
compiled statistics, `--no-stats` remains accepted, summaries and statistics
|
||||
warnings are disabled, and IPDV columns retain their names with absent values
|
||||
(`-` in tables, empty CSV/TSV fields, and JSON `null`). Measurement events and
|
||||
protocol-level received statistics remain available.
|
||||
|
||||
`--reconnect` enables automatic recovery for individual targets in continuous mode (`--continuous` or `--duration 0`). Failed connections, closed sessions, and unresponsive servers are retried without interrupting other targets. Retries double the delay after each failed attempt, from 250 ms up to 10 seconds by default. A valid reply resets the delay to the initial value; each target backs off independently.
|
||||
|
||||
`--reconnect-initial-delay DURATION` (default: `250ms`) and `--reconnect-max-delay DURATION` (default: `10s`) configure these delays. Both require `--reconnect`, must be positive and schedulable, and the maximum must be at least twice the initial delay. For example, `--reconnect-initial-delay 1s --reconnect-max-delay 5s` retries after `1s`, `2s`, `4s`, `5s`, then `5s` thereafter.
|
||||
|
||||
`--reconnect-timeout DURATION` controls how long a session can go without valid replies before reconnection is attempted (default: `10s`). The timeout applies only while actively sending probes and should account for the probe interval and expected packet loss.
|
||||
|
||||
**Statistics reset on reconnection**, so final summaries only reflect the latest session per target. A warning is emitted when reconnection is enabled with statistics collection. Use `--no-stats` to disable collection and suppress the warning.
|
||||
|
||||
Example for continuous, multi-target CSV output without statistics:
|
||||
|
||||
```sh
|
||||
irtt-client --continuous --reconnect --no-stats \
|
||||
--format csv --header never \
|
||||
--columns client_receive_wall_ns,target,seq,rd_us,sd_us \
|
||||
reflector1 reflector2
|
||||
```
|
||||
|
||||
## OUTPUT
|
||||
|
||||
### `--format FORMAT`
|
||||
@@ -389,7 +304,7 @@ approaches a fixed multiple of `targets` regardless of how long it runs.
|
||||
## PEER CLOSE AND EXIT BEHAVIOR
|
||||
|
||||
A peer closing a finite run is accepted as a terminal, successful outcome.
|
||||
Without `--reconnect`, in continuous mode, an uninterrupted peer closure is treated as an error —
|
||||
In continuous mode, an uninterrupted peer closure is treated as an error —
|
||||
the client exits nonzero — so a process supervisor can restart it; an
|
||||
interrupted run (`Ctrl-C`) never reports peer closure as an error, whichever
|
||||
mode it was in.
|
||||
|
||||
+1
-4
@@ -49,12 +49,9 @@ reports which feature to rebuild with, and `irtt-rs --help` lists which
|
||||
applets the current build actually contains.
|
||||
|
||||
```sh
|
||||
cargo install irtt-rs --locked --no-default-features --features client,stats
|
||||
cargo install irtt-rs --locked --no-default-features --features client
|
||||
```
|
||||
|
||||
Use `--features client` to omit statistics. The default build includes the
|
||||
`stats` feature, and `tui` always enables it.
|
||||
|
||||
## RELATIONSHIP TO irtt-client, irtt-tui, irtt-server
|
||||
|
||||
`irtt-rs` is packaging and dispatch only. All probing, display, and server
|
||||
|
||||
+2
-3
@@ -74,9 +74,8 @@ IPv6 listener.
|
||||
|
||||
### `--hmac KEY`
|
||||
|
||||
Accepts a plain-text key or a hexadecimal key prefixed with `0x`. For example, `secret` and `0x736563726574` are equivalent. The `IRTT_SERVER_HMAC` environment variable uses the same syntax.
|
||||
|
||||
With a key configured, every request must carry a valid MAC and every reply is authenticated;
|
||||
HMAC key, taken as the UTF-8 bytes of this argument. With a key configured,
|
||||
every request must carry a valid MAC and every reply is authenticated;
|
||||
without one, authenticated requests are dropped. The key is visible in the
|
||||
process arguments.
|
||||
|
||||
|
||||
+4
-7
@@ -16,9 +16,9 @@ At least one target is required.
|
||||
|
||||
Target syntax, multiple targets, and pacing are identical to
|
||||
`irtt-client(1)` — see that manual for `[LABEL=]TARGET` syntax and the
|
||||
`--pacing staggered|burst` option. The client configuration flags
|
||||
(`--interval`, `--timeouts`, `--length`, `--hmac`, `--clock`, `--tstamp`,
|
||||
`--stats`, `--sfill`, `--dscp`, `--ttl`, `--loose`) are shared with `irtt-client` as
|
||||
`--pacing staggered|burst` option. The full set of negotiation flags
|
||||
(`--interval`, `--length`, `--hmac`, `--clock`, `--tstamp`, `--stats`,
|
||||
`--sfill`, `--dscp`, `--ttl`, `--loose`) is shared with `irtt-client` as
|
||||
well; consult that manual for their meaning.
|
||||
|
||||
Address-family controls are shared too: `-4` / `--ipv4` uses IPv4 only,
|
||||
@@ -40,10 +40,7 @@ See the client manual's address-family section for discovery errors and labels.
|
||||
|
||||
Unlike `irtt-client`, **the TUI defaults to continuous mode**
|
||||
(`--duration 0`): it runs until you quit. Pass an explicit `--duration` for
|
||||
a finite run. Both `--duration` and `--interval` use the
|
||||
[client duration syntax](irtt-client.md#finite-versus-continuous-operation),
|
||||
including fractional values (`2.5s`), composite values (`1m30s`), and units
|
||||
from nanoseconds (`ns`) through hours (`h`):
|
||||
a finite run:
|
||||
|
||||
```sh
|
||||
irtt-tui <server> --duration 30s
|
||||
|
||||
Generated
+16
-16
@@ -4,9 +4,9 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.5.0"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -31,9 +31,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.9.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df"
|
||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -76,9 +76,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctutils"
|
||||
version = "0.4.4"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12218662ccac37d9714f2191252553d67585b97df8c839cbe49e0bde278dc865"
|
||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||
dependencies = [
|
||||
"cmov",
|
||||
]
|
||||
@@ -97,9 +97,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.15"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
@@ -174,9 +174,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
@@ -285,9 +285,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -316,9 +316,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.54.1"
|
||||
version = "1.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
@@ -347,9 +347,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
arbitrary = "1.5.0"
|
||||
arbitrary = "1.4.2"
|
||||
libfuzzer-sys = "0.4.13"
|
||||
|
||||
[dependencies.irtt-proto]
|
||||
|
||||
@@ -215,7 +215,7 @@ def select_scenarios(args: argparse.Namespace) -> list[Scenario]:
|
||||
def preflight(args: argparse.Namespace) -> None:
|
||||
if platform.system() != "Linux":
|
||||
raise RuntimeError("netem interop requires Linux network namespaces and tc netem")
|
||||
for binary in ["ip", "tc", "nsenter"]:
|
||||
for binary in ["ip", "tc"]:
|
||||
if shutil.which(binary) is None:
|
||||
raise RuntimeError(f"required command not found in PATH: {binary}")
|
||||
if args.sudo and shutil.which(args.sudo) is None:
|
||||
@@ -256,7 +256,7 @@ def sudo_prefix(args: argparse.Namespace) -> list[str]:
|
||||
|
||||
|
||||
def netns(args: argparse.Namespace, namespace: str, command: list[str]) -> list[str]:
|
||||
return [*sudo_prefix(args), "nsenter", f"--net=/run/netns/{namespace}", "--", *command]
|
||||
return [*sudo_prefix(args), "ip", "netns", "exec", namespace, *command]
|
||||
|
||||
|
||||
def run_checked(
|
||||
@@ -300,12 +300,12 @@ def setup_namespaces(args: argparse.Namespace, run_dir: Path) -> None:
|
||||
[*sudo_prefix(args), "ip", "link", "add", CLIENT_VETH, "type", "veth", "peer", "name", SERVER_VETH],
|
||||
[*sudo_prefix(args), "ip", "link", "set", CLIENT_VETH, "netns", CLIENT_NS],
|
||||
[*sudo_prefix(args), "ip", "link", "set", SERVER_VETH, "netns", SERVER_NS],
|
||||
netns(args, CLIENT_NS, ["ip", "addr", "add", CLIENT_ADDR, "dev", CLIENT_VETH]),
|
||||
netns(args, SERVER_NS, ["ip", "addr", "add", SERVER_ADDR, "dev", SERVER_VETH]),
|
||||
netns(args, CLIENT_NS, ["ip", "link", "set", "lo", "up"]),
|
||||
netns(args, SERVER_NS, ["ip", "link", "set", "lo", "up"]),
|
||||
netns(args, CLIENT_NS, ["ip", "link", "set", CLIENT_VETH, "up"]),
|
||||
netns(args, SERVER_NS, ["ip", "link", "set", SERVER_VETH, "up"]),
|
||||
[*sudo_prefix(args), "ip", "-n", CLIENT_NS, "addr", "add", CLIENT_ADDR, "dev", CLIENT_VETH],
|
||||
[*sudo_prefix(args), "ip", "-n", SERVER_NS, "addr", "add", SERVER_ADDR, "dev", SERVER_VETH],
|
||||
[*sudo_prefix(args), "ip", "-n", CLIENT_NS, "link", "set", "lo", "up"],
|
||||
[*sudo_prefix(args), "ip", "-n", SERVER_NS, "link", "set", "lo", "up"],
|
||||
[*sudo_prefix(args), "ip", "-n", CLIENT_NS, "link", "set", CLIENT_VETH, "up"],
|
||||
[*sudo_prefix(args), "ip", "-n", SERVER_NS, "link", "set", SERVER_VETH, "up"],
|
||||
]
|
||||
for index, command in enumerate(commands):
|
||||
run_checked(command, run_dir / "setup" / f"{index:02d}")
|
||||
@@ -409,7 +409,12 @@ def apply_netem(args: argparse.Namespace, scenario: Scenario, scenario_dir: Path
|
||||
for namespace, dev, setting in commands:
|
||||
if not setting:
|
||||
continue
|
||||
command = netns(args, namespace, [
|
||||
command = [
|
||||
*sudo_prefix(args),
|
||||
"ip",
|
||||
"netns",
|
||||
"exec",
|
||||
namespace,
|
||||
"tc",
|
||||
"qdisc",
|
||||
"replace",
|
||||
@@ -418,20 +423,25 @@ def apply_netem(args: argparse.Namespace, scenario: Scenario, scenario_dir: Path
|
||||
"root",
|
||||
"netem",
|
||||
*shlex.split(setting),
|
||||
])
|
||||
]
|
||||
run_checked(command, scenario_dir / "netem" / f"{namespace}-{dev}")
|
||||
|
||||
|
||||
def delete_qdiscs(args: argparse.Namespace, log_dir: Path) -> None:
|
||||
for namespace, dev in [(CLIENT_NS, CLIENT_VETH), (SERVER_NS, SERVER_VETH)]:
|
||||
command = netns(args, namespace, [
|
||||
command = [
|
||||
*sudo_prefix(args),
|
||||
"ip",
|
||||
"netns",
|
||||
"exec",
|
||||
namespace,
|
||||
"tc",
|
||||
"qdisc",
|
||||
"del",
|
||||
"dev",
|
||||
dev,
|
||||
"root",
|
||||
])
|
||||
]
|
||||
run_checked(
|
||||
command,
|
||||
log_dir / "qdisc-cleanup" / f"{namespace}-{dev}",
|
||||
@@ -688,19 +698,12 @@ def cleanup(args: argparse.Namespace, quiet: bool) -> None:
|
||||
return
|
||||
if args.sudo and shutil.which(args.sudo) is None:
|
||||
return
|
||||
commands = []
|
||||
if shutil.which("nsenter") is not None:
|
||||
commands.extend([
|
||||
netns(args, CLIENT_NS, ["tc", "qdisc", "del", "dev", CLIENT_VETH, "root"]),
|
||||
netns(args, SERVER_NS, ["tc", "qdisc", "del", "dev", SERVER_VETH, "root"]),
|
||||
])
|
||||
commands.extend([
|
||||
commands = [
|
||||
[*sudo_prefix(args), "ip", "netns", "exec", CLIENT_NS, "tc", "qdisc", "del", "dev", CLIENT_VETH, "root"],
|
||||
[*sudo_prefix(args), "ip", "netns", "exec", SERVER_NS, "tc", "qdisc", "del", "dev", SERVER_VETH, "root"],
|
||||
[*sudo_prefix(args), "ip", "netns", "delete", CLIENT_NS],
|
||||
[*sudo_prefix(args), "ip", "netns", "delete", SERVER_NS],
|
||||
# A setup failure can leave either end of the pair in the host namespace.
|
||||
[*sudo_prefix(args), "ip", "link", "delete", CLIENT_VETH],
|
||||
[*sudo_prefix(args), "ip", "link", "delete", SERVER_VETH],
|
||||
])
|
||||
]
|
||||
for command in commands:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
|
||||
Reference in new issue
Block a user