Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa1313e95 | ||
|
|
8b7e793d50 | ||
|
|
e16336e51b | ||
|
|
04e1f11f3a | ||
|
|
3344407706 | ||
|
|
492f33408c | ||
|
|
f290e3a3f0 | ||
|
|
f96dc71beb | ||
|
|
e7caf7ec9e | ||
|
|
da433f9902 | ||
|
|
9bb3fd63d8 | ||
|
|
ea8734a7aa | ||
|
|
f08d40bfe9 | ||
|
|
583001f091 | ||
|
|
7aca022e11 | ||
|
|
3d2554cf2b | ||
|
|
e52c03f3b8 | ||
|
|
643e3b8b76 | ||
|
|
52f2a07490 | ||
|
|
d2a7acdec4 | ||
|
|
56fe30d0e2 | ||
|
|
d4c89ee22c | ||
|
|
5481009fe3 | ||
|
|
7d640793b6 | ||
|
|
9e8559d713 | ||
|
|
06fff94505 | ||
|
|
d7d3f58716 | ||
|
|
2a82951572 | ||
|
|
7da8d2141c | ||
|
|
bdb8c13d7d | ||
|
|
a04ea5727b | ||
|
|
ab5f1ac807 | ||
|
|
45b0c8549d | ||
|
|
18a8e307fb | ||
|
|
ab241c4a2b |
No files matched your search
@@ -0,0 +1,313 @@
|
|||||||
|
|
||||||
|
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
|
if: matrix.musl
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install --no-install-recommends musl-tools
|
sudo apt-get install -y --no-install-recommends musl-tools
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||||
|
|
||||||
- name: Log in to Gitea Container Registry
|
- name: Log in to Gitea Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GITEA_REGISTRY }}
|
registry: ${{ env.GITEA_REGISTRY }}
|
||||||
# The automatic per-job token comes back unauthorized for package
|
# The automatic per-job token comes back unauthorized for package
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ secrets.GHCR_USERNAME }}
|
username: ${{ secrets.GHCR_USERNAME }}
|
||||||
|
|||||||
@@ -34,12 +34,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Install Go toolchain
|
||||||
|
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||||
|
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
|
- name: Check host tools and network administration access
|
||||||
run: |
|
run: |
|
||||||
ip -V
|
ip -V
|
||||||
tc -V
|
tc -V
|
||||||
|
nsenter --version
|
||||||
go version
|
go version
|
||||||
sudo -n true
|
# 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
|
||||||
|
|
||||||
- name: Install upstream irtt
|
- name: Install upstream irtt
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
|
||||||
- name: Install cargo-deny
|
- name: Install cargo-deny
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
# It remains here while portable actionlint runs with Gitea CI, but scans
|
# It remains here while portable actionlint runs with Gitea CI, but scans
|
||||||
# both workflow directories through its GitHub Actions rule set.
|
# both workflow directories through its GitHub Actions rule set.
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
||||||
with:
|
with:
|
||||||
persona: regular
|
persona: regular
|
||||||
inputs: |
|
inputs: |
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
target: aarch64-unknown-linux-musl
|
target: aarch64-unknown-linux-musl
|
||||||
musl: true
|
musl: true
|
||||||
|
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-26.04-arm
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
if: matrix.musl
|
if: matrix.musl
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install --no-install-recommends musl-tools
|
sudo apt-get install -y --no-install-recommends musl-tools
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
# the old --skip-git flag is no longer supported.
|
# the old --skip-git flag is no longer supported.
|
||||||
scan-scheduled:
|
scan-scheduled:
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
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@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@b1d267aea5211fa648d9d442fa64b90dbdcbdd36" # v2.7.0
|
||||||
scan-pr:
|
scan-pr:
|
||||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@b1d267aea5211fa648d9d442fa64b90dbdcbdd36" # v2.7.0
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
- name: Upload Clippy SARIF
|
- name: Upload Clippy SARIF
|
||||||
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
|
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: rust-clippy-results.sarif
|
sarif_file: rust-clippy-results.sarif
|
||||||
|
|||||||
+97
-2
@@ -10,6 +10,12 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-proto
|
## irtt-proto
|
||||||
|
|
||||||
|
### 0.5.2
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Exported `VERSION`, the compiled crate's Cargo package version, for applications reporting their component versions. Wire behavior is unchanged.
|
||||||
|
|
||||||
### 0.5.1
|
### 0.5.1
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -34,6 +40,25 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-client
|
## irtt-client
|
||||||
|
|
||||||
|
### 0.7.0
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Opt-in automatic session reconnection in the Tokio managed driver and `BlockingManagedClient`. Continuous measurement runs can retry initial connection failures, failed or peer-closed sessions, and server-limited session completion. Silence detection reconnects a session that stops replying while probes are being sent.
|
||||||
|
- Each desired target retries independently, leaving healthy targets running. Exponential backoff doubles from `reconnect_initial_delay` (default 250 ms) to `reconnect_max_delay` (default 10 s); a valid reply restores the initial delay. Reopening resolves the original endpoint again. Target removal and explicit shutdown cancel recovery.
|
||||||
|
- Exported `VERSION` for compiled component-version reporting.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Stopping or replacing a target during session opening cancels the pending open immediately, instead of waiting for its retry schedule to finish.
|
||||||
|
|
||||||
|
#### Compatibility
|
||||||
|
|
||||||
|
- `ManagedClientConfig` gains `reconnect_timeout`, `reconnect_initial_delay`, and `reconnect_max_delay`; update complete struct literals or use `..Default::default()`. Reconnection is disabled by default (`reconnect_timeout: None`) and is valid only for continuous normal measurement mode, with a positive schedulable silence timeout. Retry delays must be positive and schedulable, and the maximum must be at least twice the initial delay.
|
||||||
|
- `ManagedConfigError` gains `InvalidReconnectPolicy` and `InvalidReconnectDelays`; downstream exhaustive matches must handle them. Delay validation applies even when recovery is disabled.
|
||||||
|
- A reconnecting target returns to pending/opening status rather than producing a terminal outcome for each retry. Consumers must handle repeated session-start events for the same target instance and reset session-bound statistics and sequence tracking on each new session.
|
||||||
|
- Use `irtt-stats` 0.6 with this client version when exchanging `ClientEvent` values. Tokio remains optional; the default low-level client remains runtime-free. No wire-protocol migration is required.
|
||||||
|
|
||||||
### 0.6.0
|
### 0.6.0
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -105,6 +130,12 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-server
|
## irtt-server
|
||||||
|
|
||||||
|
### 0.5.4
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Exported `VERSION`, the compiled crate's Cargo package version, for applications reporting their component versions. Server policy and wire behavior are unchanged.
|
||||||
|
|
||||||
### 0.5.3
|
### 0.5.3
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -150,6 +181,23 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-stats
|
## irtt-stats
|
||||||
|
|
||||||
|
### 0.6.0
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
- Updated the public `irtt-client` dependency to 0.7. Applications passing `ClientEvent` values into this crate must upgrade both dependencies together; client 0.6 and 0.7 event types come from different crate versions and cannot be mixed.
|
||||||
|
- Added signed nanosecond changes to `IpdvPairUpdate`: `rtt_ipdv_ns`, `send_ipdv_ns`, and `receive_ipdv_ns`. Existing `Duration` fields and aggregate IPDV statistics remain absolute magnitudes. Update complete struct literals to include the new fields.
|
||||||
|
- Exported `VERSION` for compiled component-version reporting.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Directional IPDV is available with midpoint timestamps, using the midpoint for both server endpoints.
|
||||||
|
- Per-packet IPDV preserves the sign of the delay change instead of reporting only its magnitude.
|
||||||
|
|
||||||
|
#### Compatibility
|
||||||
|
|
||||||
|
- Collectors remain scoped to one target session. Reset or replace a collector when the managed client reconnects; this release does not aggregate across session sequence resets.
|
||||||
|
|
||||||
### 0.5.2
|
### 0.5.2
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
@@ -192,6 +240,48 @@ release. The format is loosely based on
|
|||||||
|
|
||||||
## irtt-rs
|
## irtt-rs
|
||||||
|
|
||||||
|
### 0.10.0
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
- Continuous CLI recovery with `--continuous --reconnect` (or `--duration 0 --reconnect`). Failed initial opens, failed or closed sessions, server-limited completion, and reply silence trigger independent per-target retries. Healthy targets keep running, including in multi-target and `--targets-stdin` runs; removing a desired target cancels its retries.
|
||||||
|
- `--reconnect-timeout` configures reply-silence detection while sending (default 10 s). `--reconnect-initial-delay` and `--reconnect-max-delay` configure per-target exponential backoff (defaults 250 ms and 10 s); the maximum must be at least twice the initial delay. Valid replies reset the backoff. Explicit stops do not retry.
|
||||||
|
- `--no-stats` disables client statistics collection, retention warnings, and final summaries while retaining measurement events and structured output columns. Statistics-dependent fields have absent values.
|
||||||
|
- Optional `stats` Cargo feature, enabled by default and required by `tui`. Build a lightweight client without statistics using `cargo build --release -p irtt-rs --bin irtt-client --no-default-features --features client`; use `--features client,stats` for a client-only build with statistics.
|
||||||
|
- `--continuous` as an explicit alias for zero duration, `-d` for duration, `-i` for interval, `-l` for packet length, client `-f` for output format, and `-v` for verbose output. Client/TUI durations accept fractional and composite values such as `2.5s` and `1m30s`.
|
||||||
|
- Client/TUI `--timeouts` configures the session-opening attempt schedule independently of session reconnection.
|
||||||
|
- Client `-n` / `--no-test` negotiates parameters and exits without measurement probes, using the selected output format.
|
||||||
|
- `--version` on the dispatcher and applets; applets also report their compiled library versions and whether client statistics are disabled.
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Signed per-packet IPDV is preserved in output; directional IPDV is available with midpoint timestamps.
|
||||||
|
|
||||||
|
#### Compatibility
|
||||||
|
|
||||||
|
- Reconnection is opt-in for the CLI and requires continuous measurement mode; it cannot be combined with `--no-test`. Statistics reset on every reopened session, so summaries describe only the latest session per target, not the whole continuous run. A warning explains this when recovery and statistics are both enabled. Continuous TUI recovery remains enabled as before.
|
||||||
|
- Statistics are no longer implied by the `client` feature. Existing custom builds using `--no-default-features --features client` must add `stats` to retain collection and summaries. `--no-stats` is accepted in builds without statistics; output schemas are preserved, with absent IPDV values.
|
||||||
|
- Nonzero duration values require units; zero (including `0s`) still selects continuous mode. `--continuous` and an explicit `--duration` are mutually exclusive.
|
||||||
|
- Application-library users must account for the added client argument/configuration fields and optional statistics feature. Dependencies are now client 0.7 and stats 0.6, with proto >=0.5.2 and server >=0.5.4 for component-version reporting. MSRV remains Rust 1.88.
|
||||||
|
|
||||||
|
### 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
|
### 0.8.0
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
@@ -312,10 +402,15 @@ Starting with this release, crates are versioned and tagged independently
|
|||||||
rather than in lockstep. Each crate's release is tagged as `<crate>/vX.Y.Z`
|
rather than in lockstep. Each crate's release is tagged as `<crate>/vX.Y.Z`
|
||||||
(e.g. `irtt-proto/v0.5.0`), pointing at the commit its version was released
|
(e.g. `irtt-proto/v0.5.0`), pointing at the commit its version was released
|
||||||
from. `irtt-proto`, `irtt-client`, `irtt-server`, and `irtt-stats` are
|
from. `irtt-proto`, `irtt-client`, `irtt-server`, and `irtt-stats` are
|
||||||
libraries published to [crates.io](https://crates.io); their tags exist to
|
libraries published to [crates.io](https://crates.io); publication is a separate
|
||||||
|
manual Cargo step. Their tags exist to
|
||||||
identify the exact source for a given crates.io release and do not produce a
|
identify the exact source for a given crates.io release and do not produce a
|
||||||
GitHub Release. The application package (`irtt-cli` through 0.5.0, `irtt-rs`
|
GitHub Release. The application package (`irtt-cli` through 0.5.0, `irtt-rs`
|
||||||
from 0.6.0 onward) is the only package with prebuilt binary artifacts: pushing
|
from 0.6.0 onward) is the only package with prebuilt binary artifacts: pushing
|
||||||
an `irtt-rs/vX.Y.Z` tag triggers [cargo-dist](https://opensource.axo.dev/cargo-dist/)
|
an `irtt-rs/vX.Y.Z` tag triggers [cargo-dist](https://opensource.axo.dev/cargo-dist/)
|
||||||
to build and publish a GitHub Release with platform binaries. The historical
|
to build and publish a GitHub Release with platform binaries. On Gitea, the
|
||||||
|
same application tag triggers the server Docker image workflow, publishing
|
||||||
|
`X.Y.Z` and `latest` to Gitea and GHCR. Library tags
|
||||||
|
trigger the Gitea Semver workflow, but neither binary nor image publication.
|
||||||
|
The application tag also triggers that Semver check. The historical
|
||||||
`irtt-cli/v0.5.0` tag remains as released; it is not rewritten or replaced.
|
`irtt-cli/v0.5.0` tag remains as released; it is not rewritten or replaced.
|
||||||
Generated
+249
-144
@@ -123,11 +123,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-vec 0.8.0",
|
"bit-vec 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -138,9 +138,13 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-vec"
|
name = "bit-vec"
|
||||||
version = "0.8.0"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269"
|
||||||
|
dependencies = [
|
||||||
|
"borsh",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
@@ -181,6 +185,30 @@ dependencies = [
|
|||||||
"objc2",
|
"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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@@ -199,6 +227,12 @@ version = "1.25.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "castaway"
|
name = "castaway"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
@@ -210,9 +244,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.6.0"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -230,6 +264,17 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
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]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -251,6 +296,15 @@ dependencies = [
|
|||||||
"clap_derive",
|
"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]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -272,7 +326,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -328,6 +382,12 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core_detect"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -337,6 +397,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "critical-section"
|
name = "critical-section"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -413,9 +482,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctutils"
|
name = "ctutils"
|
||||||
version = "0.4.3"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574"
|
checksum = "fe3e9d21d39e6f39ff46e03d5b621424f88ee23d8a1672c6bfc9ccc7f2da4f6a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cmov",
|
"cmov",
|
||||||
]
|
]
|
||||||
@@ -440,7 +509,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -451,7 +520,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -462,9 +531,9 @@ checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
@@ -539,9 +608,9 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
@@ -591,9 +660,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "finl_unicode"
|
name = "finl_unicode"
|
||||||
@@ -674,17 +743,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"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]]
|
[[package]]
|
||||||
@@ -719,6 +778,12 @@ dependencies = [
|
|||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@@ -759,10 +824,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indoc"
|
name = "indexmap"
|
||||||
version = "2.0.7"
|
version = "2.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
@@ -777,12 +852,12 @@ dependencies = [
|
|||||||
"indoc",
|
"indoc",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-client"
|
name = "irtt-client"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
"irtt-server",
|
"irtt-server",
|
||||||
@@ -804,7 +879,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-proto"
|
name = "irtt-proto"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"md-5",
|
"md-5",
|
||||||
@@ -815,13 +890,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-rs"
|
name = "irtt-rs"
|
||||||
version = "0.8.0"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"clap-num",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"hex",
|
||||||
|
"humantime",
|
||||||
"irtt-client",
|
"irtt-client",
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
"irtt-server",
|
"irtt-server",
|
||||||
@@ -833,7 +911,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-server"
|
name = "irtt-server"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
@@ -845,7 +923,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-stats"
|
name = "irtt-stats"
|
||||||
version = "0.5.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"irtt-client",
|
"irtt-client",
|
||||||
"measurement-stats",
|
"measurement-stats",
|
||||||
@@ -886,11 +964,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kasuari"
|
name = "kasuari"
|
||||||
version = "0.4.12"
|
version = "0.4.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
|
checksum = "72f14c90e40a6133f4805b83542a7ed7364f56070b447f56e7f4e90297122209"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.16.1",
|
"hashbrown",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
]
|
]
|
||||||
@@ -909,9 +987,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.190"
|
version = "0.2.191"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
@@ -936,9 +1014,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litrs"
|
name = "litrs"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -961,16 +1039,16 @@ version = "0.18.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
|
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.17.1",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac_address"
|
name = "mac_address"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a"
|
checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nix 0.30.1",
|
"nix 0.31.3",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1041,19 +1119,6 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.31.3"
|
version = "0.31.3"
|
||||||
@@ -1105,9 +1170,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_threads"
|
name = "num_threads"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
checksum = "cabcc654086861f9e1bc5115258de62ef92c79b20f4615a1b0cc57c4e82027d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -1206,19 +1271,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.9.2"
|
version = "2.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8"
|
checksum = "a8db75153506001cb36ed924f08cadc862eb3f8c4d0d534139f29074769d7660"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
|
"pest_stack_guard",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.9.2"
|
version = "2.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf"
|
checksum = "20aa17e11fb35664b67d5214a0afac57a360d231b6ec53d6eba0700b27d933b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
@@ -1226,26 +1292,36 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.9.2"
|
version = "2.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109"
|
checksum = "626ec5afccf44ccbfeb2b232316510bb55b4bf34d891afcb0fb07d2b17d7fe9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.119",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.9.2"
|
version = "2.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb"
|
checksum = "b3f45b6caa65c1b08186832745159c2529994146340a7bf44168095469720f5d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_stack_guard"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97e6cd2217057fa86b6eaa512917e58089d875b957a31e35e7ab729977e0a5f2"
|
||||||
|
dependencies = [
|
||||||
|
"psm",
|
||||||
|
"stacker",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
@@ -1317,12 +1393,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "proc-macro-crate"
|
||||||
version = "0.2.21"
|
version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1336,16 +1412,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proptest"
|
name = "proptest"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set 0.8.0",
|
"bit-set 0.11.1",
|
||||||
"bit-vec 0.8.0",
|
"bit-vec 0.10.1",
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
|
"chacha20",
|
||||||
|
"core_detect",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rand 0.9.5",
|
"rand 0.10.3",
|
||||||
"rand_chacha",
|
|
||||||
"rand_xorshift",
|
"rand_xorshift",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
"rusty-fork",
|
"rusty-fork",
|
||||||
@@ -1353,6 +1430,15 @@ dependencies = [
|
|||||||
"unarray",
|
"unarray",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psm"
|
||||||
|
version = "0.1.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
@@ -1391,22 +1477,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.5"
|
version = "0.10.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"getrandom 0.4.3",
|
||||||
"rand_core 0.9.5",
|
"rand_core 0.10.1",
|
||||||
]
|
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
@@ -1417,20 +1493,17 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.9.5"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.3.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_xorshift"
|
name = "rand_xorshift"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core 0.9.5",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1458,7 +1531,7 @@ dependencies = [
|
|||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown",
|
||||||
"itertools",
|
"itertools",
|
||||||
"kasuari",
|
"kasuari",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -1521,7 +1594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
|
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"hashbrown 0.17.1",
|
"hashbrown",
|
||||||
"indoc",
|
"indoc",
|
||||||
"instability",
|
"instability",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -1583,9 +1656,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.5"
|
version = "1.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.2",
|
"bitflags 2.13.2",
|
||||||
"errno",
|
"errno",
|
||||||
@@ -1657,7 +1730,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1667,7 +1740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1722,9 +1795,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.16.2"
|
version = "1.16.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -1736,6 +1809,19 @@ dependencies = [
|
|||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "static_assertions"
|
name = "static_assertions"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1799,9 +1885,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.6"
|
version = "3.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1934,7 +2020,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1960,9 +2046,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.2"
|
version = "1.54.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
@@ -1980,7 +2066,37 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"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",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2003,9 +2119,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
@@ -2038,9 +2154,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.27.0"
|
version = "1.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
|
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic",
|
"atomic",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
@@ -2119,7 +2235,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.6",
|
"syn 3.0.9",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2294,28 +2410,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.57.1"
|
version = "0.57.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
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",
|
|
||||||
]
|
|
||||||
+4
-5
@@ -14,13 +14,12 @@ edition = "2021"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
repository = "https://github.com/Lochnair/irtt-rs"
|
repository = "https://github.com/Lochnair/irtt-rs"
|
||||||
homepage = "https://github.com/Lochnair/irtt-rs"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
irtt-proto = { version = "0.5.0", path = "crates/irtt-proto" }
|
irtt-proto = { version = "0.5.2", path = "crates/irtt-proto" }
|
||||||
irtt-client = { version = "0.6.0", path = "crates/irtt-client" }
|
irtt-client = { version = "0.7.0", path = "crates/irtt-client" }
|
||||||
irtt-server = { version = "0.5.2", path = "crates/irtt-server" }
|
irtt-server = { version = "0.5.4", path = "crates/irtt-server" }
|
||||||
irtt-stats = { version = "0.5.2", path = "crates/irtt-stats" }
|
irtt-stats = { version = "0.6.0", path = "crates/irtt-stats" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
[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"]
|
||||||
@@ -45,8 +45,8 @@ this project's interop harness.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Wire protocol | Protocol version 1, verified against upstream v0.9.1 | Protocol version 1 (unchanged in v0.9.2) |
|
| Wire protocol | Protocol version 1, verified against upstream v0.9.1 | Protocol version 1 (unchanged in v0.9.2) |
|
||||||
| Finite probing | Supported, default 10s | Supported, default 1m |
|
| Finite probing | Supported, default 10s | Supported, default 1m |
|
||||||
| Continuous/indefinite operation | Supported (`--duration 0`); still computes running statistics (see below) | Supported via streaming mode (`-s`, added in v0.9.2); computes no statistics at all in that mode |
|
| Continuous/indefinite operation | Supported (`--continuous` or `--duration 0`), with optional per-target reconnection | Supported via streaming mode (`-s`, added in v0.9.2); computes no statistics at all in that mode |
|
||||||
| Statistics in continuous mode | Running count/min/max/mean/variance per target; exact medians unavailable; bounded IPDV tracking | None in streaming mode — raw/event output only, by design, to avoid unbounded memory |
|
| Statistics in continuous mode | Optional running count/min/max/mean/variance per session; no exact medians; bounded IPDV tracking. Use `--no-stats` or omit the `stats` feature to disable collection | None in streaming mode — raw/event output only, by design, to avoid unbounded memory |
|
||||||
| Multi-target probing (one invocation) | Supported, with labels and staggered/burst pacing | Not supported — one remote address per invocation |
|
| Multi-target probing (one invocation) | Supported, with labels and staggered/burst pacing | Not supported — one remote address per invocation |
|
||||||
| Terminal UI | `irtt-tui`, a live graph/dashboard applet | Not present |
|
| Terminal UI | `irtt-tui`, a live graph/dashboard applet | Not present |
|
||||||
| Output formats | table, csv, tsv, jsonl, with selectable columns | Text summary, JSON (optionally gzip to file), and a fixed-field raw mode; no CSV/TSV and no column selection |
|
| Output formats | table, csv, tsv, jsonl, with selectable columns | Text summary, JSON (optionally gzip to file), and a fixed-field raw mode; no CSV/TSV and no column selection |
|
||||||
@@ -84,12 +84,15 @@ cargo install irtt-rs --locked
|
|||||||
This installs the `irtt-rs` dispatcher plus dedicated `irtt-client`,
|
This installs the `irtt-rs` dispatcher plus dedicated `irtt-client`,
|
||||||
`irtt-tui`, and `irtt-server` binaries. Each applet can also be built in
|
`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
|
isolation with `--no-default-features` plus the applet's own feature, for
|
||||||
example a lightweight client-only install:
|
example a lightweight client-only install without statistics:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install irtt-rs --locked --no-default-features --features client
|
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:
|
For development, or to install directly from a checkout:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -104,14 +107,24 @@ Probe a server:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
irtt-client netperf-eu.bufferbloat.net:2112 --duration 30s --interval 100ms
|
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`):
|
Probe continuously until interrupted (`Ctrl-C`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
irtt-client netperf-eu.bufferbloat.net:2112 --duration 0
|
irtt-client netperf-eu.bufferbloat.net:2112 --continuous
|
||||||
|
|
||||||
|
# Recover failed, closed, or silent sessions independently per target
|
||||||
|
irtt-client netperf-eu.bufferbloat.net:2112 --continuous --reconnect \
|
||||||
|
--reconnect-timeout 10s --reconnect-initial-delay 250ms --reconnect-max-delay 10s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Reconnection resets statistics for that target; summaries cover its latest
|
||||||
|
session. Add `--no-stats` for measurement events without statistics collection.
|
||||||
|
|
||||||
Watch the same session live in the terminal UI:
|
Watch the same session live in the terminal UI:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -162,6 +175,7 @@ when to use it instead of `irtt-client`/`irtt-server` directly.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--no-default-features` | `irtt-rs` |
|
| `--no-default-features` | `irtt-rs` |
|
||||||
| `--no-default-features --features client` | `irtt-rs`, `irtt-client` |
|
| `--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 server` | `irtt-rs`, `irtt-server` |
|
||||||
| `--no-default-features --features tui` | `irtt-rs`, `irtt-tui` |
|
| `--no-default-features --features tui` | `irtt-rs`, `irtt-tui` |
|
||||||
| Default features (or `--all-features`) | `irtt-rs`, `irtt-client`, `irtt-tui`, `irtt-server` |
|
| Default features (or `--all-features`) | `irtt-rs`, `irtt-client`, `irtt-tui`, `irtt-server` |
|
||||||
|
|||||||
+11
-14
@@ -1,12 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-rs"
|
name = "irtt-rs"
|
||||||
version = "0.8.0"
|
version = "0.10.0"
|
||||||
description = "IRTT-compatible client, TUI, and server applications"
|
description = "IRTT-compatible client, TUI, and server applications"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["irtt", "networking", "latency", "rtt"]
|
keywords = ["irtt", "networking", "latency", "rtt"]
|
||||||
categories = ["command-line-utilities", "network-programming"]
|
categories = ["command-line-utilities", "network-programming"]
|
||||||
@@ -41,33 +40,31 @@ required-features = ["server"]
|
|||||||
doc = false
|
doc = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["client", "server", "tui"]
|
default = ["client", "server", "tui", "stats"]
|
||||||
client = ["dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats"]
|
client = ["dep:humantime", "dep:tokio", "dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto"]
|
||||||
server = ["dep:irtt-server", "dep:tokio"]
|
stats = ["dep:irtt-stats"]
|
||||||
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"]
|
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"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.45", default-features = false, features = ["clock"], optional = true }
|
chrono = { version = "0.4.45", default-features = false, features = ["clock"], optional = true }
|
||||||
clap = { version = "4.6.7", features = ["derive", "env"] }
|
clap = { version = "4.6.7", features = ["derive", "env"] }
|
||||||
|
clap-num = "=1.2.0"
|
||||||
crossterm = { version = "0.29.0", features = ["event-stream"], optional = true }
|
crossterm = { version = "0.29.0", features = ["event-stream"], optional = true }
|
||||||
futures-core = { version = "0.3.34", default-features = false, 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"] }
|
ctrlc = { version = "3.5.2", features = ["termination"] }
|
||||||
irtt-client = { workspace = true, optional = true }
|
irtt-client = { workspace = true, optional = true }
|
||||||
irtt-proto = { workspace = true, optional = true }
|
irtt-proto = { workspace = true, optional = true }
|
||||||
irtt-server = { workspace = true, optional = true }
|
irtt-server = { workspace = true, optional = true }
|
||||||
irtt-stats = { workspace = true, optional = true }
|
irtt-stats = { workspace = true, optional = true }
|
||||||
ratatui = { version = "0.30.2", optional = true }
|
ratatui = { version = "0.30.2", optional = true }
|
||||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync", "macros"], optional = true }
|
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync", "macros"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
irtt-server.workspace = true
|
irtt-server.workspace = true
|
||||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "sync"] }
|
tokio = { version = "1.54.1", default-features = false, features = ["rt", "sync"] }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dev-dependencies]
|
[target.'cfg(unix)'.dev-dependencies]
|
||||||
nix = { version = "0.31.3", features = ["term", "poll", "signal", "process"] }
|
nix = { version = "0.31.3", features = ["term", "poll", "signal", "process"] }
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
strip = "symbols"
|
|
||||||
@@ -11,8 +11,13 @@ Installing this crate builds:
|
|||||||
- `irtt-tui` — the interactive terminal UI (feature `tui`).
|
- `irtt-tui` — the interactive terminal UI (feature `tui`).
|
||||||
- `irtt-server` — the dedicated server binary (feature `server`).
|
- `irtt-server` — the dedicated server binary (feature `server`).
|
||||||
|
|
||||||
All three features are enabled by default, so a plain `cargo install irtt-rs`
|
The `client`, `server`, `tui`, and `stats` features are enabled by default, so a
|
||||||
installs everything.
|
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.
|
||||||
|
|
||||||
## This is an application, not a library
|
## This is an application, not a library
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
|
|||||||
};
|
};
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
|
"-V" | "--version" => Ok(AppletDispatch::Help(crate::version::app())),
|
||||||
"-h" | "--help" => Ok(AppletDispatch::Help(dispatcher_help())),
|
"-h" | "--help" => Ok(AppletDispatch::Help(dispatcher_help())),
|
||||||
"client" => Ok(AppletDispatch::Run {
|
"client" => Ok(AppletDispatch::Run {
|
||||||
applet: RequestedApplet::Client,
|
applet: RequestedApplet::Client,
|
||||||
@@ -78,6 +79,7 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
|
|||||||
|
|
||||||
pub fn dispatcher_help() -> String {
|
pub fn dispatcher_help() -> String {
|
||||||
let mut command = Command::new("irtt-rs")
|
let mut command = Command::new("irtt-rs")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("IRTT-compatible multi-applet dispatcher")
|
.about("IRTT-compatible multi-applet dispatcher")
|
||||||
.subcommand(Command::new("client").about(client_applet_about()))
|
.subcommand(Command::new("client").about(client_applet_about()))
|
||||||
.subcommand(Command::new("tui").about(tui_applet_about()))
|
.subcommand(Command::new("tui").about(tui_applet_about()))
|
||||||
|
|||||||
@@ -9,14 +9,18 @@ use clap::{Parser, ValueEnum};
|
|||||||
pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10);
|
pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[command(name = "irtt-client", about = "Minimal IRTT-compatible stream client")]
|
#[command(
|
||||||
|
name = "irtt-client",
|
||||||
|
version,
|
||||||
|
about = "Minimal IRTT-compatible stream client"
|
||||||
|
)]
|
||||||
pub struct ClientArgs {
|
pub struct ClientArgs {
|
||||||
/// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.
|
/// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.
|
||||||
#[arg(
|
#[arg(
|
||||||
value_name = "TARGET",
|
value_name = "TARGET",
|
||||||
num_args = 0..,
|
num_args = 0..,
|
||||||
value_parser = parse_target,
|
value_parser = parse_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."
|
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."
|
||||||
)]
|
)]
|
||||||
pub targets: Vec<TargetArg>,
|
pub targets: Vec<TargetArg>,
|
||||||
|
|
||||||
@@ -27,6 +31,15 @@ pub struct ClientArgs {
|
|||||||
)]
|
)]
|
||||||
pub targets_stdin: bool,
|
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.
|
/// Managed group pacing for multi-target mode.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
@@ -38,13 +51,43 @@ pub struct ClientArgs {
|
|||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
|
short = 'd',
|
||||||
default_value = "10s",
|
default_value = "10s",
|
||||||
value_parser = parse_test_duration,
|
value_parser = parse_test_duration,
|
||||||
help = "Test duration; use 0 for continuous mode",
|
help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)",
|
||||||
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."
|
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."
|
||||||
)]
|
)]
|
||||||
pub duration: Duration,
|
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)]
|
#[command(flatten)]
|
||||||
pub common: CommonClientArgs,
|
pub common: CommonClientArgs,
|
||||||
|
|
||||||
@@ -52,6 +95,7 @@ pub struct ClientArgs {
|
|||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_enum,
|
value_enum,
|
||||||
|
short = 'f',
|
||||||
default_value_t = OutputFormat::Table,
|
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."
|
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."
|
||||||
)]
|
)]
|
||||||
@@ -83,7 +127,7 @@ pub struct ClientArgs {
|
|||||||
pub header: HeaderMode,
|
pub header: HeaderMode,
|
||||||
|
|
||||||
/// Include extra fields in table output and final summaries.
|
/// Include extra fields in table output and final summaries.
|
||||||
#[arg(long)]
|
#[arg(short = 'v', long)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,23 +137,43 @@ impl ClientArgs {
|
|||||||
/// Targets are only required here. `--list-columns` returns before this is
|
/// Targets are only required here. `--list-columns` returns before this is
|
||||||
/// called, so listing columns still needs no target.
|
/// called, so listing columns still needs no target.
|
||||||
pub async fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
pub async fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
||||||
if self.targets_stdin && !self.is_continuous() {
|
if self.reconnect && !self.is_continuous() {
|
||||||
return Err("--targets-stdin requires --duration 0".to_owned());
|
return Err("--reconnect requires --continuous or --duration 0".to_owned());
|
||||||
}
|
}
|
||||||
prepare_managed_run(
|
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());
|
||||||
|
}
|
||||||
|
let mut setup = prepare_managed_run(
|
||||||
&self.common,
|
&self.common,
|
||||||
self.duration,
|
if self.continuous {
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
self.duration
|
||||||
|
},
|
||||||
TargetSelection {
|
TargetSelection {
|
||||||
targets: &self.targets,
|
targets: &self.targets,
|
||||||
pacing: self.pacing,
|
pacing: self.pacing,
|
||||||
stdin_controlled: self.targets_stdin,
|
stdin_controlled: self.targets_stdin,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await?;
|
||||||
|
if self.no_test {
|
||||||
|
setup.client.request.run_mode = irtt_client::RunMode::NoTest;
|
||||||
|
}
|
||||||
|
Ok(setup)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_continuous(&self) -> bool {
|
pub fn is_continuous(&self) -> bool {
|
||||||
self.duration == Duration::ZERO
|
self.continuous || self.duration == Duration::ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timestamp_mode(&self) -> TimestampArg {
|
pub fn timestamp_mode(&self) -> TimestampArg {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod run;
|
pub mod run;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
pub mod summary;
|
pub mod summary;
|
||||||
|
|
||||||
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
pub use args::{ClientArgs, HeaderMode, OutputFormat};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use irtt_client::{
|
|||||||
ServerTiming, SignedDuration, WarningKind,
|
ServerTiming, SignedDuration, WarningKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate};
|
use irtt_stats::{EventStatsUpdate, IpdvPairUpdate};
|
||||||
|
|
||||||
use super::args::{HeaderMode, OutputFormat};
|
use super::args::{HeaderMode, OutputFormat};
|
||||||
@@ -48,10 +49,12 @@ impl OutputConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
pub(super) fn prints_summary(&self) -> bool {
|
pub(super) fn prints_summary(&self) -> bool {
|
||||||
self.format.prints_summary()
|
self.format.prints_summary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
pub(super) fn summary_verbose(&self) -> bool {
|
pub(super) fn summary_verbose(&self) -> bool {
|
||||||
self.verbose
|
self.verbose
|
||||||
}
|
}
|
||||||
@@ -96,15 +99,22 @@ impl OutputConfig {
|
|||||||
&self,
|
&self,
|
||||||
event: &ClientEvent,
|
event: &ClientEvent,
|
||||||
target: Option<&str>,
|
target: Option<&str>,
|
||||||
stats: Option<&EventStatsUpdate>,
|
#[cfg(feature = "stats")] stats: Option<&EventStatsUpdate>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let row = OutputRow::from_event(event);
|
let row = OutputRow::from_event(event);
|
||||||
if self.default_table_rows && row.is_default_table_hidden() {
|
if self.default_table_rows && row.is_default_table_hidden() {
|
||||||
return None;
|
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 {
|
let context = RenderContext {
|
||||||
stats,
|
ipdv_ns,
|
||||||
target,
|
target,
|
||||||
verbose: self.verbose,
|
verbose: self.verbose,
|
||||||
};
|
};
|
||||||
@@ -155,7 +165,7 @@ impl OutputConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct RenderContext<'a> {
|
struct RenderContext<'a> {
|
||||||
stats: Option<&'a EventStatsUpdate>,
|
ipdv_ns: Option<i128>,
|
||||||
target: Option<&'a str>,
|
target: Option<&'a str>,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
}
|
}
|
||||||
@@ -541,7 +551,7 @@ columns! {
|
|||||||
SendDelay("sd", ["send_delay"], 9, right, "human-readable client-to-server delay");
|
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");
|
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");
|
Ipdv("ipdv", [], 9, right, "human-readable round-trip IPDV for adjacent samples");
|
||||||
IpdvUs("ipdv_us", [], 10, right, "round-trip IPDV in microseconds");
|
IpdvUs("ipdv_us", [], 10, right, "round-trip IPDV in signed microseconds");
|
||||||
ServerProcessing("proc", ["server_processing"], 9, right, "human-readable server processing time");
|
ServerProcessing("proc", ["server_processing"], 9, right, "human-readable server processing time");
|
||||||
ServerProcessingUs("server_processing_us", [], 20, right, "server processing time in microseconds");
|
ServerProcessingUs("server_processing_us", [], 20, right, "server processing time in microseconds");
|
||||||
Bytes("bytes", [], 10, right, "packet bytes for packet events");
|
Bytes("bytes", [], 10, right, "packet bytes for packet events");
|
||||||
@@ -817,14 +827,12 @@ fn cell_for(row: &OutputRow, column: Column, context: RenderContext<'_>) -> Opti
|
|||||||
.reply()
|
.reply()
|
||||||
.and_then(|reply| reply.one_way.and_then(|one_way| one_way.client_to_server))
|
.and_then(|reply| reply.one_way.and_then(|one_way| one_way.client_to_server))
|
||||||
.map(|value| CellValue::Integer(signed_duration_us(value))),
|
.map(|value| CellValue::Integer(signed_duration_us(value))),
|
||||||
Column::Ipdv => row
|
Column::Ipdv => context.ipdv_ns.map(|value| {
|
||||||
.seq()
|
CellValue::text(format_signed_duration(SignedDuration::from_nanos(value)))
|
||||||
.and_then(|seq| ipdv_pair(context.stats, seq))
|
}),
|
||||||
.map(|pair| CellValue::text(format_duration(pair.rtt_ipdv))),
|
Column::IpdvUs => context
|
||||||
Column::IpdvUs => row
|
.ipdv_ns
|
||||||
.seq()
|
.map(|value| CellValue::Integer(value / 1_000)),
|
||||||
.and_then(|seq| ipdv_pair(context.stats, seq))
|
|
||||||
.map(|pair| CellValue::Unsigned(duration_us(pair.rtt_ipdv))),
|
|
||||||
Column::ServerProcessing => row
|
Column::ServerProcessing => row
|
||||||
.reply()
|
.reply()
|
||||||
.and_then(|reply| reply.server_timing.and_then(|timing| timing.processing))
|
.and_then(|reply| reply.server_timing.and_then(|timing| timing.processing))
|
||||||
@@ -931,6 +939,7 @@ fn server_timing_i64(
|
|||||||
.map(|value| CellValue::Integer(i128::from(value)))
|
.map(|value| CellValue::Integer(i128::from(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn ipdv_pair(stats: Option<&EventStatsUpdate>, seq: u32) -> Option<&IpdvPairUpdate> {
|
fn ipdv_pair(stats: Option<&EventStatsUpdate>, seq: u32) -> Option<&IpdvPairUpdate> {
|
||||||
let stats = stats?;
|
let stats = stats?;
|
||||||
stats
|
stats
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
|
collections::{HashSet, VecDeque},
|
||||||
io::{self, BufRead, Write},
|
io::{self, BufRead, Write},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
thread,
|
thread,
|
||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
|
|
||||||
use irtt_client::{
|
use irtt_client::{
|
||||||
managed::{
|
managed::{
|
||||||
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent, ManagedStatus,
|
ManagedClient, ManagedCommandApplyError, ManagedEndReason, ManagedEvent,
|
||||||
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
ManagedTargetEndReason, ManagedTargetOutcome, TargetInstance,
|
||||||
},
|
},
|
||||||
ClientEvent,
|
ClientEvent,
|
||||||
@@ -16,26 +16,25 @@ use irtt_client::{
|
|||||||
use super::{args::ClientArgs, output::OutputConfig};
|
use super::{args::ClientArgs, output::OutputConfig};
|
||||||
|
|
||||||
use crate::shared::client::{
|
use crate::shared::client::{
|
||||||
expected_probe_count, parse_stdin_target_set,
|
parse_stdin_target_set,
|
||||||
session::{
|
session::{
|
||||||
drain_final_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
drain_final_events, peer_close_run_error, request_managed_stop_for_peer_close,
|
||||||
request_managed_stop_once, should_print_final_summary,
|
request_managed_stop_once,
|
||||||
},
|
},
|
||||||
worker::ManagedWorker,
|
worker::ManagedWorker,
|
||||||
ManagedRunSetup, TargetSpec, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
ManagedRunSetup, TargetSpec, STDIN_MAX_DESIRED_TARGETS, STDIN_OUTCOME_HISTORY_LIMIT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
use irtt_stats::{EventStatsUpdate, StatsCollector, StatsConfig};
|
use irtt_stats::{EventStatsUpdate, StatsCollector, StatsConfig};
|
||||||
|
|
||||||
const MIB: u64 = 1024 * 1024;
|
#[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 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;
|
const MAX_STDIN_RECORD_BYTES: usize = 64 * 1024;
|
||||||
|
|
||||||
pub async fn run_stream(
|
pub async fn run_stream(
|
||||||
@@ -50,6 +49,7 @@ pub async fn run_stream(
|
|||||||
result = args.prepare() => result.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?,
|
result = args.prepare() => result.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?,
|
||||||
_ = shutdown.wait_for(|requested| *requested) => return Ok(()),
|
_ = shutdown.wait_for(|requested| *requested) => return Ok(()),
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let multi_target = setup.is_multi_target();
|
let multi_target = setup.is_multi_target();
|
||||||
let output_config = OutputConfig::new(
|
let output_config = OutputConfig::new(
|
||||||
args.format,
|
args.format,
|
||||||
@@ -58,34 +58,55 @@ pub async fn run_stream(
|
|||||||
args.verbose,
|
args.verbose,
|
||||||
)
|
)
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
|
.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 {
|
if setup.stdin_controlled {
|
||||||
return run_stdin_stream(setup, output_config, shutdown).await;
|
return run_stdin_stream(setup, output_config, shutdown, &args).await;
|
||||||
}
|
}
|
||||||
let continuous = args.is_continuous();
|
let continuous = args.is_continuous();
|
||||||
let target_count = setup.target_count();
|
|
||||||
|
|
||||||
if let Some(warning) = finite_stats_memory_warning(&args, target_count) {
|
#[cfg(feature = "stats")]
|
||||||
|
if let Some(warning) = finite_stats_memory_warning(&args, setup.target_count()) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
}
|
}
|
||||||
if *shutdown.borrow() {
|
if *shutdown.borrow() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let (task, handle) = ManagedClient::task(setup.managed_config(), setup.managed_targets())?;
|
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 mut events = handle.subscribe()?;
|
let mut events = handle.subscribe()?;
|
||||||
let mut status = handle.subscribe_status();
|
let mut status = handle.subscribe_status();
|
||||||
let owner = ManagedWorker::start(task, handle.clone())?;
|
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
continuous: args.is_continuous(),
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
collect_stats: !args.no_stats,
|
||||||
config: output_config,
|
config: output_config,
|
||||||
header_printed: false,
|
header_printed: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
print_final_summary: false,
|
print_final_summary: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
show_running_only_summary_note: false,
|
show_running_only_summary_note: false,
|
||||||
out: &mut stdout,
|
out: &mut stdout,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let mut stats = setup
|
let mut stats = setup
|
||||||
.targets
|
.targets
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|_| !args.no_stats)
|
||||||
.map(|target| {
|
.map(|target| {
|
||||||
(
|
(
|
||||||
target.label.clone(),
|
target.label.clone(),
|
||||||
@@ -120,7 +141,7 @@ pub async fn run_stream(
|
|||||||
_ = shutdown.changed(), if !interrupted => {}
|
_ = shutdown.changed(), if !interrupted => {}
|
||||||
changed = status.changed() => { if changed.is_err() { break; } }
|
changed = status.changed() => { if changed.is_err() { break; } }
|
||||||
event = events.recv() => match event {
|
event = events.recv() => match event {
|
||||||
Ok(event) => process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?,
|
Ok(event) => process_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?,
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
dropped_events = dropped_events.saturating_add(count);
|
dropped_events = dropped_events.saturating_add(count);
|
||||||
}
|
}
|
||||||
@@ -133,7 +154,13 @@ pub async fn run_stream(
|
|||||||
}
|
}
|
||||||
let outcome = owner.join().await?;
|
let outcome = owner.join().await?;
|
||||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||||
process_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
process_event(
|
||||||
|
event,
|
||||||
|
&mut stream_output,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
&mut stats,
|
||||||
|
&mut terminal_targets,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
@@ -167,21 +194,25 @@ pub async fn run_stream(
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
stream_output.print_final_summary = should_print_final_summary(continuous, interrupted);
|
#[cfg(feature = "stats")]
|
||||||
stream_output.show_running_only_summary_note =
|
{
|
||||||
continuous && interrupted && stream_output.print_final_summary;
|
stream_output.print_final_summary =
|
||||||
for target in &setup.targets {
|
!args.no_stats && !args.no_test && should_print_final_summary(continuous, interrupted);
|
||||||
let target_stats = stats.get(&target.label).expect(
|
stream_output.show_running_only_summary_note =
|
||||||
"every prepared target must have a stats collector inserted before the run begins",
|
continuous && interrupted && stream_output.print_final_summary;
|
||||||
);
|
for target in &setup.targets {
|
||||||
if multi_target
|
let Some(target_stats) = stats.get(&target.label) else {
|
||||||
&& stream_output.print_final_summary
|
continue;
|
||||||
&& stream_output.config.prints_summary()
|
};
|
||||||
{
|
if multi_target
|
||||||
writeln!(stream_output.out)?;
|
&& stream_output.print_final_summary
|
||||||
writeln!(stream_output.out, "target: {}", target.label)?;
|
&& 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_summary(target_stats)?;
|
|
||||||
}
|
}
|
||||||
stream_output.out.flush()?;
|
stream_output.out.flush()?;
|
||||||
if let Some(error) = terminal_error {
|
if let Some(error) = terminal_error {
|
||||||
@@ -356,21 +387,33 @@ impl BoundedTargetSet {
|
|||||||
fn process_stdin_event<W: Write>(
|
fn process_stdin_event<W: Write>(
|
||||||
event: ManagedEvent,
|
event: ManagedEvent,
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
stream_output: &mut StreamOutput<'_, W>,
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
#[cfg(feature = "stats")] stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
terminal_targets: &mut BoundedTargetSet,
|
terminal_targets: &mut BoundedTargetSet,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
match event {
|
match event {
|
||||||
ManagedEvent::TargetStateChanged { target, .. } => {
|
#[cfg(feature = "stats")]
|
||||||
|
ManagedEvent::TargetStateChanged { target, .. } if stream_output.collect_stats => {
|
||||||
stats
|
stats
|
||||||
.entry(target)
|
.entry(target)
|
||||||
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
||||||
}
|
}
|
||||||
ManagedEvent::Client { target, event } => {
|
ManagedEvent::Client { target, event } => {
|
||||||
let collector = stats
|
#[cfg(feature = "stats")]
|
||||||
.entry(target.clone())
|
let update = stream_output.collect_stats.then(|| {
|
||||||
.or_insert_with(|| StatsCollector::new(stats_config(true)));
|
let collector = stats
|
||||||
let update = collector.process(&event);
|
.entry(target.clone())
|
||||||
stream_output.print_event(&event, Some(target.id.as_str()), &update)?;
|
.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(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ManagedEvent::TargetFinished { outcome }
|
ManagedEvent::TargetFinished { outcome }
|
||||||
if terminal_targets.insert(outcome.target.clone()) =>
|
if terminal_targets.insert(outcome.target.clone()) =>
|
||||||
@@ -382,6 +425,7 @@ fn process_stdin_event<W: Write>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn reconcile_stdin_stats(
|
fn reconcile_stdin_stats(
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
status: &ManagedStatus,
|
status: &ManagedStatus,
|
||||||
@@ -398,10 +442,15 @@ fn reconcile_stdin_stats(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn snapshot_stdin_summary_targets(
|
fn snapshot_stdin_summary_targets(
|
||||||
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
stats: &mut BTreeMap<TargetInstance, StatsCollector>,
|
||||||
status: &ManagedStatus,
|
status: &ManagedStatus,
|
||||||
|
enabled: bool,
|
||||||
) -> BTreeSet<TargetInstance> {
|
) -> BTreeSet<TargetInstance> {
|
||||||
|
if !enabled {
|
||||||
|
return BTreeSet::new();
|
||||||
|
}
|
||||||
let targets = status
|
let targets = status
|
||||||
.targets
|
.targets
|
||||||
.iter()
|
.iter()
|
||||||
@@ -426,11 +475,21 @@ async fn run_stdin_stream(
|
|||||||
setup: ManagedRunSetup,
|
setup: ManagedRunSetup,
|
||||||
output_config: OutputConfig,
|
output_config: OutputConfig,
|
||||||
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
mut shutdown: tokio::sync::watch::Receiver<bool>,
|
||||||
|
args: &ClientArgs,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if *shutdown.borrow() {
|
if *shutdown.borrow() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let (task, handle) = ManagedClient::task(setup.managed_config(), setup.managed_targets())?;
|
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 mut events = handle.subscribe()?;
|
let mut events = handle.subscribe()?;
|
||||||
let mut status = handle.subscribe_status();
|
let mut status = handle.subscribe_status();
|
||||||
let owner = ManagedWorker::start(task, handle.clone())?;
|
let owner = ManagedWorker::start(task, handle.clone())?;
|
||||||
@@ -439,12 +498,19 @@ async fn run_stdin_stream(
|
|||||||
|
|
||||||
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
let mut stdout = io::LineWriter::new(io::stdout().lock());
|
||||||
let mut stream_output = StreamOutput {
|
let mut stream_output = StreamOutput {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
continuous: args.is_continuous(),
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
collect_stats: !args.no_stats,
|
||||||
config: output_config,
|
config: output_config,
|
||||||
header_printed: false,
|
header_printed: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
print_final_summary: false,
|
print_final_summary: false,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
show_running_only_summary_note: false,
|
show_running_only_summary_note: false,
|
||||||
out: &mut stdout,
|
out: &mut stdout,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let mut stats = BTreeMap::new();
|
let mut stats = BTreeMap::new();
|
||||||
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
let mut terminal_targets = BoundedTargetSet::new(STDIN_OUTCOME_HISTORY_LIMIT);
|
||||||
let mut stdin_changed = false;
|
let mut stdin_changed = false;
|
||||||
@@ -458,6 +524,7 @@ async fn run_stdin_stream(
|
|||||||
let mut dropped_events = 0_u64;
|
let mut dropped_events = 0_u64;
|
||||||
let mut stop_requested = false;
|
let mut stop_requested = false;
|
||||||
let mut stop = None;
|
let mut stop = None;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
let mut summary_targets = None;
|
let mut summary_targets = None;
|
||||||
loop {
|
loop {
|
||||||
let snapshot = status.borrow_and_update().clone();
|
let snapshot = status.borrow_and_update().clone();
|
||||||
@@ -509,12 +576,20 @@ async fn run_stdin_stream(
|
|||||||
}
|
}
|
||||||
if stop.is_some() {
|
if stop.is_some() {
|
||||||
preparing = None;
|
preparing = None;
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &snapshot));
|
#[cfg(feature = "stats")]
|
||||||
|
{
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(
|
||||||
|
&mut stats,
|
||||||
|
&snapshot,
|
||||||
|
!args.no_stats,
|
||||||
|
));
|
||||||
|
}
|
||||||
if request_managed_stop_once(&mut stop_requested) {
|
if request_managed_stop_once(&mut stop_requested) {
|
||||||
drop(handle.stop());
|
drop(handle.stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
reconcile_stdin_stats(&mut stats, &snapshot, summary_targets.as_ref());
|
reconcile_stdin_stats(&mut stats, &snapshot, summary_targets.as_ref());
|
||||||
if snapshot.final_outcome.is_some() {
|
if snapshot.final_outcome.is_some() {
|
||||||
break;
|
break;
|
||||||
@@ -536,7 +611,10 @@ async fn run_stdin_stream(
|
|||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
stop = Some(StdinStop::Fatal(format!("invalid --targets-stdin target set: {error}")));
|
stop = Some(StdinStop::Fatal(format!("invalid --targets-stdin target set: {error}")));
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow()));
|
#[cfg(feature = "stats")]
|
||||||
|
{
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
||||||
|
}
|
||||||
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,14 +635,18 @@ async fn run_stdin_stream(
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
stop = Some(StdinStop::Fatal("--targets-stdin update was rejected".to_owned()));
|
stop = Some(StdinStop::Fatal("--targets-stdin update was rejected".to_owned()));
|
||||||
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow()));
|
#[cfg(feature = "stats")]
|
||||||
|
{
|
||||||
|
summary_targets = Some(snapshot_stdin_summary_targets(&mut stats, &status.borrow(), !args.no_stats));
|
||||||
|
}
|
||||||
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
if request_managed_stop_once(&mut stop_requested) { drop(handle.stop()); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event = events.recv() => match event {
|
event = events.recv() => match event {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)?;
|
process_stdin_event(event, &mut stream_output, #[cfg(feature = "stats")] &mut stats, &mut terminal_targets)?;
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
reconcile_stdin_stats(&mut stats, &status.borrow(), summary_targets.as_ref());
|
reconcile_stdin_stats(&mut stats, &status.borrow(), summary_targets.as_ref());
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
@@ -577,7 +659,13 @@ async fn run_stdin_stream(
|
|||||||
|
|
||||||
let outcome = owner.join().await?;
|
let outcome = owner.join().await?;
|
||||||
drain_final_events(&mut events, &mut dropped_events, |event| {
|
drain_final_events(&mut events, &mut dropped_events, |event| {
|
||||||
process_stdin_event(event, &mut stream_output, &mut stats, &mut terminal_targets)
|
process_stdin_event(
|
||||||
|
event,
|
||||||
|
&mut stream_output,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
&mut stats,
|
||||||
|
&mut terminal_targets,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
if let Some(warning) = dropped_event_warning(dropped_events) {
|
if let Some(warning) = dropped_event_warning(dropped_events) {
|
||||||
eprintln!("{warning}");
|
eprintln!("{warning}");
|
||||||
@@ -594,24 +682,28 @@ async fn run_stdin_stream(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let print_summary = matches!(stop, Some(StdinStop::Interrupted | StdinStop::Eof));
|
#[cfg(feature = "stats")]
|
||||||
stream_output.print_final_summary = print_summary;
|
{
|
||||||
stream_output.show_running_only_summary_note = print_summary;
|
let print_summary =
|
||||||
if let Some(summary_targets) = summary_targets {
|
!args.no_stats && matches!(stop, Some(StdinStop::Interrupted | StdinStop::Eof));
|
||||||
let multi_target = summary_targets.len() > 1;
|
stream_output.print_final_summary = print_summary;
|
||||||
for target in summary_targets {
|
stream_output.show_running_only_summary_note = print_summary;
|
||||||
let stats = stats
|
if let Some(summary_targets) = summary_targets {
|
||||||
.get(&target)
|
let multi_target = summary_targets.len() > 1;
|
||||||
.expect("summary snapshot retains every selected target collector");
|
for target in summary_targets {
|
||||||
if multi_target && stream_output.config.prints_summary() {
|
let stats = stats
|
||||||
writeln!(stream_output.out)?;
|
.get(&target)
|
||||||
writeln!(
|
.expect("summary snapshot retains every selected target collector");
|
||||||
stream_output.out,
|
if multi_target && stream_output.config.prints_summary() {
|
||||||
"target: {} (generation {})",
|
writeln!(stream_output.out)?;
|
||||||
target.id, target.generation
|
writeln!(
|
||||||
)?;
|
stream_output.out,
|
||||||
|
"target: {} (generation {})",
|
||||||
|
target.id, target.generation
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
stream_output.print_summary(stats)?;
|
||||||
}
|
}
|
||||||
stream_output.print_summary(stats)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stream_output.out.flush()?;
|
stream_output.out.flush()?;
|
||||||
@@ -630,16 +722,28 @@ async fn run_stdin_stream(
|
|||||||
fn process_event<W: Write>(
|
fn process_event<W: Write>(
|
||||||
event: ManagedEvent,
|
event: ManagedEvent,
|
||||||
stream_output: &mut StreamOutput<'_, W>,
|
stream_output: &mut StreamOutput<'_, W>,
|
||||||
stats: &mut BTreeMap<String, StatsCollector>,
|
#[cfg(feature = "stats")] stats: &mut BTreeMap<String, StatsCollector>,
|
||||||
terminal_targets: &mut HashSet<TargetInstance>,
|
terminal_targets: &mut HashSet<TargetInstance>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
match event {
|
match event {
|
||||||
ManagedEvent::Client { target, event } => {
|
ManagedEvent::Client { target, event } => {
|
||||||
let collector = stats
|
#[cfg(feature = "stats")]
|
||||||
.entry(target.id.as_str().to_owned())
|
let update = stream_output.collect_stats.then(|| {
|
||||||
.or_insert_with(|| StatsCollector::new(stats_config(false)));
|
let collector = stats
|
||||||
let update = collector.process(&event);
|
.get_mut(target.id.as_str())
|
||||||
stream_output.print_event(&event, Some(target.id.as_str()), &update)?;
|
.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(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ManagedEvent::TargetFinished { outcome } => {
|
ManagedEvent::TargetFinished { outcome } => {
|
||||||
terminal_targets.insert(outcome.target.clone());
|
terminal_targets.insert(outcome.target.clone());
|
||||||
@@ -673,13 +777,24 @@ fn target_failure_messages(target: &ManagedTargetOutcome) -> Vec<String> {
|
|||||||
messages
|
messages
|
||||||
}
|
}
|
||||||
fn dropped_event_warning(dropped_events: u64) -> Option<String> {
|
fn dropped_event_warning(dropped_events: u64) -> Option<String> {
|
||||||
(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" }))
|
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" }))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamOutput<'a, W: Write> {
|
struct StreamOutput<'a, W: Write> {
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
continuous: bool,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
collect_stats: bool,
|
||||||
config: OutputConfig,
|
config: OutputConfig,
|
||||||
header_printed: bool,
|
header_printed: bool,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
print_final_summary: bool,
|
print_final_summary: bool,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
show_running_only_summary_note: bool,
|
show_running_only_summary_note: bool,
|
||||||
out: &'a mut W,
|
out: &'a mut W,
|
||||||
}
|
}
|
||||||
@@ -688,10 +803,15 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
event: &ClientEvent,
|
event: &ClientEvent,
|
||||||
target: Option<&str>,
|
target: Option<&str>,
|
||||||
update: &EventStatsUpdate,
|
#[cfg(feature = "stats")] update: Option<&EventStatsUpdate>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
self.print_header()?;
|
self.print_header()?;
|
||||||
if let Some(line) = self.config.render_event(event, target, Some(update)) {
|
if let Some(line) = self.config.render_event(
|
||||||
|
event,
|
||||||
|
target,
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
|
update,
|
||||||
|
) {
|
||||||
writeln!(self.out, "{line}")?;
|
writeln!(self.out, "{line}")?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -707,6 +827,7 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn print_summary(&mut self, stats: &StatsCollector) -> io::Result<()> {
|
fn print_summary(&mut self, stats: &StatsCollector) -> io::Result<()> {
|
||||||
if self.print_final_summary && self.config.prints_summary() {
|
if self.print_final_summary && self.config.prints_summary() {
|
||||||
write!(
|
write!(
|
||||||
@@ -725,6 +846,7 @@ impl<W: Write> StreamOutput<'_, W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn stats_config(continuous: bool) -> StatsConfig {
|
fn stats_config(continuous: bool) -> StatsConfig {
|
||||||
if continuous {
|
if continuous {
|
||||||
StatsConfig::continuous()
|
StatsConfig::continuous()
|
||||||
@@ -732,8 +854,18 @@ fn stats_config(continuous: bool) -> StatsConfig {
|
|||||||
StatsConfig::finite()
|
StatsConfig::finite()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "stats")]
|
||||||
fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option<String> {
|
fn finite_stats_memory_warning(args: &ClientArgs, target_count: usize) -> Option<String> {
|
||||||
if args.is_continuous() || args.duration.is_zero() {
|
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() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let target_count = u64::try_from(target_count).unwrap_or(u64::MAX);
|
let target_count = u64::try_from(target_count).unwrap_or(u64::MAX);
|
||||||
@@ -760,3 +892,8 @@ 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}"))
|
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,12 +57,13 @@ pub(crate) fn format_ns_i128(value: i128) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
/// 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 {
|
pub(crate) fn format_optional_ns_i128(value: Option<i128>) -> String {
|
||||||
value.map(format_ns_i128).unwrap_or_else(absent)
|
value.map(format_ns_i128).unwrap_or_else(absent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
/// Format an optional nanosecond scalar, rendering `None` as [`ABSENT`].
|
||||||
#[cfg(feature = "client")]
|
#[cfg(all(feature = "client", feature = "stats"))]
|
||||||
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> String {
|
pub(crate) fn format_optional_ns_f64(value: Option<f64>) -> String {
|
||||||
value.map(format_ns_f64).unwrap_or_else(absent)
|
value.map(format_ns_f64).unwrap_or_else(absent)
|
||||||
}
|
}
|
||||||
@@ -80,6 +81,7 @@ pub(crate) fn format_signed_duration(value: SignedDuration) -> String {
|
|||||||
|
|
||||||
/// Format a percentage with two decimals; non-finite values render as
|
/// Format a percentage with two decimals; non-finite values render as
|
||||||
/// [`ABSENT`].
|
/// [`ABSENT`].
|
||||||
|
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||||
pub(crate) fn format_percent(value: f64) -> String {
|
pub(crate) fn format_percent(value: f64) -> String {
|
||||||
if value.is_finite() {
|
if value.is_finite() {
|
||||||
format!("{value:.2}%")
|
format!("{value:.2}%")
|
||||||
@@ -112,6 +114,7 @@ pub(crate) fn format_optional_count(value: Option<u64>) -> String {
|
|||||||
value.map(format_count).unwrap_or_else(absent)
|
value.map(format_count).unwrap_or_else(absent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(all(feature = "client", feature = "stats"), feature = "tui"))]
|
||||||
fn absent() -> String {
|
fn absent() -> String {
|
||||||
ABSENT.to_owned()
|
ABSENT.to_owned()
|
||||||
}
|
}
|
||||||
@@ -12,9 +12,10 @@ const DEFAULT_PORT: u16 = 2112;
|
|||||||
// Every policy option is optional. An omitted option leaves the corresponding
|
// Every policy option is optional. An omitted option leaves the corresponding
|
||||||
// `ServerConfig` default in place rather than restating it here, so library and
|
// `ServerConfig` default in place rather than restating it here, so library and
|
||||||
// CLI defaults cannot drift apart.
|
// CLI defaults cannot drift apart.
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Clone, Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "irtt-server",
|
name = "irtt-server",
|
||||||
|
version,
|
||||||
about = "Minimal IRTT-compatible UDP server",
|
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."
|
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."
|
||||||
)]
|
)]
|
||||||
@@ -28,13 +29,14 @@ pub struct ServerArgs {
|
|||||||
)]
|
)]
|
||||||
pub bind: Vec<SocketAddr>,
|
pub bind: Vec<SocketAddr>,
|
||||||
|
|
||||||
|
// Decode during configuration preparation so Clap cannot echo rejected keys.
|
||||||
/// HMAC key; every request must then carry a valid MAC.
|
/// HMAC key; every request must then carry a valid MAC.
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
value_name = "KEY",
|
value_name = "KEY",
|
||||||
value_parser = parse_hmac_key,
|
|
||||||
env = "IRTT_SERVER_HMAC",
|
env = "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."
|
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."
|
||||||
)]
|
)]
|
||||||
pub hmac: Option<String>,
|
pub hmac: Option<String>,
|
||||||
|
|
||||||
@@ -126,6 +128,23 @@ pub struct ServerArgs {
|
|||||||
pub no_dscp: bool,
|
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`].
|
/// The command-line spelling of [`TimestampAllowance`].
|
||||||
///
|
///
|
||||||
/// A CLI-local enum rather than a `clap` derive on the library type: `irtt-server`
|
/// A CLI-local enum rather than a `clap` derive on the library type: `irtt-server`
|
||||||
@@ -166,11 +185,10 @@ impl ServerArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the server configuration this invocation asks for.
|
/// Builds the server configuration this invocation asks for.
|
||||||
#[must_use]
|
pub fn server_config(&self) -> Result<ServerConfig, String> {
|
||||||
pub fn server_config(&self) -> ServerConfig {
|
|
||||||
let mut config = ServerConfig::default();
|
let mut config = ServerConfig::default();
|
||||||
if let Some(key) = self.hmac.as_ref() {
|
if let Some(key) = self.hmac.as_ref() {
|
||||||
config = config.with_hmac_key(key.as_bytes());
|
config = config.with_hmac_key(crate::shared::decode_hmac_key(key)?);
|
||||||
}
|
}
|
||||||
if let Some(value) = self.max_sessions {
|
if let Some(value) = self.max_sessions {
|
||||||
config = config.with_max_sessions(value);
|
config = config.with_max_sessions(value);
|
||||||
@@ -198,19 +216,10 @@ impl ServerArgs {
|
|||||||
if self.no_dscp {
|
if self.no_dscp {
|
||||||
config = config.with_dscp_allowed(false);
|
config = config.with_dscp_allowed(false);
|
||||||
}
|
}
|
||||||
config
|
Ok(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.
|
/// Parses a duration whose zero is meaningful server policy.
|
||||||
fn parse_duration_allow_zero(input: &str) -> Result<Duration, String> {
|
fn parse_duration_allow_zero(input: &str) -> Result<Duration, String> {
|
||||||
if input == "0" {
|
if input == "0" {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async fn serve(
|
|||||||
args: ServerArgs,
|
args: ServerArgs,
|
||||||
shutdown_requested: &AtomicBool,
|
shutdown_requested: &AtomicBool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = args.server_config();
|
let config = args.server_config()?;
|
||||||
let binds = args.resolve_binds();
|
let binds = args.resolve_binds();
|
||||||
let is_implicit_default = args.bind.is_empty();
|
let is_implicit_default = args.bind.is_empty();
|
||||||
let set = match ServerSet::bind(binds.clone(), config.clone()).await {
|
let set = match ServerSet::bind(binds.clone(), config.clone()).await {
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ use crate::shared::client::{
|
|||||||
pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO;
|
pub const DEFAULT_TUI_DURATION: Duration = Duration::ZERO;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
#[command(name = "irtt-tui", about = "Minimal IRTT-compatible TUI client")]
|
#[command(
|
||||||
|
name = "irtt-tui",
|
||||||
|
version,
|
||||||
|
about = "Minimal IRTT-compatible TUI client"
|
||||||
|
)]
|
||||||
pub struct TuiArgs {
|
pub struct TuiArgs {
|
||||||
/// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.
|
/// Server address/host, optionally prefixed with LABEL= and suffixed with @hmac=KEY. Repeat for multi-target mode.
|
||||||
#[arg(
|
#[arg(
|
||||||
@@ -18,7 +22,7 @@ pub struct TuiArgs {
|
|||||||
num_args = 1..,
|
num_args = 1..,
|
||||||
required = true,
|
required = true,
|
||||||
value_parser = parse_target,
|
value_parser = parse_target,
|
||||||
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"
|
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"
|
||||||
)]
|
)]
|
||||||
pub targets: Vec<TargetArg>,
|
pub targets: Vec<TargetArg>,
|
||||||
|
|
||||||
@@ -33,13 +37,18 @@ pub struct TuiArgs {
|
|||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
|
short = 'd',
|
||||||
default_value = "0",
|
default_value = "0",
|
||||||
value_parser = parse_test_duration,
|
value_parser = parse_test_duration,
|
||||||
help = "Test duration; use 0 for continuous mode",
|
help = "Test duration (e.g. 2.5s, 1m30s, 1h); use --continuous for continuous mode (0 is also accepted)",
|
||||||
long_help = "Test duration; use 0 for continuous mode. The TUI defaults to continuous mode."
|
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."
|
||||||
)]
|
)]
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
|
||||||
|
/// Run continuously until interrupted (equivalent to --duration 0).
|
||||||
|
#[arg(long, conflicts_with = "duration")]
|
||||||
|
pub continuous: bool,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub common: CommonClientArgs,
|
pub common: CommonClientArgs,
|
||||||
}
|
}
|
||||||
@@ -52,7 +61,11 @@ impl TuiArgs {
|
|||||||
pub async fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
pub async fn prepare(&self) -> Result<ManagedRunSetup, String> {
|
||||||
prepare_managed_run(
|
prepare_managed_run(
|
||||||
&self.common,
|
&self.common,
|
||||||
self.duration,
|
if self.continuous {
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
self.duration
|
||||||
|
},
|
||||||
TargetSelection {
|
TargetSelection {
|
||||||
targets: &self.targets,
|
targets: &self.targets,
|
||||||
pacing: self.pacing,
|
pacing: self.pacing,
|
||||||
@@ -63,7 +76,7 @@ impl TuiArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_continuous(&self) -> bool {
|
pub fn is_continuous(&self) -> bool {
|
||||||
self.duration == Duration::ZERO
|
self.continuous || self.duration == Duration::ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,11 +89,10 @@ fn with_shutdown_flag(
|
|||||||
|
|
||||||
#[cfg(feature = "client")]
|
#[cfg(feature = "client")]
|
||||||
fn run_client_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_client_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
let shutdown = crate::signal::install_async_signal_handler()
|
let shutdown = crate::signal::install_async_signal_handler()
|
||||||
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
||||||
let args = crate::cmd::client::ClientArgs::parse_from(argv);
|
let args =
|
||||||
|
crate::version::parse::<crate::cmd::client::ClientArgs>(argv, crate::version::client);
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()?
|
.build()?
|
||||||
@@ -107,10 +106,9 @@ fn run_client_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Err
|
|||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
fn run_server_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_server_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
with_shutdown_flag(|shutdown_requested| {
|
with_shutdown_flag(|shutdown_requested| {
|
||||||
let args = crate::cmd::server::ServerArgs::parse_from(argv);
|
let args =
|
||||||
|
crate::version::parse::<crate::cmd::server::ServerArgs>(argv, crate::version::server);
|
||||||
crate::cmd::server::run_server(args, shutdown_requested)
|
crate::cmd::server::run_server(args, shutdown_requested)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -122,11 +120,9 @@ fn run_server_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Err
|
|||||||
|
|
||||||
#[cfg(feature = "tui")]
|
#[cfg(feature = "tui")]
|
||||||
fn run_tui_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_tui_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
let shutdown = crate::signal::install_async_signal_handler()
|
let shutdown = crate::signal::install_async_signal_handler()
|
||||||
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
.map_err(|err| format!("failed to install signal handler: {err}"))?;
|
||||||
let args = crate::cmd::tui::TuiArgs::parse_from(argv);
|
let args = crate::version::parse::<crate::cmd::tui::TuiArgs>(argv, crate::version::client);
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()?
|
.build()?
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ pub mod cmd;
|
|||||||
pub mod entry;
|
pub mod entry;
|
||||||
pub mod shared;
|
pub mod shared;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
|
||||||
|
mod version;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{fmt, time::Duration};
|
use std::{fmt, net::IpAddr, net::SocketAddr, time::Duration};
|
||||||
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use irtt_client::{MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH};
|
use irtt_client::{MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH};
|
||||||
@@ -18,16 +18,21 @@ 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.")]
|
#[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,
|
pub dual_stack: bool,
|
||||||
|
|
||||||
/// Probe interval.
|
/// Positive probe interval (e.g. 2.5s, 1m30s, 500us, 100ns, 1h).
|
||||||
#[arg(long, default_value = "1s", value_parser = parse_duration)]
|
#[arg(short = 'i', long, default_value = "1s", value_parser = parse_duration)]
|
||||||
pub interval: 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.
|
/// UDP payload length.
|
||||||
#[arg(long, default_value_t = 0, value_parser = parse_length)]
|
#[arg(short = 'l', long, default_value_t = 0, value_parser = parse_length)]
|
||||||
pub length: u32,
|
pub length: u32,
|
||||||
|
|
||||||
/// HMAC key.
|
// Decode during configuration preparation so Clap cannot echo rejected keys.
|
||||||
#[arg(long, value_parser = parse_hmac_key)]
|
/// HMAC key: literal UTF-8 or lowercase 0x followed by hexadecimal bytes.
|
||||||
|
#[arg(long)]
|
||||||
pub hmac: Option<String>,
|
pub hmac: Option<String>,
|
||||||
|
|
||||||
/// Clock mode to request.
|
/// Clock mode to request.
|
||||||
@@ -70,6 +75,38 @@ pub struct CommonClientArgs {
|
|||||||
#[arg(long, value_name = "1..=255", value_parser = parse_ttl)]
|
#[arg(long, value_name = "1..=255", value_parser = parse_ttl)]
|
||||||
pub ttl: Option<u32>,
|
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.
|
/// Accept safe server restrictions during negotiation.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub loose: bool,
|
pub loose: bool,
|
||||||
@@ -77,11 +114,13 @@ pub struct CommonClientArgs {
|
|||||||
|
|
||||||
impl fmt::Debug for CommonClientArgs {
|
impl fmt::Debug for CommonClientArgs {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("CommonClientArgs")
|
let mut debug = f.debug_struct("CommonClientArgs");
|
||||||
|
debug
|
||||||
.field("ipv4", &self.ipv4)
|
.field("ipv4", &self.ipv4)
|
||||||
.field("ipv6", &self.ipv6)
|
.field("ipv6", &self.ipv6)
|
||||||
.field("dual_stack", &self.dual_stack)
|
.field("dual_stack", &self.dual_stack)
|
||||||
.field("interval", &self.interval)
|
.field("interval", &self.interval)
|
||||||
|
.field("timeouts", &self.timeouts)
|
||||||
.field("length", &self.length)
|
.field("length", &self.length)
|
||||||
.field("hmac", &self.hmac.as_ref().map(|_| "[REDACTED]"))
|
.field("hmac", &self.hmac.as_ref().map(|_| "[REDACTED]"))
|
||||||
.field("clock", &self.clock)
|
.field("clock", &self.clock)
|
||||||
@@ -90,8 +129,14 @@ impl fmt::Debug for CommonClientArgs {
|
|||||||
.field("server_fill", &self.server_fill)
|
.field("server_fill", &self.server_fill)
|
||||||
.field("dscp", &self.dscp)
|
.field("dscp", &self.dscp)
|
||||||
.field("ttl", &self.ttl)
|
.field("ttl", &self.ttl)
|
||||||
.field("loose", &self.loose)
|
.field("bind_addr", &self.bind_addr);
|
||||||
.finish()
|
#[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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,63 +197,26 @@ 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> {
|
pub fn parse_duration(input: &str) -> Result<Duration, String> {
|
||||||
let (number, unit) = split_duration(input)?;
|
let duration = parse_test_duration(input)?;
|
||||||
let value: u64 = number
|
if duration.is_zero() {
|
||||||
.parse()
|
|
||||||
.map_err(|_| format!("invalid duration value {input:?}"))?;
|
|
||||||
if value == 0 {
|
|
||||||
return Err("duration must be greater than zero".to_owned());
|
return Err("duration must be greater than zero".to_owned());
|
||||||
}
|
}
|
||||||
match unit {
|
Ok(duration)
|
||||||
"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> {
|
pub fn parse_test_duration(input: &str) -> Result<Duration, String> {
|
||||||
if input == "0" {
|
if input == "0" {
|
||||||
return Ok(Duration::ZERO);
|
return Ok(Duration::ZERO);
|
||||||
}
|
}
|
||||||
let (number, unit) = split_duration(input)?;
|
humantime::parse_duration(input).map_err(|error| error.to_string())
|
||||||
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> {
|
pub fn parse_length(input: &str) -> Result<u32, String> {
|
||||||
@@ -221,13 +229,6 @@ pub fn parse_length(input: &str) -> Result<u32, String> {
|
|||||||
Ok(length)
|
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> {
|
pub fn parse_server_fill(input: &str) -> Result<String, String> {
|
||||||
if input.is_empty() {
|
if input.is_empty() {
|
||||||
return Err("server fill must not be empty".to_owned());
|
return Err("server fill must not be empty".to_owned());
|
||||||
|
|||||||
@@ -21,17 +21,22 @@ impl CommonClientArgs {
|
|||||||
/// Build the shared client configuration template for a managed run.
|
/// Build the shared client configuration template for a managed run.
|
||||||
///
|
///
|
||||||
/// The config is endpoint-independent; managed targets own their addresses.
|
/// The config is endpoint-independent; managed targets own their addresses.
|
||||||
pub fn to_client_config(&self, duration: Duration) -> ClientConfig {
|
pub fn to_client_config(&self, duration: Duration) -> Result<ClientConfig, String> {
|
||||||
ClientConfig {
|
let mut open = OpenPolicy {
|
||||||
address_family: self.address_family(),
|
negotiation: if self.loose {
|
||||||
open: OpenPolicy {
|
NegotiationPolicy::Loose
|
||||||
negotiation: if self.loose {
|
} else {
|
||||||
NegotiationPolicy::Loose
|
NegotiationPolicy::Strict
|
||||||
} else {
|
|
||||||
NegotiationPolicy::Strict
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if !self.timeouts.is_empty() {
|
||||||
|
open.timeouts.clone_from(&self.timeouts);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ClientConfig {
|
||||||
|
address_family: self.address_family(),
|
||||||
|
open,
|
||||||
request: SessionRequest {
|
request: SessionRequest {
|
||||||
duration: (!duration.is_zero()).then_some(duration),
|
duration: (!duration.is_zero()).then_some(duration),
|
||||||
interval: self.interval,
|
interval: self.interval,
|
||||||
@@ -45,16 +50,24 @@ impl CommonClientArgs {
|
|||||||
},
|
},
|
||||||
auth: self
|
auth: self
|
||||||
.hmac
|
.hmac
|
||||||
.as_ref()
|
.as_deref()
|
||||||
|
.map(crate::shared::decode_hmac_key)
|
||||||
|
.transpose()?
|
||||||
.map_or(Authentication::Unauthenticated, |key| {
|
.map_or(Authentication::Unauthenticated, |key| {
|
||||||
Authentication::Hmac(HmacKey::new(key.as_bytes()))
|
Authentication::Hmac(HmacKey::new(key))
|
||||||
}),
|
}),
|
||||||
socket: SocketConfig {
|
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,
|
ttl: self.ttl,
|
||||||
..SocketConfig::default()
|
|
||||||
},
|
},
|
||||||
..ClientConfig::default()
|
..ClientConfig::default()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ pub async fn prepare_managed_run(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(ManagedRunSetup {
|
Ok(ManagedRunSetup {
|
||||||
targets,
|
targets,
|
||||||
client: common.to_client_config(duration),
|
client: common.to_client_config(duration)?,
|
||||||
pacing: selection.pacing.into(),
|
pacing: selection.pacing.into(),
|
||||||
stdin_controlled: selection.stdin_controlled,
|
stdin_controlled: selection.stdin_controlled,
|
||||||
preparation,
|
preparation,
|
||||||
|
|||||||
@@ -88,12 +88,13 @@ impl From<GroupPacingArg> for ManagedPacing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
enum TargetParseError {
|
enum TargetParseError {
|
||||||
EmptyLabel,
|
EmptyLabel,
|
||||||
EmptyAddress,
|
EmptyAddress,
|
||||||
InvalidEscape,
|
InvalidEscape,
|
||||||
EmptyTarget,
|
EmptyTarget,
|
||||||
|
InvalidHmac(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for TargetParseError {
|
impl fmt::Display for TargetParseError {
|
||||||
@@ -103,6 +104,7 @@ impl fmt::Display for TargetParseError {
|
|||||||
Self::EmptyAddress => "target address must not be empty",
|
Self::EmptyAddress => "target address must not be empty",
|
||||||
Self::InvalidEscape => "invalid target escape sequence",
|
Self::InvalidEscape => "invalid target escape sequence",
|
||||||
Self::EmptyTarget => "target must not be empty",
|
Self::EmptyTarget => "target must not be empty",
|
||||||
|
Self::InvalidHmac(message) => message,
|
||||||
};
|
};
|
||||||
f.write_str(message)
|
f.write_str(message)
|
||||||
}
|
}
|
||||||
@@ -197,7 +199,10 @@ fn parse_target_syntax(
|
|||||||
let auth = if value.is_empty() {
|
let auth = if value.is_empty() {
|
||||||
TargetAuth::Override(Authentication::Unauthenticated)
|
TargetAuth::Override(Authentication::Unauthenticated)
|
||||||
} else {
|
} else {
|
||||||
TargetAuth::Override(Authentication::Hmac(HmacKey::new(value.into_bytes())))
|
TargetAuth::Override(Authentication::Hmac(HmacKey::new(
|
||||||
|
crate::shared::decode_hmac_key(&value)
|
||||||
|
.map_err(TargetParseError::InvalidHmac)?,
|
||||||
|
)))
|
||||||
};
|
};
|
||||||
(&input[..index], auth)
|
(&input[..index], auth)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/// 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,2 +1,7 @@
|
|||||||
#[cfg(any(feature = "client", feature = "tui"))]
|
#[cfg(any(feature = "client", feature = "tui"))]
|
||||||
pub mod client;
|
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;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#[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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-client"
|
name = "irtt-client"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
description = "IRTT-compatible Rust client library"
|
description = "IRTT-compatible Rust client library"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["irtt", "networking", "latency", "rtt"]
|
keywords = ["irtt", "networking", "latency", "rtt"]
|
||||||
categories = ["network-programming"]
|
categories = ["network-programming"]
|
||||||
@@ -15,7 +14,7 @@ categories = ["network-programming"]
|
|||||||
irtt-proto.workspace = true
|
irtt-proto.workspace = true
|
||||||
socket2 = { version = "0.6.5", features = ["all"] }
|
socket2 = { version = "0.6.5", features = ["all"] }
|
||||||
thiserror = "2.0.21"
|
thiserror = "2.0.21"
|
||||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync"], optional = true}
|
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync"], optional = true}
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
nix = { version = "0.31.3", default-features = false, features = ["net", "uio"], optional = true }
|
nix = { version = "0.31.3", default-features = false, features = ["net", "uio"], optional = true }
|
||||||
|
|||||||
@@ -35,6 +35,21 @@ Use `ClientEvent::from(receipt)` for an `EchoSent` event with `scheduled_at` and
|
|||||||
`timer_error` absent; the managed driver supplies those schedule measurements
|
`timer_error` absent; the managed driver supplies those schedule measurements
|
||||||
and publishes committed sends even when post-send processing fails.
|
and publishes committed sends even when post-send processing fails.
|
||||||
|
|
||||||
|
Managed continuous measurement runs can opt into automatic recovery with
|
||||||
|
`ManagedClientConfig.reconnect_timeout: Some(timeout)`. Initial connection
|
||||||
|
failures, closed/failed sessions, server-limited completion, and reply silence
|
||||||
|
while sending trigger independent per-target retries. `reconnect_initial_delay`
|
||||||
|
(default 250 ms) doubles to `reconnect_max_delay` (default 10 s); a valid reply
|
||||||
|
resets the delay. Both delays must be positive and schedulable, with the maximum
|
||||||
|
at least twice the initial delay. Explicit stop and target removal cancel retries.
|
||||||
|
Recovery is disabled by default and is invalid for finite or negotiation-only runs.
|
||||||
|
|
||||||
|
In 0.7, complete `ManagedClientConfig` literals must include the three recovery
|
||||||
|
fields (or use `..Default::default()`), and exhaustive `ManagedConfigError`
|
||||||
|
matches must handle the new recovery-policy variants. A target can emit repeated
|
||||||
|
session-start events within one instance; reset session-bound state, including
|
||||||
|
statistics, for each reopened session. Use `irtt-stats` 0.6 with client 0.7 events.
|
||||||
|
|
||||||
Tokio stays optional: the default build has no runtime and no Tokio
|
Tokio stays optional: the default build has no runtime and no Tokio
|
||||||
dependency at all.
|
dependency at all.
|
||||||
|
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ pub(crate) struct AsyncOpenState {
|
|||||||
deadline: Option<Instant>,
|
deadline: Option<Instant>,
|
||||||
deadline_timer: Option<Pin<Box<time::Sleep>>>,
|
deadline_timer: Option<Pin<Box<time::Sleep>>>,
|
||||||
request_submitted: bool,
|
request_submitted: bool,
|
||||||
stop_after_current_attempt: bool,
|
|
||||||
stopped_after_current_attempt: bool,
|
|
||||||
buffer: [u8; MAX_OPEN_PACKET_SIZE],
|
buffer: [u8; MAX_OPEN_PACKET_SIZE],
|
||||||
cleanup: Option<AsyncOpenCleanup>,
|
cleanup: Option<AsyncOpenCleanup>,
|
||||||
}
|
}
|
||||||
@@ -106,8 +104,6 @@ impl AsyncOpenState {
|
|||||||
deadline: None,
|
deadline: None,
|
||||||
deadline_timer: None,
|
deadline_timer: None,
|
||||||
request_submitted: false,
|
request_submitted: false,
|
||||||
stop_after_current_attempt: false,
|
|
||||||
stopped_after_current_attempt: false,
|
|
||||||
buffer: [0_u8; MAX_OPEN_PACKET_SIZE],
|
buffer: [0_u8; MAX_OPEN_PACKET_SIZE],
|
||||||
cleanup: None,
|
cleanup: None,
|
||||||
}
|
}
|
||||||
@@ -128,21 +124,12 @@ impl AsyncOpenState {
|
|||||||
self.deadline = None;
|
self.deadline = None;
|
||||||
self.deadline_timer = None;
|
self.deadline_timer = None;
|
||||||
self.request_submitted = false;
|
self.request_submitted = false;
|
||||||
self.stopped_after_current_attempt = self.stop_after_current_attempt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_in_flight_work(&self) -> bool {
|
pub(crate) fn has_in_flight_work(&self) -> bool {
|
||||||
self.request_submitted || self.cleanup.is_some()
|
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 {
|
fn deadline(&self) -> Instant {
|
||||||
self.deadline
|
self.deadline
|
||||||
.expect("an open attempt has an active deadline")
|
.expect("an open attempt has an active deadline")
|
||||||
@@ -324,10 +311,6 @@ impl AsyncClient {
|
|||||||
if state.attempt >= attempt_count {
|
if state.attempt >= attempt_count {
|
||||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
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() {
|
if state.deadline.is_none() {
|
||||||
let timeout = self.machine.config().open.timeouts[state.attempt];
|
let timeout = self.machine.config().open.timeouts[state.attempt];
|
||||||
if let Err(error) = state.start_attempt(timeout) {
|
if let Err(error) = state.start_attempt(timeout) {
|
||||||
@@ -368,9 +351,6 @@ impl AsyncClient {
|
|||||||
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
||||||
Poll::Ready(Ok(false)) => {
|
Poll::Ready(Ok(false)) => {
|
||||||
state.finish_attempt();
|
state.finish_attempt();
|
||||||
if state.stopped_after_current_attempt() {
|
|
||||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Poll::Ready(Ok(true)) => {}
|
Poll::Ready(Ok(true)) => {}
|
||||||
@@ -396,9 +376,6 @@ impl AsyncClient {
|
|||||||
};
|
};
|
||||||
if datagram.received_at.mono > state.deadline() {
|
if datagram.received_at.mono > state.deadline() {
|
||||||
state.finish_attempt();
|
state.finish_attempt();
|
||||||
if state.stopped_after_current_attempt() {
|
|
||||||
return Poll::Ready(Err(ClientError::OpenTimeout));
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,6 +755,16 @@ impl AsyncClient {
|
|||||||
.map_err(ClientError::Socket)
|
.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(
|
fn poll_open_cleanup(
|
||||||
&self,
|
&self,
|
||||||
state: &mut AsyncOpenState,
|
state: &mut AsyncOpenState,
|
||||||
|
|||||||
@@ -47,6 +47,9 @@
|
|||||||
//!
|
//!
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
/// Cargo package version of this compiled library.
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[cfg(feature = "tokio")]
|
#[cfg(feature = "tokio")]
|
||||||
mod async_client;
|
mod async_client;
|
||||||
mod auth;
|
mod auth;
|
||||||
|
|||||||
@@ -101,6 +101,15 @@ pub struct ManagedClientConfig {
|
|||||||
pub outcome_history_limit: usize,
|
pub outcome_history_limit: usize,
|
||||||
pub max_live_target_generations: usize,
|
pub max_live_target_generations: usize,
|
||||||
pub final_drain: Duration,
|
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 {
|
impl Default for ManagedClientConfig {
|
||||||
@@ -114,6 +123,9 @@ impl Default for ManagedClientConfig {
|
|||||||
outcome_history_limit: DEFAULT_MANAGED_OUTCOME_HISTORY_LIMIT,
|
outcome_history_limit: DEFAULT_MANAGED_OUTCOME_HISTORY_LIMIT,
|
||||||
max_live_target_generations: DEFAULT_MANAGED_MAX_LIVE_TARGET_GENERATIONS,
|
max_live_target_generations: DEFAULT_MANAGED_MAX_LIVE_TARGET_GENERATIONS,
|
||||||
final_drain: DEFAULT_MANAGED_FINAL_DRAIN,
|
final_drain: DEFAULT_MANAGED_FINAL_DRAIN,
|
||||||
|
reconnect_timeout: None,
|
||||||
|
reconnect_initial_delay: Duration::from_millis(250),
|
||||||
|
reconnect_max_delay: Duration::from_secs(10),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,6 +376,13 @@ pub struct ManagedTargetFailure {
|
|||||||
/// Configuration failure detected before any runtime work begins.
|
/// Configuration failure detected before any runtime work begins.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ManagedConfigError {
|
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")]
|
#[error("FinishWhenQuiescent requires at least one initial target")]
|
||||||
EmptyInitialTargets,
|
EmptyInitialTargets,
|
||||||
#[error("duplicate managed target id {id}")]
|
#[error("duplicate managed target id {id}")]
|
||||||
|
|||||||
@@ -327,7 +327,14 @@ impl ManagedClientTask {
|
|||||||
let mut non_send_deadline = None;
|
let mut non_send_deadline = None;
|
||||||
let mut send_deadline = None;
|
let mut send_deadline = None;
|
||||||
for target in &self.targets {
|
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() {
|
match target.phase() {
|
||||||
|
TargetPhase::Retrying { deadline } => {
|
||||||
|
non_send_deadline = non_send_deadline.into_iter().chain(Some(*deadline)).min();
|
||||||
|
}
|
||||||
TargetPhase::Active {
|
TargetPhase::Active {
|
||||||
client,
|
client,
|
||||||
schedule,
|
schedule,
|
||||||
|
|||||||
@@ -367,6 +367,35 @@ fn build_task(
|
|||||||
maximum: tokio::sync::Semaphore::MAX_PERMITS,
|
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 {
|
if config.max_live_target_generations == 0 {
|
||||||
return Err(ManagedConfigError::ZeroLiveGenerationLimit);
|
return Err(ManagedConfigError::ZeroLiveGenerationLimit);
|
||||||
}
|
}
|
||||||
@@ -417,6 +446,10 @@ fn build_task(
|
|||||||
server_addr: Arc::from(target.server_addr),
|
server_addr: Arc::from(target.server_addr),
|
||||||
remote: None,
|
remote: None,
|
||||||
counters: TargetCounters::default(),
|
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 }),
|
phase: Some(TargetPhase::Pending { client_config }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ impl ManagedClientTask {
|
|||||||
};
|
};
|
||||||
self.targets[index].sync_packets_sent(&client);
|
self.targets[index].sync_packets_sent(&client);
|
||||||
send_waiting = result.is_pending();
|
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 {
|
self.targets[index].restore_phase(TargetPhase::Active {
|
||||||
client,
|
client,
|
||||||
schedule,
|
schedule,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
time::Instant,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -27,6 +27,9 @@ type ConnectFuture =
|
|||||||
Pin<Box<dyn Future<Output = Result<AsyncClient, ClientError>> + Send + 'static>>;
|
Pin<Box<dyn Future<Output = Result<AsyncClient, ClientError>> + Send + 'static>>;
|
||||||
|
|
||||||
pub(super) enum TargetPhase {
|
pub(super) enum TargetPhase {
|
||||||
|
Retrying {
|
||||||
|
deadline: Instant,
|
||||||
|
},
|
||||||
Pending {
|
Pending {
|
||||||
client_config: ClientConfig,
|
client_config: ClientConfig,
|
||||||
},
|
},
|
||||||
@@ -65,6 +68,7 @@ pub(super) enum TargetPhase {
|
|||||||
impl TargetPhase {
|
impl TargetPhase {
|
||||||
pub(super) fn lifecycle(&self) -> ManagedTargetLifecycle {
|
pub(super) fn lifecycle(&self) -> ManagedTargetLifecycle {
|
||||||
match self {
|
match self {
|
||||||
|
Self::Retrying { .. } => ManagedTargetLifecycle::Pending,
|
||||||
Self::Pending { .. } => ManagedTargetLifecycle::Pending,
|
Self::Pending { .. } => ManagedTargetLifecycle::Pending,
|
||||||
Self::Connecting { .. } => ManagedTargetLifecycle::Connecting,
|
Self::Connecting { .. } => ManagedTargetLifecycle::Connecting,
|
||||||
Self::Opening { .. } => ManagedTargetLifecycle::Opening,
|
Self::Opening { .. } => ManagedTargetLifecycle::Opening,
|
||||||
@@ -182,11 +186,30 @@ pub(super) struct TargetRuntime {
|
|||||||
pub(super) server_addr: Arc<str>,
|
pub(super) server_addr: Arc<str>,
|
||||||
pub(super) remote: Option<std::net::SocketAddr>,
|
pub(super) remote: Option<std::net::SocketAddr>,
|
||||||
pub(super) counters: TargetCounters,
|
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.
|
// None only while a synchronous operation owns the phase locally.
|
||||||
pub(super) phase: Option<TargetPhase>,
|
pub(super) phase: Option<TargetPhase>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TargetRuntime {
|
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 {
|
pub(super) fn phase(&self) -> &TargetPhase {
|
||||||
self.phase
|
self.phase
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -218,16 +241,18 @@ impl TargetRuntime {
|
|||||||
self.membership.is_desired() && matches!(phase, TargetPhase::Active { .. })
|
self.membership.is_desired() && matches!(phase, TargetPhase::Active { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adopt the underlying client's authoritative sent count.
|
/// Combine the current client's authoritative sent count with retired
|
||||||
///
|
/// sessions, so one target generation keeps durable accounting on retries.
|
||||||
/// 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) {
|
pub(super) fn sync_packets_sent(&mut self, client: &AsyncClient) {
|
||||||
self.counters.packets_sent = client.packets_sent();
|
self.counters.packets_sent = self
|
||||||
|
.sent_before_session
|
||||||
|
.saturating_add(client.packets_sent());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn sync_live_packets_sent(&mut self) {
|
pub(super) fn sync_live_packets_sent(&mut self) {
|
||||||
self.counters.packets_sent = self.phase().client().packets_sent();
|
self.counters.packets_sent = self
|
||||||
|
.sent_before_session
|
||||||
|
.saturating_add(self.phase().client().packets_sent());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build this target's durable outcome.
|
/// Build this target's durable outcome.
|
||||||
@@ -284,6 +309,30 @@ impl ManagedClientTask {
|
|||||||
|
|
||||||
pub(super) fn publish_client_events(&mut self, index: usize, events: Vec<ClientEvent>) {
|
pub(super) fn publish_client_events(&mut self, index: usize, events: Vec<ClientEvent>) {
|
||||||
for event in events {
|
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.targets[index].counters.observe(&event);
|
||||||
self.publish_event(ManagedEvent::Client {
|
self.publish_event(ManagedEvent::Client {
|
||||||
target: self.targets[index].instance.clone(),
|
target: self.targets[index].instance.clone(),
|
||||||
@@ -295,9 +344,33 @@ impl ManagedClientTask {
|
|||||||
fn finish_target(
|
fn finish_target(
|
||||||
&mut self,
|
&mut self,
|
||||||
index: usize,
|
index: usize,
|
||||||
end_reason: ManagedTargetEndReason,
|
mut end_reason: ManagedTargetEndReason,
|
||||||
cleanup_failure: Option<ManagedTargetFailure>,
|
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));
|
let outcome = Arc::new(self.targets[index].outcome(end_reason, cleanup_failure));
|
||||||
// Finalize history before the phase publisher installs Terminal, updates
|
// Finalize history before the phase publisher installs Terminal, updates
|
||||||
// pacing and publishes one complete status followed by the lifecycle event.
|
// pacing and publishes one complete status followed by the lifecycle event.
|
||||||
@@ -311,6 +384,12 @@ impl ManagedClientTask {
|
|||||||
self.publish_event(ManagedEvent::TargetFinished { outcome });
|
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) {
|
fn fail_target(&mut self, index: usize, phase: ManagedTargetFailurePhase, error: ClientError) {
|
||||||
let failure = classify_client_error(phase, &error);
|
let failure = classify_client_error(phase, &error);
|
||||||
self.finish_target(index, ManagedTargetEndReason::Failed(failure), None);
|
self.finish_target(index, ManagedTargetEndReason::Failed(failure), None);
|
||||||
@@ -332,6 +411,9 @@ impl ManagedClientTask {
|
|||||||
};
|
};
|
||||||
client.discard_prepared_probe();
|
client.discard_prepared_probe();
|
||||||
self.targets[index].sync_live_packets_sent();
|
self.targets[index].sync_live_packets_sent();
|
||||||
|
if self.can_reconnect(index) {
|
||||||
|
return self.begin_close(index, primary_end, None, now);
|
||||||
|
}
|
||||||
match cleanup {
|
match cleanup {
|
||||||
OpenSessionFailureCleanup::Drain => self.begin_drain(index, primary_end, now),
|
OpenSessionFailureCleanup::Drain => self.begin_drain(index, primary_end, now),
|
||||||
OpenSessionFailureCleanup::Close => self.begin_close(index, primary_end, None, now),
|
OpenSessionFailureCleanup::Close => self.begin_close(index, primary_end, None, now),
|
||||||
@@ -359,6 +441,27 @@ impl ManagedClientTask {
|
|||||||
let retirement = self.effective_retirement(index);
|
let retirement = self.effective_retirement(index);
|
||||||
let stopping = self.phase.is_stopping() || retirement.is_some();
|
let stopping = self.phase.is_stopping() || retirement.is_some();
|
||||||
match self.targets[index].phase_mut() {
|
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 { .. } => {
|
TargetPhase::Pending { .. } => {
|
||||||
if stopping {
|
if stopping {
|
||||||
self.finish_target(
|
self.finish_target(
|
||||||
@@ -409,12 +512,12 @@ impl ManagedClientTask {
|
|||||||
let retirement = retirement
|
let retirement = retirement
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or(ManagedTargetEndReason::Stopped);
|
.unwrap_or(ManagedTargetEndReason::Stopped);
|
||||||
if !open.has_in_flight_work() {
|
let cleanup_failure = client.cancel_open(open).map(|error| {
|
||||||
self.targets[index].sync_live_packets_sent();
|
classify_client_error(ManagedTargetFailurePhase::Opening, &error)
|
||||||
self.finish_target(index, retirement, None);
|
});
|
||||||
return false;
|
self.targets[index].sync_live_packets_sent();
|
||||||
}
|
self.finish_target(index, retirement, cleanup_failure);
|
||||||
open.request_stop_after_current_attempt();
|
return false;
|
||||||
}
|
}
|
||||||
match client.poll_open(open, cx) {
|
match client.poll_open(open, cx) {
|
||||||
Poll::Pending => false,
|
Poll::Pending => false,
|
||||||
@@ -438,64 +541,25 @@ impl ManagedClientTask {
|
|||||||
index,
|
index,
|
||||||
vec![ClientEvent::SessionStarted(started)],
|
vec![ClientEvent::SessionStarted(started)],
|
||||||
);
|
);
|
||||||
if stopping {
|
let previous = self.targets[index].take_phase();
|
||||||
self.targets[index].sync_live_packets_sent();
|
self.targets[index].restore_phase(TargetPhase::Active {
|
||||||
self.begin_drain(
|
client: previous.into_client(),
|
||||||
index,
|
schedule,
|
||||||
retirement
|
send_waiting: false,
|
||||||
.clone()
|
});
|
||||||
.unwrap_or(ManagedTargetEndReason::Stopped),
|
self.publish_target_phase(index, ManagedTargetLifecycle::Opening);
|
||||||
now,
|
true
|
||||||
)
|
|
||||||
} 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))) => {
|
Poll::Ready(Ok(OpenOutcome::NoTestCompleted(completed))) => {
|
||||||
self.publish_client_events(
|
self.publish_client_events(
|
||||||
index,
|
index,
|
||||||
vec![ClientEvent::NoTestCompleted(completed)],
|
vec![ClientEvent::NoTestCompleted(completed)],
|
||||||
);
|
);
|
||||||
let end_reason = if stopping {
|
self.finish_target(index, ManagedTargetEndReason::NoTestComplete, None);
|
||||||
retirement
|
|
||||||
.clone()
|
|
||||||
.unwrap_or(ManagedTargetEndReason::Stopped)
|
|
||||||
} else {
|
|
||||||
ManagedTargetEndReason::NoTestComplete
|
|
||||||
};
|
|
||||||
self.finish_target(index, end_reason, None);
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Poll::Ready(Err(error)) => {
|
Poll::Ready(Err(error)) => {
|
||||||
if stopping {
|
self.fail_target(index, ManagedTargetFailurePhase::Opening, error);
|
||||||
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
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,6 +568,16 @@ impl ManagedClientTask {
|
|||||||
if stopping {
|
if stopping {
|
||||||
client.discard_prepared_probe();
|
client.discard_prepared_probe();
|
||||||
self.targets[index].sync_live_packets_sent();
|
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(
|
return self.begin_drain(
|
||||||
index,
|
index,
|
||||||
retirement
|
retirement
|
||||||
@@ -539,6 +613,22 @@ impl ManagedClientTask {
|
|||||||
self.finish_target(index, ManagedTargetEndReason::PeerClosed, None);
|
self.finish_target(index, ManagedTargetEndReason::PeerClosed, None);
|
||||||
return false;
|
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 {
|
let TargetPhase::Active {
|
||||||
client, schedule, ..
|
client, schedule, ..
|
||||||
} = self.targets[index].phase_mut()
|
} = self.targets[index].phase_mut()
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ struct UpdatePlan {
|
|||||||
|
|
||||||
fn synchronously_retireable(phase: &TargetPhase) -> bool {
|
fn synchronously_retireable(phase: &TargetPhase) -> bool {
|
||||||
match phase {
|
match phase {
|
||||||
TargetPhase::Pending { .. }
|
TargetPhase::Retrying { .. }
|
||||||
|
| TargetPhase::Pending { .. }
|
||||||
| TargetPhase::Connecting { .. }
|
| TargetPhase::Connecting { .. }
|
||||||
| TargetPhase::Terminal { .. } => true,
|
| TargetPhase::Terminal { .. } => true,
|
||||||
TargetPhase::Opening { open, .. } => !open.has_in_flight_work(),
|
TargetPhase::Opening { open, .. } => !open.has_in_flight_work(),
|
||||||
@@ -140,6 +141,10 @@ impl ManagedClientTask {
|
|||||||
membership: TargetMembership::Desired,
|
membership: TargetMembership::Desired,
|
||||||
remote: None,
|
remote: None,
|
||||||
counters: TargetCounters::default(),
|
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 {
|
phase: Some(TargetPhase::Pending {
|
||||||
client_config: planned.client_config,
|
client_config: planned.client_config,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ publish = false
|
|||||||
irtt-client = { workspace = true, features = ["ancillary", "tokio"] }
|
irtt-client = { workspace = true, features = ["ancillary", "tokio"] }
|
||||||
irtt-server.workspace = true
|
irtt-server.workspace = true
|
||||||
irtt-proto.workspace = true
|
irtt-proto.workspace = true
|
||||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-proto"
|
name = "irtt-proto"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
description = "IRTT protocol encoding and decoding primitives"
|
description = "IRTT protocol encoding and decoding primitives"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["irtt", "networking", "latency", "protocol"]
|
keywords = ["irtt", "networking", "latency", "protocol"]
|
||||||
categories = ["network-programming"]
|
categories = ["network-programming"]
|
||||||
@@ -18,4 +17,4 @@ subtle = "2.6.1"
|
|||||||
thiserror = "2.0.21"
|
thiserror = "2.0.21"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.11.0"
|
proptest = "1.12.0"
|
||||||
@@ -77,6 +77,9 @@
|
|||||||
//!
|
//!
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
/// Cargo package version of this compiled library.
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
pub mod echo;
|
pub mod echo;
|
||||||
mod envelope;
|
mod envelope;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-server"
|
name = "irtt-server"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
description = "IRTT-compatible Rust server library"
|
description = "IRTT-compatible Rust server library"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["irtt", "networking", "latency", "server"]
|
keywords = ["irtt", "networking", "latency", "server"]
|
||||||
categories = ["network-programming"]
|
categories = ["network-programming"]
|
||||||
@@ -16,7 +15,7 @@ irtt-proto.workspace = true
|
|||||||
getrandom = "0.4.3"
|
getrandom = "0.4.3"
|
||||||
socket2 = { version = "0.6.5", features = ["all"] }
|
socket2 = { version = "0.6.5", features = ["all"] }
|
||||||
thiserror = "2.0.21"
|
thiserror = "2.0.21"
|
||||||
tokio = { version = "1.53.2", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
tokio = { version = "1.54.1", default-features = false, features = ["rt", "net", "time", "sync", "macros"] }
|
||||||
|
|
||||||
# Wildcard listeners recover each request's local destination address and send
|
# Wildcard listeners recover each request's local destination address and send
|
||||||
# the reply from it. Only these targets have a safe destination-metadata and
|
# the reply from it. Only these targets have a safe destination-metadata and
|
||||||
|
|||||||
@@ -108,6 +108,9 @@
|
|||||||
//! payload bytes never reach a reply under any mode.
|
//! payload bytes never reach a reply under any mode.
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
/// Cargo package version of this compiled library.
|
||||||
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
mod clock;
|
mod clock;
|
||||||
mod config;
|
mod config;
|
||||||
mod core;
|
mod core;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "irtt-stats"
|
name = "irtt-stats"
|
||||||
version = "0.5.2"
|
version = "0.6.0"
|
||||||
description = "Statistics aggregation for irtt-client events"
|
description = "Statistics aggregation for irtt-client events"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["irtt", "networking", "latency", "statistics"]
|
keywords = ["irtt", "networking", "latency", "statistics"]
|
||||||
categories = ["network-programming"]
|
categories = ["network-programming"]
|
||||||
@@ -19,4 +18,4 @@ irtt-client.workspace = true
|
|||||||
name = "finite_vs_continuous"
|
name = "finite_vs_continuous"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.11.0"
|
proptest = "1.12.0"
|
||||||
@@ -16,10 +16,16 @@ re-export. Event interpretation, IPDV pairing, loss, and snapshot reconstruction
|
|||||||
remain here.
|
remain here.
|
||||||
|
|
||||||
A `StatsCollector` has one sequence/IPDV namespace and one packet count, both
|
A `StatsCollector` has one sequence/IPDV namespace and one packet count, both
|
||||||
scoped to a single target. A managed session covering multiple targets needs
|
scoped to a single target session. A managed session covering multiple targets
|
||||||
one collector per target — as the `irtt-rs` CLI does — since a target's
|
needs one collector per target — as the `irtt-rs` CLI does — since a target's
|
||||||
sequence numbers start over at zero and feeding two targets into one
|
sequence numbers start over at zero and feeding two targets into one
|
||||||
collector would pair up unrelated probes.
|
collector would pair up unrelated probes. Reset or replace the collector when a
|
||||||
|
target reconnects, since the new session restarts its sequence numbers.
|
||||||
|
|
||||||
|
Version 0.6 consumes `irtt-client` 0.7 events. Applications using both crates
|
||||||
|
must upgrade them together to avoid incompatible `ClientEvent` types.
|
||||||
|
`IpdvPairUpdate` exposes signed nanosecond changes alongside the existing
|
||||||
|
absolute `Duration` values; aggregate IPDV statistics remain absolute magnitudes.
|
||||||
|
|
||||||
## Retention modes
|
## Retention modes
|
||||||
|
|
||||||
|
|||||||
@@ -175,29 +175,31 @@ impl CoreStats {
|
|||||||
let mut updates = Vec::new();
|
let mut updates = Vec::new();
|
||||||
|
|
||||||
for pair in self.ipdv_tracker.insert(sample) {
|
for pair in self.ipdv_tracker.insert(sample) {
|
||||||
let Some(rtt_ipdv) = duration_from_non_negative_i128_ns(pair.rtt_ipdv_ns) else {
|
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 {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let send_ipdv = pair
|
let send_ipdv = send_magnitude_ns.and_then(duration_from_non_negative_i128_ns);
|
||||||
.send_ipdv_ns
|
let receive_ipdv = receive_magnitude_ns.and_then(duration_from_non_negative_i128_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(pair.rtt_ipdv_ns);
|
self.ipdv_round_trip.push_ns(rtt_magnitude_ns);
|
||||||
|
|
||||||
if let Some(value) = pair.send_ipdv_ns {
|
if let Some(value) = send_magnitude_ns {
|
||||||
self.ipdv_send.push_ns(value);
|
self.ipdv_send.push_ns(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(value) = pair.receive_ipdv_ns {
|
if let Some(value) = receive_magnitude_ns {
|
||||||
self.ipdv_receive.push_ns(value);
|
self.ipdv_receive.push_ns(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
updates.push(IpdvPairUpdate {
|
updates.push(IpdvPairUpdate {
|
||||||
previous_seq: pair.previous_seq,
|
previous_seq: pair.previous_seq,
|
||||||
current_seq: pair.current_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,
|
rtt_ipdv,
|
||||||
send_ipdv,
|
send_ipdv,
|
||||||
receive_ipdv,
|
receive_ipdv,
|
||||||
|
|||||||
@@ -89,9 +89,9 @@ impl IpdvTracker {
|
|||||||
Some(CompletedIpdvPair {
|
Some(CompletedIpdvPair {
|
||||||
previous_seq,
|
previous_seq,
|
||||||
current_seq,
|
current_seq,
|
||||||
rtt_ipdv_ns: abs_i128_ns(current.rtt_primary_ns - previous.rtt_primary_ns),
|
rtt_ipdv_ns: current.rtt_primary_ns - previous.rtt_primary_ns,
|
||||||
send_ipdv_ns: send_ipdv_ns(previous, current).map(abs_i128_ns),
|
send_ipdv_ns: send_ipdv_ns(previous, current),
|
||||||
receive_ipdv_ns: receive_ipdv_ns(previous, current).map(abs_i128_ns),
|
receive_ipdv_ns: receive_ipdv_ns(previous, current),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,10 +161,6 @@ fn duration_ns_i128(duration: Duration) -> i128 {
|
|||||||
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
|
i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn abs_i128_ns(value: i128) -> i128 {
|
|
||||||
value.saturating_abs()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![warn(rustdoc::missing_crate_level_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 std::time::Duration;
|
||||||
|
|
||||||
use irtt_client::ClientEvent;
|
use irtt_client::ClientEvent;
|
||||||
@@ -520,6 +523,12 @@ pub struct IpdvPairUpdate {
|
|||||||
pub previous_seq: u32,
|
pub previous_seq: u32,
|
||||||
/// Sequence number of the later reply in the adjacent pair.
|
/// Sequence number of the later reply in the adjacent pair.
|
||||||
pub current_seq: u32,
|
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.
|
/// Absolute round-trip IPDV between the adjacent replies.
|
||||||
pub rtt_ipdv: Duration,
|
pub rtt_ipdv: Duration,
|
||||||
/// Absolute send-side IPDV when send one-way delay is available for both replies.
|
/// Absolute send-side IPDV when send one-way delay is available for both replies.
|
||||||
|
|||||||
@@ -191,10 +191,15 @@ impl ReplySample {
|
|||||||
client_receive_mono: received_at.mono,
|
client_receive_mono: received_at.mono,
|
||||||
client_send_wall_ns: system_time_ns(sent_at.wall),
|
client_send_wall_ns: system_time_ns(sent_at.wall),
|
||||||
client_receive_wall_ns: system_time_ns(received_at.wall),
|
client_receive_wall_ns: system_time_ns(received_at.wall),
|
||||||
server_receive_mono_ns: server_timing.and_then(|timing| timing.receive_mono_ns),
|
// Midpoint timestamps serve as both directional endpoints.
|
||||||
server_send_mono_ns: server_timing.and_then(|timing| timing.send_mono_ns),
|
server_receive_mono_ns: server_timing
|
||||||
server_receive_wall_ns: server_timing.and_then(|timing| timing.receive_wall_ns),
|
.and_then(|timing| timing.receive_mono_ns.or(timing.midpoint_mono_ns)),
|
||||||
server_send_wall_ns: server_timing.and_then(|timing| timing.send_wall_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)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-13
@@ -37,12 +37,13 @@ overrides it for that target, and `@hmac=` explicitly disables HMAC for that
|
|||||||
target. The modifier uses `@`, not a shell command separator; quote an argument
|
target. The modifier uses `@`, not a shell command separator; quote an argument
|
||||||
if its key needs shell quoting.
|
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
|
Target syntax supports these backslash escapes in labels, addresses, and HMAC
|
||||||
values: `\\` for a literal backslash, `\@` for a literal `@`, `\=` for a
|
values: `\\` for a literal backslash, `\@` for a literal `@`, `\=` for a
|
||||||
literal `=`, and `\;` for a literal semicolon. Escaping `@` is necessary in a
|
literal `=`, and `\;` for a literal semicolon. Escaping `@` is necessary in a
|
||||||
label or address when it would otherwise begin `@hmac=`; escaping the first
|
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
|
`=` is how an unlabeled target or a label can contain that character. An `=` in an HMAC value does not need escaping. Positional
|
||||||
an HMAC value, including Base64 padding, does not need escaping. Positional
|
|
||||||
target arguments may contain a raw comma; stdin target sets have the additional
|
target arguments may contain a raw comma; stdin target sets have the additional
|
||||||
`\,` escape described below.
|
`\,` escape described below.
|
||||||
|
|
||||||
@@ -93,9 +94,29 @@ 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
|
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.
|
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
|
## DYNAMIC TARGET SETS FROM STANDARD INPUT
|
||||||
|
|
||||||
`--targets-stdin` is available only with continuous mode (`--duration 0`).
|
`--targets-stdin` is available only with continuous mode (`--continuous` or
|
||||||
|
`--duration 0`).
|
||||||
Positional targets remain an initial desired set, and may be omitted to
|
Positional targets remain an initial desired set, and may be omitted to
|
||||||
start empty:
|
start empty:
|
||||||
|
|
||||||
@@ -152,23 +173,92 @@ desired set to 128 targets and retains at most 256 live target generations
|
|||||||
while replacements drain. The 128-target limit applies after dual-stack
|
while replacements drain. The 128-target limit applies after dual-stack
|
||||||
expansion: a hostname producing both families consumes two target slots.
|
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
|
## 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:
|
`--duration` controls the run length:
|
||||||
|
|
||||||
- Any nonzero duration (default `10s`) is a **finite** run: probing stops
|
- Any nonzero duration (default `10s`) is a **finite** run: probing stops
|
||||||
after that long, and a final summary is always printed for table output
|
after that long, and a final summary is printed for table output when
|
||||||
(see [OUTPUT](#output) below — CSV, TSV, and JSON Lines never print one).
|
statistics are enabled (see [OUTPUT](#output) below — CSV, TSV, and
|
||||||
- `--duration 0` is **continuous**: the client runs until interrupted
|
JSON Lines never print one).
|
||||||
(`Ctrl-C`). For table output, a final summary is printed only if the run
|
- `--continuous` (or `--duration 0`) is **continuous**: the client runs until
|
||||||
was interrupted. Stdin-controlled mode is the exception: EOF also requests
|
interrupted (`Ctrl-C`). With statistics enabled and table output, a final
|
||||||
a graceful stop and prints the final retained summary. An uninterrupted
|
summary is printed only if the run was interrupted. Stdin-controlled mode
|
||||||
|
is the exception: EOF also requests a graceful stop and prints the final
|
||||||
|
retained summary. An uninterrupted
|
||||||
continuous run that exits for another reason does not get one.
|
continuous run that exits for another reason does not get one.
|
||||||
|
|
||||||
See [MEASUREMENTS AND MEMORY](#measurements-and-memory) below for why this
|
See [MEASUREMENTS AND MEMORY](#measurements-and-memory) below for why this
|
||||||
distinction also determines what statistics can be computed and how much
|
distinction also determines what statistics can be computed and how much
|
||||||
memory a long run retains.
|
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 \
|
||||||
|
--reconnect-timeout 10s \
|
||||||
|
--reconnect-initial-delay 250ms --reconnect-max-delay 10s \
|
||||||
|
--format csv --header never \
|
||||||
|
--columns client_receive_wall_ns,target,seq,rd_us,sd_us \
|
||||||
|
reflector1 reflector2
|
||||||
|
```
|
||||||
|
|
||||||
## OUTPUT
|
## OUTPUT
|
||||||
|
|
||||||
### `--format FORMAT`
|
### `--format FORMAT`
|
||||||
@@ -259,6 +349,10 @@ processing time is derived from the same server receive endpoint.
|
|||||||
|
|
||||||
## MEASUREMENTS AND MEMORY
|
## MEASUREMENTS AND MEMORY
|
||||||
|
|
||||||
|
The statistics retention below applies only when the `stats` feature is compiled
|
||||||
|
in and `--no-stats` is absent. Disabling collection keeps the client
|
||||||
|
classification bounds but allocates no statistics collectors.
|
||||||
|
|
||||||
Finite and continuous modes make a different tradeoff between exact
|
Finite and continuous modes make a different tradeoff between exact
|
||||||
statistics and bounded memory.
|
statistics and bounded memory.
|
||||||
|
|
||||||
@@ -304,14 +398,15 @@ approaches a fixed multiple of `targets` regardless of how long it runs.
|
|||||||
## PEER CLOSE AND EXIT BEHAVIOR
|
## PEER CLOSE AND EXIT BEHAVIOR
|
||||||
|
|
||||||
A peer closing a finite run is accepted as a terminal, successful outcome.
|
A peer closing a finite run is accepted as a terminal, successful outcome.
|
||||||
In continuous mode, an uninterrupted peer closure is treated as an error —
|
Without `--reconnect`, in continuous mode, an uninterrupted peer closure is treated as an error —
|
||||||
the client exits nonzero — so a process supervisor can restart it; an
|
the client exits nonzero — so a process supervisor can restart it; an
|
||||||
interrupted run (`Ctrl-C`) never reports peer closure as an error, whichever
|
interrupted run (`Ctrl-C`) never reports peer closure as an error, whichever
|
||||||
mode it was in.
|
mode it was in.
|
||||||
|
|
||||||
With `--targets-stdin`, peer closure is target-local: other desired targets
|
With `--reconnect`, a desired target reopens automatically after peer closure.
|
||||||
and the stdin controller continue running. Including that target in a later
|
Without recovery, `--targets-stdin` makes peer closure target-local: other
|
||||||
complete set starts a fresh generation.
|
desired targets and the stdin controller continue running. Including that
|
||||||
|
target in a later complete set starts a fresh generation.
|
||||||
|
|
||||||
## EXIT STATUS
|
## EXIT STATUS
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -49,9 +49,12 @@ reports which feature to rebuild with, and `irtt-rs --help` lists which
|
|||||||
applets the current build actually contains.
|
applets the current build actually contains.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install irtt-rs --locked --no-default-features --features client
|
cargo install irtt-rs --locked --no-default-features --features client,stats
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## RELATIONSHIP TO irtt-client, irtt-tui, irtt-server
|
||||||
|
|
||||||
`irtt-rs` is packaging and dispatch only. All probing, display, and server
|
`irtt-rs` is packaging and dispatch only. All probing, display, and server
|
||||||
|
|||||||
+3
-2
@@ -74,8 +74,9 @@ IPv6 listener.
|
|||||||
|
|
||||||
### `--hmac KEY`
|
### `--hmac KEY`
|
||||||
|
|
||||||
HMAC key, taken as the UTF-8 bytes of this argument. With a key configured,
|
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.
|
||||||
every request must carry a valid MAC and every reply is authenticated;
|
|
||||||
|
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
|
without one, authenticated requests are dropped. The key is visible in the
|
||||||
process arguments.
|
process arguments.
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -16,9 +16,9 @@ At least one target is required.
|
|||||||
|
|
||||||
Target syntax, multiple targets, and pacing are identical to
|
Target syntax, multiple targets, and pacing are identical to
|
||||||
`irtt-client(1)` — see that manual for `[LABEL=]TARGET` syntax and the
|
`irtt-client(1)` — see that manual for `[LABEL=]TARGET` syntax and the
|
||||||
`--pacing staggered|burst` option. The full set of negotiation flags
|
`--pacing staggered|burst` option. The client configuration flags
|
||||||
(`--interval`, `--length`, `--hmac`, `--clock`, `--tstamp`, `--stats`,
|
(`--interval`, `--timeouts`, `--length`, `--hmac`, `--clock`, `--tstamp`,
|
||||||
`--sfill`, `--dscp`, `--ttl`, `--loose`) is shared with `irtt-client` as
|
`--stats`, `--sfill`, `--dscp`, `--ttl`, `--loose`) are shared with `irtt-client` as
|
||||||
well; consult that manual for their meaning.
|
well; consult that manual for their meaning.
|
||||||
|
|
||||||
Address-family controls are shared too: `-4` / `--ipv4` uses IPv4 only,
|
Address-family controls are shared too: `-4` / `--ipv4` uses IPv4 only,
|
||||||
@@ -40,7 +40,10 @@ See the client manual's address-family section for discovery errors and labels.
|
|||||||
|
|
||||||
Unlike `irtt-client`, **the TUI defaults to continuous mode**
|
Unlike `irtt-client`, **the TUI defaults to continuous mode**
|
||||||
(`--duration 0`): it runs until you quit. Pass an explicit `--duration` for
|
(`--duration 0`): it runs until you quit. Pass an explicit `--duration` for
|
||||||
a finite run:
|
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`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
irtt-tui <server> --duration 30s
|
irtt-tui <server> --duration 30s
|
||||||
|
|||||||
Generated
+18
-18
@@ -4,9 +4,9 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
version = "1.4.2"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
@@ -31,9 +31,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.6.0"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
|
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -76,9 +76,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctutils"
|
name = "ctutils"
|
||||||
version = "0.4.2"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
checksum = "fe3e9d21d39e6f39ff46e03d5b621424f88ee23d8a1672c6bfc9ccc7f2da4f6a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cmov",
|
"cmov",
|
||||||
]
|
]
|
||||||
@@ -97,9 +97,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
@@ -132,7 +132,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-proto"
|
name = "irtt-proto"
|
||||||
version = "0.5.1"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"md-5",
|
"md-5",
|
||||||
@@ -152,7 +152,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "irtt-server"
|
name = "irtt-server"
|
||||||
version = "0.5.3"
|
version = "0.5.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
@@ -174,9 +174,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.190"
|
version = "0.2.191"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
@@ -285,9 +285,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.6"
|
version = "3.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -316,9 +316,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.53.2"
|
version = "1.54.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
@@ -347,9 +347,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ edition = "2021"
|
|||||||
cargo-fuzz = true
|
cargo-fuzz = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arbitrary = "1.4.2"
|
arbitrary = "1.5.0"
|
||||||
libfuzzer-sys = "0.4.13"
|
libfuzzer-sys = "0.4.13"
|
||||||
|
|
||||||
[dependencies.irtt-proto]
|
[dependencies.irtt-proto]
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ def select_scenarios(args: argparse.Namespace) -> list[Scenario]:
|
|||||||
def preflight(args: argparse.Namespace) -> None:
|
def preflight(args: argparse.Namespace) -> None:
|
||||||
if platform.system() != "Linux":
|
if platform.system() != "Linux":
|
||||||
raise RuntimeError("netem interop requires Linux network namespaces and tc netem")
|
raise RuntimeError("netem interop requires Linux network namespaces and tc netem")
|
||||||
for binary in ["ip", "tc"]:
|
for binary in ["ip", "tc", "nsenter"]:
|
||||||
if shutil.which(binary) is None:
|
if shutil.which(binary) is None:
|
||||||
raise RuntimeError(f"required command not found in PATH: {binary}")
|
raise RuntimeError(f"required command not found in PATH: {binary}")
|
||||||
if args.sudo and shutil.which(args.sudo) is None:
|
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]:
|
def netns(args: argparse.Namespace, namespace: str, command: list[str]) -> list[str]:
|
||||||
return [*sudo_prefix(args), "ip", "netns", "exec", namespace, *command]
|
return [*sudo_prefix(args), "nsenter", f"--net=/run/netns/{namespace}", "--", *command]
|
||||||
|
|
||||||
|
|
||||||
def run_checked(
|
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", "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", CLIENT_VETH, "netns", CLIENT_NS],
|
||||||
[*sudo_prefix(args), "ip", "link", "set", SERVER_VETH, "netns", SERVER_NS],
|
[*sudo_prefix(args), "ip", "link", "set", SERVER_VETH, "netns", SERVER_NS],
|
||||||
[*sudo_prefix(args), "ip", "-n", CLIENT_NS, "addr", "add", CLIENT_ADDR, "dev", CLIENT_VETH],
|
netns(args, CLIENT_NS, ["ip", "addr", "add", CLIENT_ADDR, "dev", CLIENT_VETH]),
|
||||||
[*sudo_prefix(args), "ip", "-n", SERVER_NS, "addr", "add", SERVER_ADDR, "dev", SERVER_VETH],
|
netns(args, SERVER_NS, ["ip", "addr", "add", SERVER_ADDR, "dev", SERVER_VETH]),
|
||||||
[*sudo_prefix(args), "ip", "-n", CLIENT_NS, "link", "set", "lo", "up"],
|
netns(args, CLIENT_NS, ["ip", "link", "set", "lo", "up"]),
|
||||||
[*sudo_prefix(args), "ip", "-n", SERVER_NS, "link", "set", "lo", "up"],
|
netns(args, SERVER_NS, ["ip", "link", "set", "lo", "up"]),
|
||||||
[*sudo_prefix(args), "ip", "-n", CLIENT_NS, "link", "set", CLIENT_VETH, "up"],
|
netns(args, CLIENT_NS, ["ip", "link", "set", CLIENT_VETH, "up"]),
|
||||||
[*sudo_prefix(args), "ip", "-n", SERVER_NS, "link", "set", SERVER_VETH, "up"],
|
netns(args, SERVER_NS, ["ip", "link", "set", SERVER_VETH, "up"]),
|
||||||
]
|
]
|
||||||
for index, command in enumerate(commands):
|
for index, command in enumerate(commands):
|
||||||
run_checked(command, run_dir / "setup" / f"{index:02d}")
|
run_checked(command, run_dir / "setup" / f"{index:02d}")
|
||||||
@@ -409,12 +409,7 @@ def apply_netem(args: argparse.Namespace, scenario: Scenario, scenario_dir: Path
|
|||||||
for namespace, dev, setting in commands:
|
for namespace, dev, setting in commands:
|
||||||
if not setting:
|
if not setting:
|
||||||
continue
|
continue
|
||||||
command = [
|
command = netns(args, namespace, [
|
||||||
*sudo_prefix(args),
|
|
||||||
"ip",
|
|
||||||
"netns",
|
|
||||||
"exec",
|
|
||||||
namespace,
|
|
||||||
"tc",
|
"tc",
|
||||||
"qdisc",
|
"qdisc",
|
||||||
"replace",
|
"replace",
|
||||||
@@ -423,25 +418,20 @@ def apply_netem(args: argparse.Namespace, scenario: Scenario, scenario_dir: Path
|
|||||||
"root",
|
"root",
|
||||||
"netem",
|
"netem",
|
||||||
*shlex.split(setting),
|
*shlex.split(setting),
|
||||||
]
|
])
|
||||||
run_checked(command, scenario_dir / "netem" / f"{namespace}-{dev}")
|
run_checked(command, scenario_dir / "netem" / f"{namespace}-{dev}")
|
||||||
|
|
||||||
|
|
||||||
def delete_qdiscs(args: argparse.Namespace, log_dir: Path) -> None:
|
def delete_qdiscs(args: argparse.Namespace, log_dir: Path) -> None:
|
||||||
for namespace, dev in [(CLIENT_NS, CLIENT_VETH), (SERVER_NS, SERVER_VETH)]:
|
for namespace, dev in [(CLIENT_NS, CLIENT_VETH), (SERVER_NS, SERVER_VETH)]:
|
||||||
command = [
|
command = netns(args, namespace, [
|
||||||
*sudo_prefix(args),
|
|
||||||
"ip",
|
|
||||||
"netns",
|
|
||||||
"exec",
|
|
||||||
namespace,
|
|
||||||
"tc",
|
"tc",
|
||||||
"qdisc",
|
"qdisc",
|
||||||
"del",
|
"del",
|
||||||
"dev",
|
"dev",
|
||||||
dev,
|
dev,
|
||||||
"root",
|
"root",
|
||||||
]
|
])
|
||||||
run_checked(
|
run_checked(
|
||||||
command,
|
command,
|
||||||
log_dir / "qdisc-cleanup" / f"{namespace}-{dev}",
|
log_dir / "qdisc-cleanup" / f"{namespace}-{dev}",
|
||||||
@@ -698,12 +688,19 @@ def cleanup(args: argparse.Namespace, quiet: bool) -> None:
|
|||||||
return
|
return
|
||||||
if args.sudo and shutil.which(args.sudo) is None:
|
if args.sudo and shutil.which(args.sudo) is None:
|
||||||
return
|
return
|
||||||
commands = [
|
commands = []
|
||||||
[*sudo_prefix(args), "ip", "netns", "exec", CLIENT_NS, "tc", "qdisc", "del", "dev", CLIENT_VETH, "root"],
|
if shutil.which("nsenter") is not None:
|
||||||
[*sudo_prefix(args), "ip", "netns", "exec", SERVER_NS, "tc", "qdisc", "del", "dev", SERVER_VETH, "root"],
|
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([
|
||||||
[*sudo_prefix(args), "ip", "netns", "delete", CLIENT_NS],
|
[*sudo_prefix(args), "ip", "netns", "delete", CLIENT_NS],
|
||||||
[*sudo_prefix(args), "ip", "netns", "delete", SERVER_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:
|
for command in commands:
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
command,
|
command,
|
||||||
|
|||||||
Reference in new issue
Block a user