Merge pull request #124 from Lochnair/refactor/application-binaries

refactor(app): clarify application and binary roles
This commit was merged in pull request #124.
This commit is contained in:
Nils Andreas Svee authored and GitHub committed 2026-08-20 12:09:10 +07:00
commit 0e23017100
49 files changed
+366 -215

No files matched your search

+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
- name: actionlint generated release workflow - name: actionlint generated release workflow
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8
with: with:
files: .github/workflows/irtt-cli-release.yml files: .github/workflows/irtt-rs-release.yml
shellcheck: false shellcheck: false
zizmor: zizmor:
name: zizmor name: zizmor
+3 -3
View File
@@ -46,8 +46,8 @@ jobs:
go install "github.com/heistp/irtt/cmd/irtt@${IRTT_UPSTREAM_VERSION}" go install "github.com/heistp/irtt/cmd/irtt@${IRTT_UPSTREAM_VERSION}"
"$GOBIN/irtt" version "$GOBIN/irtt" version
- name: Build irtt-rs CLI - name: Build irtt-rs client
run: cargo build -p irtt-cli --locked run: cargo build -p irtt-rs --bin irtt-client --locked
- name: Run netem interop comparisons - name: Run netem interop comparisons
run: | run: |
@@ -55,7 +55,7 @@ jobs:
--all \ --all \
--skip-build \ --skip-build \
--upstream-irtt "$GOBIN/irtt" \ --upstream-irtt "$GOBIN/irtt" \
--irtt-rs-command "$PWD/target/debug/irtt-cli" --irtt-rs-command "$PWD/target/debug/irtt-client"
- name: Append netem interop summary - name: Append netem interop summary
if: always() if: always()
@@ -42,7 +42,7 @@ on:
pull_request: pull_request:
push: push:
tags: tags:
- 'irtt-cli**[0-9]+.[0-9]+.[0-9]+*' - 'irtt-rs**[0-9]+.[0-9]+.[0-9]+*'
jobs: jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do # Run 'dist plan' (or host) to determine what tasks we need to do
+1 -1
View File
@@ -8,7 +8,7 @@ on:
- "irtt-client/v*.*.*" - "irtt-client/v*.*.*"
- "irtt-server/v*.*.*" - "irtt-server/v*.*.*"
- "irtt-stats/v*.*.*" - "irtt-stats/v*.*.*"
- "irtt-cli/v*.*.*" - "irtt-rs/v*.*.*"
permissions: permissions:
contents: read contents: read
+1 -1
View File
@@ -75,5 +75,5 @@ jobs:
- name: actionlint generated release workflow - name: actionlint generated release workflow
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8
with: with:
files: .github/workflows/irtt-cli-release.yml files: .github/workflows/irtt-rs-release.yml
shellcheck: false shellcheck: false
+3 -3
View File
@@ -41,7 +41,7 @@ For compatibility questions, the verified spec and black-box observations define
Main boundary: Main boundary:
> `irtt-proto` — wire semantics shared by client/server. `irtt-client` — client/session behavior. `irtt-server` — server/session behavior and Tokio runtime/orchestration. `irtt-stats` — statistics over client-facing events/results as currently designed. `irtt-cli` — presentation/orchestration. > `irtt-proto` — wire semantics shared by client/server. `irtt-client` — client/session behavior. `irtt-server` — server/session behavior and Tokio runtime/orchestration. `irtt-stats` — statistics over client-facing events/results as currently designed. `irtt-rs` (application package, `crates/irtt-app`) — presentation/orchestration.
- `irtt-proto`: pure protocol encoding/decoding, flags, layouts, params, varints/zigzag, HMAC-MD5, validation. No sockets, runtimes, lifecycle, stats, or presentation. - `irtt-proto`: pure protocol encoding/decoding, flags, layouts, params, varints/zigzag, HMAC-MD5, validation. No sockets, runtimes, lifecycle, stats, or presentation.
- `irtt-client`: socket/session lifecycle, negotiation, probe send/receive, timing metadata, classification, bounded state, and events. - `irtt-client`: socket/session lifecycle, negotiation, probe send/receive, timing metadata, classification, bounded state, and events.
@@ -51,7 +51,7 @@ Main boundary:
- `BlockingManagedClient`: synchronous owner using a dedicated current-thread Tokio runtime. - `BlockingManagedClient`: synchronous owner using a dedicated current-thread Tokio runtime.
- `irtt-server`: exists; see "Server architecture (`irtt-server`)" below. - `irtt-server`: exists; see "Server architecture (`irtt-server`)" below.
- `irtt-stats`: cumulative/rolling/finite statistics over client events. No wire/session state machine logic. - `irtt-stats`: cumulative/rolling/finite statistics over client events. No wire/session state machine logic.
- `irtt-cli`: argument parsing, managed orchestration, output, summaries, TUI, server applet orchestration, diagnostics, and exit behavior. Do not duplicate protocol/session state machines here. - `irtt-rs` (application package, `crates/irtt-app`): argument parsing, managed orchestration, output, summaries, TUI, server applet orchestration, diagnostics, and exit behavior. Produces the `irtt-rs` multicall dispatcher plus the dedicated `irtt-client`, `irtt-tui`, and `irtt-server` binaries. Do not duplicate protocol/session state machines here.
Tokio must remain optional for `irtt-client`; default builds must remain runtime-free and Tokio-free unless an explicit architectural change says otherwise. This rule is specific to `irtt-client` — do not generalize it to `irtt-server`, which has the opposite policy below. Tokio must remain optional for `irtt-client`; default builds must remain runtime-free and Tokio-free unless an explicit architectural change says otherwise. This rule is specific to `irtt-client` — do not generalize it to `irtt-server`, which has the opposite policy below.
@@ -66,7 +66,7 @@ Tokio must remain optional for `irtt-client`; default builds must remain runtime
- Protocol/session logic should be separated from socket/runtime orchestration where that materially improves deterministic testing, ownership clarity, state-machine reasoning, or maintainability. A `ServerCore`-like internal boundary is therefore desirable, but its exact name/API is not prescribed, it need not be public, it is not a promise of runtime independence, and simple code should not be tortured merely to make the core artificially pure. - Protocol/session logic should be separated from socket/runtime orchestration where that materially improves deterministic testing, ownership clarity, state-machine reasoning, or maintainability. A `ServerCore`-like internal boundary is therefore desirable, but its exact name/API is not prescribed, it need not be public, it is not a promise of runtime independence, and simple code should not be tortured merely to make the core artificially pure.
- Runtime/server orchestration belongs around that core and may freely use Tokio for UDP sockets, `recv_from`/`send_to`, IPv4/IPv6 listeners, timers, session expiry, max-duration deadlines, shutdown, bounded control/config channels where useful, and `select!`-style orchestration. - Runtime/server orchestration belongs around that core and may freely use Tokio for UDP sockets, `recv_from`/`send_to`, IPv4/IPv6 listeners, timers, session expiry, max-duration deadlines, shutdown, bounded control/config channels where useful, and `select!`-style orchestration.
- Ownership is layered and the layers do not blur: `Server` is the one-listener primitive (one socket, one `ServerCore`, one sequential loop), and `ServerSet` supervises one or more of them without owning protocol or session state itself. A common `ServerConfig` is cloned into each listener, so its bounds — `max_sessions` included — are per listener and not per process. Listeners share nothing else: no shared `ServerCore`, no global session table or token namespace, no shared clock, and one sequential sender per socket, which is what keeps the socket-wide reply traffic class race-free. - Ownership is layered and the layers do not blur: `Server` is the one-listener primitive (one socket, one `ServerCore`, one sequential loop), and `ServerSet` supervises one or more of them without owning protocol or session state itself. A common `ServerConfig` is cloned into each listener, so its bounds — `max_sessions` included — are per listener and not per process. Listeners share nothing else: no shared `ServerCore`, no global session table or token namespace, no shared clock, and one sequential sender per socket, which is what keeps the socket-wide reply traffic class race-free.
- The CLI server applet exists behind the `irtt-cli` `server` feature, which is part of that crate's default build. It is thin orchestration/configuration over reusable `irtt-server` — one current-thread Tokio runtime, one repeatable `--bind`, one `ServerSet` — and must not grow a second server state machine. **It always uses `ServerSet`, including for a single bind**: there is no direct CLI-to-`Server` path and no bind-count branch, so startup, shutdown and listener-failure semantics are identical whatever was configured, and the singleton case exercises the same orchestration in normal use. Hostname resolution, interface expansion, daemonization, and configuration files are deliberately absent. - The server applet exists behind the `irtt-rs` application package's `server` feature, which is part of that crate's default build. It is thin orchestration/configuration over reusable `irtt-server` — one current-thread Tokio runtime, one repeatable `--bind`, one `ServerSet` — and must not grow a second server state machine. **It always uses `ServerSet`, including for a single bind**: there is no direct CLI-to-`Server` path and no bind-count branch, so startup, shutdown and listener-failure semantics are identical whatever was configured, and the singleton case exercises the same orchestration in normal use. Hostname resolution, interface expansion, daemonization, and configuration files are deliberately absent.
- Resource policy: the server should use bounded session/resource policy. Upstream's observed lack of useful default session/per-peer bounds, and its never expiring a session that has not carried an echo request (see `IRTT_SERVER_PROTOCOL_SPEC.md`), are not interoperability requirements and must not be reproduced merely for compatibility. - Resource policy: the server should use bounded session/resource policy. Upstream's observed lack of useful default session/per-peer bounds, and its never expiring a session that has not carried an echo request (see `IRTT_SERVER_PROTOCOL_SPEC.md`), are not interoperability requirements and must not be reproduced merely for compatibility.
- Testing: prefer `irtt-server` for normal compliant fake-peer behavior where practical, as far as its implemented behavior reaches. Keep narrow adversarial/raw peers for intentionally malformed, ambiguous, or non-compliant wire behavior. - Testing: prefer `irtt-server` for normal compliant fake-peer behavior where practical, as far as its implemented behavior reaches. Keep narrow adversarial/raw peers for intentionally malformed, ambiguous, or non-compliant wire behavior.
+16 -3
View File
@@ -88,6 +88,17 @@ release. The format is loosely based on
- `LateReplyMode` (`Measure` / `CountOnly`) lets a consumer choose whether replies that arrive after their probe is considered late are still fully measured into the running statistics or only counted, without affecting the other samples. - `LateReplyMode` (`Measure` / `CountOnly`) lets a consumer choose whether replies that arrive after their probe is considered late are still fully measured into the running statistics or only counted, without affecting the other samples.
- `StatsConfig::estimated_retained_bytes(probe_count)` gives callers an API to estimate the memory a stats configuration will retain for a given probe count, ahead of actually running a session (used by `irtt-cli`'s multi-target memory-usage warning, see below). - `StatsConfig::estimated_retained_bytes(probe_count)` gives callers an API to estimate the memory a stats configuration will retain for a given probe count, ahead of actually running a session (used by `irtt-cli`'s multi-target memory-usage warning, see below).
## irtt-rs
### 0.6.0
#### Changed
- The application Cargo package was renamed from `irtt-cli` to `irtt-rs`, moving to `crates/irtt-app`. This is the application distribution package that produces the `irtt-rs`, `irtt-client`, `irtt-tui`, and `irtt-server` binaries; it is unrelated to the reusable `irtt-client` library crate, which keeps its existing name and version.
- The `irtt-cli` executable was hard-renamed to `irtt-client` to avoid reading as an umbrella CLI now that the application also ships a first-class server. There is no `irtt-cli` compatibility binary or alias.
- Dedicated binaries now have role-specific entry points (`irtt-client`, `irtt-tui`, `irtt-server`) instead of all funneling through the multicall dispatcher's argv0/subcommand logic. Each dedicated binary always runs its own role regardless of the name it is invoked or copied under, and no longer links code for the other applet roles it doesn't implement. The `irtt-rs` binary remains the feature-selectable multicall dispatcher and is the only binary that inspects argv0 or subcommands.
- Future application releases move to the `irtt-rs/vX.Y.Z` tag namespace (previously `irtt-cli/vX.Y.Z`); see [Releasing](#releasing).
## irtt-cli ## irtt-cli
### 0.5.0 ### 0.5.0
@@ -125,6 +136,8 @@ rather than in lockstep. Each crate's release is tagged as `<crate>/vX.Y.Z`
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); 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. `irtt-cli` is the only crate with prebuilt binary artifacts: GitHub Release. The application package (`irtt-cli` through 0.5.0, `irtt-rs`
pushing an `irtt-cli/vX.Y.Z` tag triggers [cargo-dist](https://opensource.axo.dev/cargo-dist/) from 0.6.0 onward) is the only package with prebuilt binary artifacts: pushing
to build and publish a GitHub Release with platform binaries. 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
`irtt-cli/v0.5.0` tag remains as released; it is not rewritten or replaced.
Generated
+15 -15
View File
@@ -713,21 +713,6 @@ dependencies = [
"syn 3.0.3", "syn 3.0.3",
] ]
[[package]]
name = "irtt-cli"
version = "0.5.0"
dependencies = [
"clap",
"crossterm",
"ctrlc",
"irtt-client",
"irtt-proto",
"irtt-server",
"irtt-stats",
"ratatui",
"tokio",
]
[[package]] [[package]]
name = "irtt-client" name = "irtt-client"
version = "0.5.0" version = "0.5.0"
@@ -762,6 +747,21 @@ dependencies = [
"thiserror 2.0.20", "thiserror 2.0.20",
] ]
[[package]]
name = "irtt-rs"
version = "0.6.0"
dependencies = [
"clap",
"crossterm",
"ctrlc",
"irtt-client",
"irtt-proto",
"irtt-server",
"irtt-stats",
"ratatui",
"tokio",
]
[[package]] [[package]]
name = "irtt-server" name = "irtt-server"
version = "0.5.0" version = "0.5.0"
+1 -1
View File
@@ -4,7 +4,7 @@ members = [
"crates/irtt-client", "crates/irtt-client",
"crates/irtt-server", "crates/irtt-server",
"crates/irtt-stats", "crates/irtt-stats",
"crates/irtt-cli", "crates/irtt-app",
"crates/irtt-integration-tests", "crates/irtt-integration-tests",
] ]
resolver = "2" resolver = "2"
+26 -24
View File
@@ -31,19 +31,21 @@ From a local checkout:
```sh ```sh
git clone https://github.com/Lochnair/irtt-rs.git git clone https://github.com/Lochnair/irtt-rs.git
cd irtt-rs cd irtt-rs
cargo install --path crates/irtt-cli cargo install --path crates/irtt-app
``` ```
This installs: This installs:
* `irtt-rs`, the canonical multi-applet dispatcher * `irtt-rs`, the canonical multi-applet dispatcher
* `irtt-cli`, the stream and text client * `irtt-client`, the stream and text client
* `irtt-tui`, the terminal UI
* `irtt-server`, the UDP server * `irtt-server`, the UDP server
To also install the optional terminal UI: Each applet can also be built in isolation with `--no-default-features` plus
the applet's own feature. For example, a lightweight client-only install:
```sh ```sh
cargo install --path crates/irtt-cli --features tui cargo install --path crates/irtt-app --no-default-features --features client
``` ```
## Quick start ## Quick start
@@ -51,13 +53,13 @@ cargo install --path crates/irtt-cli --features tui
Probe a server using the default settings: Probe a server using the default settings:
```sh ```sh
irtt-cli netperf-eu.bufferbloat.net:2112 irtt-client netperf-eu.bufferbloat.net:2112
``` ```
Set the test duration and probe interval: Set the test duration and probe interval:
```sh ```sh
irtt-cli netperf-eu.bufferbloat.net:2112 \ irtt-client netperf-eu.bufferbloat.net:2112 \
--duration 30s \ --duration 30s \
--interval 100ms --interval 100ms
``` ```
@@ -65,7 +67,7 @@ irtt-cli netperf-eu.bufferbloat.net:2112 \
Run continuously until interrupted: Run continuously until interrupted:
```sh ```sh
irtt-cli netperf-eu.bufferbloat.net:2112 --duration 0 irtt-client netperf-eu.bufferbloat.net:2112 --duration 0
``` ```
Use `Ctrl-C` to stop gracefully. Use `Ctrl-C` to stop gracefully.
@@ -75,7 +77,7 @@ Use `Ctrl-C` to stop gracefully.
Probe several targets concurrently: Probe several targets concurrently:
```sh ```sh
irtt-cli host-a:2112 host-b:2112 irtt-client host-a:2112 host-b:2112
``` ```
Every target argument accepts `[LABEL=]TARGET`. An optional `LABEL=` prefix assigns the logical Every target argument accepts `[LABEL=]TARGET`. An optional `LABEL=` prefix assigns the logical
@@ -83,19 +85,19 @@ target name used in output and the TUI; without it, the target string itself is
stable labels to targets: stable labels to targets:
```sh ```sh
irtt-cli ams=ams.example.com:2112 sg=sg.example.com:2112 irtt-client ams=ams.example.com:2112 sg=sg.example.com:2112
``` ```
Labeled and unlabeled targets can be freely mixed in one argument list: Labeled and unlabeled targets can be freely mixed in one argument list:
```sh ```sh
irtt-cli host-a:2112 ams=ams.example.com:2112 irtt-client host-a:2112 ams=ams.example.com:2112
``` ```
Multi-target sessions use staggered pacing by default. To send each target's probes together: Multi-target sessions use staggered pacing by default. To send each target's probes together:
```sh ```sh
irtt-cli host-a:2112 host-b:2112 --pacing burst irtt-client host-a:2112 host-b:2112 --pacing burst
``` ```
Target labels are included in the default multi-target output. Target labels are included in the default multi-target output.
@@ -112,16 +114,16 @@ The CLI supports four event-row formats:
Examples: Examples:
```sh ```sh
irtt-cli <server> --format table irtt-client <server> --format table
irtt-cli <server> --format jsonl irtt-client <server> --format jsonl
irtt-cli <server> --format csv \ irtt-client <server> --format csv \
--columns event,seq,remote,effective_rtt_us --columns event,seq,remote,effective_rtt_us
``` ```
For a stream containing only effective RTT values in microseconds: For a stream containing only effective RTT values in microseconds:
```sh ```sh
irtt-cli <server> \ irtt-client <server> \
--format tsv \ --format tsv \
--columns effective_rtt_us \ --columns effective_rtt_us \
--header never --header never
@@ -130,7 +132,7 @@ irtt-cli <server> \
List all available columns with: List all available columns with:
```sh ```sh
irtt-cli --list-columns irtt-client --list-columns
``` ```
Useful measurement fields include: Useful measurement fields include:
@@ -164,7 +166,7 @@ It runs continuously by default. A finite duration can be selected explicitly:
irtt-tui <server> --duration 30s irtt-tui <server> --duration 30s
``` ```
Multiple targets and pacing options work the same way as in `irtt-cli`: Multiple targets and pacing options work the same way as in `irtt-client`:
```sh ```sh
irtt-tui host-a:2112 host-b:2112 --pacing burst irtt-tui host-a:2112 host-b:2112 --pacing burst
@@ -312,21 +314,21 @@ synchronous callers. Callers that already own Tokio can drive
## Binaries and features ## Binaries and features
| Build | Binaries | | Build | Binaries |
| ----------------------------------------- | ------------------------------------------------- | | -------------------------------------------- | --------------------------------------------------- |
| `--no-default-features` | `irtt-rs` | | `--no-default-features` | `irtt-rs` |
| `--no-default-features --features client` | `irtt-rs`, `irtt-client` |
| `--no-default-features --features server` | `irtt-rs`, `irtt-server` | | `--no-default-features --features server` | `irtt-rs`, `irtt-server` |
| Default features | `irtt-rs`, `irtt-cli`, `irtt-server` | | `--no-default-features --features tui` | `irtt-rs`, `irtt-tui` |
| `--features tui` | `irtt-rs`, `irtt-cli`, `irtt-server`, `irtt-tui` | | Default features (or `--all-features`) | `irtt-rs`, `irtt-client`, `irtt-tui`, `irtt-server` |
| `--all-features` | `irtt-rs`, `irtt-cli`, `irtt-server`, `irtt-tui` |
`irtt-cli` requires the `client` feature, `irtt-server` the `server` feature, `irtt-client` requires the `client` feature, `irtt-server` the `server` feature,
and `irtt-tui` the `tui` feature. The `irtt-rs` dispatcher is always built, and and `irtt-tui` the `tui` feature. The `irtt-rs` dispatcher is always built, and
reports which applets the build actually contains. reports which applets the build actually contains.
For available command-line options: For available command-line options:
```sh ```sh
irtt-cli --help irtt-client --help
irtt-server --help irtt-server --help
irtt-tui --help irtt-tui --help
``` ```
@@ -339,7 +341,7 @@ Common verification commands:
cargo fmt --check cargo fmt --check
cargo test --workspace cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings cargo clippy --workspace --all-targets -- -D warnings
cargo build -p irtt-cli --all-features --release cargo build -p irtt-rs --all-features --release
``` ```
## Project status ## Project status
@@ -1,7 +1,7 @@
[package] [package]
name = "irtt-cli" name = "irtt-rs"
version = "0.5.0" version = "0.6.0"
description = "Command-line IRTT-compatible client and server" 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
@@ -12,6 +12,7 @@ keywords = ["irtt", "networking", "latency", "rtt"]
categories = ["command-line-utilities", "network-programming"] categories = ["command-line-utilities", "network-programming"]
[lib] [lib]
name = "irtt_app"
doc = false doc = false
[[bin]] [[bin]]
@@ -19,9 +20,12 @@ name = "irtt-rs"
path = "src/bin/irtt-rs.rs" path = "src/bin/irtt-rs.rs"
[[bin]] [[bin]]
name = "irtt-cli" name = "irtt-client"
path = "src/bin/irtt-cli.rs" path = "src/bin/irtt-client.rs"
required-features = ["client"] required-features = ["client"]
# This applet shares its name with the irtt-client library crate, whose rustdoc
# output it would otherwise collide with.
doc = false
[[bin]] [[bin]]
name = "irtt-tui" name = "irtt-tui"
@@ -20,7 +20,7 @@ pub enum AppletDispatch {
pub fn detect_applet_from_argv0(argv0: &str) -> Option<RequestedApplet> { pub fn detect_applet_from_argv0(argv0: &str) -> Option<RequestedApplet> {
match applet_basename(argv0) { match applet_basename(argv0) {
"irtt-cli" => Some(RequestedApplet::Client), "irtt-client" => Some(RequestedApplet::Client),
"irtt-tui" => Some(RequestedApplet::Tui), "irtt-tui" => Some(RequestedApplet::Tui),
"irttd" | "irtt-server" => Some(RequestedApplet::Server), "irttd" | "irtt-server" => Some(RequestedApplet::Server),
"irtt-rs" => None, "irtt-rs" => None,
@@ -58,7 +58,7 @@ pub fn dispatch_from_argv(argv: Vec<OsString>) -> Result<AppletDispatch, String>
"-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,
argv: applet_argv("irtt-cli", &argv[2..]), argv: applet_argv("irtt-client", &argv[2..]),
}), }),
"tui" => Ok(AppletDispatch::Run { "tui" => Ok(AppletDispatch::Run {
applet: RequestedApplet::Tui, applet: RequestedApplet::Tui,
@@ -111,7 +111,7 @@ fn server_applet_about() -> &'static str {
} }
fn known_applet_binary_names() -> &'static str { fn known_applet_binary_names() -> &'static str {
"irtt-rs, irtt-cli, irtt-tui, irtt-server" "irtt-rs, irtt-client, irtt-tui, irtt-server"
} }
fn applet_command_names() -> &'static str { fn applet_command_names() -> &'static str {
@@ -119,11 +119,11 @@ fn applet_command_names() -> &'static str {
} }
fn dispatcher_after_help() -> String { fn dispatcher_after_help() -> String {
let mut help = "Recognized applet binary names: irtt-cli, irtt-tui, irtt-server".to_owned(); let mut help = "Recognized applet binary names: irtt-client, irtt-tui, irtt-server".to_owned();
let mut missing = Vec::new(); let mut missing = Vec::new();
if !cfg!(feature = "client") { if !cfg!(feature = "client") {
missing.push("irtt-cli requires the client feature"); missing.push("irtt-client requires the client feature");
} }
if !cfg!(feature = "server") { if !cfg!(feature = "server") {
missing.push("irtt-server requires the server feature"); missing.push("irtt-server requires the server feature");
@@ -188,7 +188,7 @@ mod tests {
#[test] #[test]
fn applet_detection_maps_binary_names() { fn applet_detection_maps_binary_names() {
assert_eq!( assert_eq!(
detect_applet_from_argv0("/usr/bin/irtt-cli"), detect_applet_from_argv0("/usr/bin/irtt-client"),
Some(RequestedApplet::Client) Some(RequestedApplet::Client)
); );
assert_eq!( assert_eq!(
@@ -203,6 +203,12 @@ mod tests {
assert_eq!(detect_applet_from_argv0("custom-name"), None); assert_eq!(detect_applet_from_argv0("custom-name"), None);
} }
#[test]
fn old_irtt_cli_binary_name_is_no_longer_a_recognized_applet() {
assert_eq!(detect_applet_from_argv0("irtt-cli"), None);
assert_eq!(detect_applet_from_argv0("/usr/bin/irtt-cli"), None);
}
#[test] #[test]
fn applet_detection_ignores_the_platform_executable_suffix() { fn applet_detection_ignores_the_platform_executable_suffix() {
let exe = std::env::consts::EXE_SUFFIX; let exe = std::env::consts::EXE_SUFFIX;
@@ -212,7 +218,7 @@ mod tests {
Some(RequestedApplet::Server) Some(RequestedApplet::Server)
); );
assert_eq!( assert_eq!(
detect_applet_from_argv0(&format!("irtt-cli{exe}")), detect_applet_from_argv0(&format!("irtt-client{exe}")),
Some(RequestedApplet::Client) Some(RequestedApplet::Client)
); );
@@ -288,7 +294,7 @@ mod tests {
.unwrap(), .unwrap(),
AppletDispatch::Run { AppletDispatch::Run {
applet: RequestedApplet::Client, applet: RequestedApplet::Client,
argv: vec!["irtt-cli".into(), "host:2112".into()], argv: vec!["irtt-client".into(), "host:2112".into()],
} }
); );
assert_eq!( assert_eq!(
@@ -311,7 +317,7 @@ mod tests {
fn unknown_irtt_binary_name_is_an_error() { fn unknown_irtt_binary_name_is_an_error() {
let err = dispatch_from_argv(vec!["/usr/bin/irtt-typo".into()]).unwrap_err(); let err = dispatch_from_argv(vec!["/usr/bin/irtt-typo".into()]).unwrap_err();
assert!(err.contains("unknown applet name 'irtt-typo'")); assert!(err.contains("unknown applet name 'irtt-typo'"));
assert!(err.contains("irtt-cli")); assert!(err.contains("irtt-client"));
if cfg!(feature = "tui") { if cfg!(feature = "tui") {
assert!(err.contains("irtt-tui")); assert!(err.contains("irtt-tui"));
} }
@@ -1,5 +1,5 @@
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
irtt_cli::entry::main() irtt_app::entry::client_main()
} }
@@ -1,5 +1,5 @@
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
irtt_cli::entry::main() irtt_app::entry::dispatcher_main()
} }
@@ -1,5 +1,5 @@
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
irtt_cli::entry::main() irtt_app::entry::server_main()
} }
@@ -1,5 +1,5 @@
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
irtt_cli::entry::main() irtt_app::entry::tui_main()
} }
@@ -9,14 +9,14 @@ 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-cli", about = "Minimal IRTT-compatible stream client")] #[command(name = "irtt-client", about = "Minimal IRTT-compatible stream client")]
pub struct ClientArgs { pub struct ClientArgs {
/// Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode. /// Server address/host, optionally prefixed with LABEL=. 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=. Repeat for multi-target mode.\n\nExamples:\n irtt-cli host-a:2112 host-b:2112\n irtt-cli eu=host-a:2112 us=host-b:2112\n irtt-cli host-a:2112 eu=host-b:2112" long_help = "Server address/host, optionally prefixed with LABEL=. 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 host-a:2112 eu=host-b:2112"
)] )]
pub targets: Vec<TargetArg>, pub targets: Vec<TargetArg>,
@@ -149,7 +149,7 @@ mod tests {
use irtt_proto::{Clock, ReceivedStats, StampAt}; use irtt_proto::{Clock, ReceivedStats, StampAt};
fn parse(args: &[&str]) -> Result<ClientArgs, clap::Error> { fn parse(args: &[&str]) -> Result<ClientArgs, clap::Error> {
let mut argv = vec!["irtt-cli"]; let mut argv = vec!["irtt-client"];
argv.extend_from_slice(args); argv.extend_from_slice(args);
ClientArgs::try_parse_from(argv) ClientArgs::try_parse_from(argv)
} }
File renamed without changes.
@@ -392,7 +392,7 @@ mod tests {
use clap::Parser; use clap::Parser;
ClientArgs::try_parse_from([ ClientArgs::try_parse_from([
"irtt-cli", "irtt-client",
"--duration", "--duration",
"4200s", "4200s",
"--interval", "--interval",
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+133
View File
@@ -0,0 +1,133 @@
#[cfg(any(feature = "client", feature = "server", feature = "tui"))]
use std::sync::{atomic::AtomicBool, Arc};
use std::{env, ffi::OsString, process::ExitCode};
use crate::applet::{dispatch_from_argv, AppletDispatch, RequestedApplet};
#[cfg(any(feature = "client", feature = "server", feature = "tui"))]
use crate::signal::install_signal_handler;
/// Entry point for the `irtt-rs` multicall dispatcher binary.
///
/// Chooses an applet by subcommand or by recognized argv0 and may reference
/// every applet enabled by the active feature set.
pub fn dispatcher_main() -> ExitCode {
run_with_exit_code("irtt-rs", run_dispatcher_from_env)
}
/// Entry point for the dedicated `irtt-client` binary.
///
/// Runs the stream client directly; it never consults argv0 or the applet
/// dispatcher, so a renamed or copied executable still runs the client.
#[cfg(feature = "client")]
pub fn client_main() -> ExitCode {
run_with_exit_code("irtt-client", || {
run_client_applet(env::args_os().collect())
})
}
/// Entry point for the dedicated `irtt-tui` binary.
///
/// Runs the terminal UI directly; it never consults argv0 or the applet
/// dispatcher, so a renamed or copied executable still runs the TUI.
#[cfg(feature = "tui")]
pub fn tui_main() -> ExitCode {
run_with_exit_code("irtt-tui", || run_tui_applet(env::args_os().collect()))
}
/// Entry point for the dedicated `irtt-server` binary.
///
/// Runs the server directly; it never consults argv0 or the applet
/// dispatcher, so a renamed or copied executable still runs the server.
#[cfg(feature = "server")]
pub fn server_main() -> ExitCode {
run_with_exit_code("irtt-server", || {
run_server_applet(env::args_os().collect())
})
}
fn run_with_exit_code(
program: &str,
f: impl FnOnce() -> Result<(), Box<dyn std::error::Error>>,
) -> ExitCode {
match f() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("{program}: {err}");
ExitCode::FAILURE
}
}
}
fn run_dispatcher_from_env() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<OsString> = env::args_os().collect();
let (requested, argv) = match dispatch_from_argv(argv)? {
AppletDispatch::Run { applet, argv } => (applet, argv),
AppletDispatch::Help(help) => {
print!("{help}");
return Ok(());
}
};
match requested {
RequestedApplet::Client => run_client_applet(argv),
RequestedApplet::Tui => run_tui_applet(argv),
RequestedApplet::Server => run_server_applet(argv),
}
}
/// Installs the shutdown signal handler shared by every applet and hands the
/// resulting flag to `f`.
#[cfg(any(feature = "client", feature = "server", feature = "tui"))]
fn with_shutdown_flag(
f: impl FnOnce(&AtomicBool) -> Result<(), Box<dyn std::error::Error>>,
) -> Result<(), Box<dyn std::error::Error>> {
let shutdown_requested = Arc::new(AtomicBool::new(false));
install_signal_handler(Arc::clone(&shutdown_requested))
.map_err(|err| format!("failed to install signal handler: {err}"))?;
f(shutdown_requested.as_ref())
}
#[cfg(feature = "client")]
fn run_client_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
with_shutdown_flag(|shutdown_requested| {
let args = crate::cmd::client::ClientArgs::parse_from(argv);
crate::cmd::client::run_stream(args, shutdown_requested)
})
}
#[cfg(not(feature = "client"))]
fn run_client_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
Err("client applet is not available; rebuild with the client feature".into())
}
#[cfg(feature = "server")]
fn run_server_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
with_shutdown_flag(|shutdown_requested| {
let args = crate::cmd::server::ServerArgs::parse_from(argv);
crate::cmd::server::run_server(args, shutdown_requested)
})
}
#[cfg(not(feature = "server"))]
fn run_server_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
Err("server applet is not available; rebuild with the server feature".into())
}
#[cfg(feature = "tui")]
fn run_tui_applet(argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
with_shutdown_flag(|shutdown_requested| {
let args = crate::cmd::tui::TuiArgs::parse_from(argv);
crate::cmd::tui::run_tui(args, shutdown_requested)
})
}
#[cfg(not(feature = "tui"))]
fn run_tui_applet(_argv: Vec<OsString>) -> Result<(), Box<dyn std::error::Error>> {
Err("TUI applet is not available; rebuild with the tui feature".into())
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -13,6 +13,31 @@ fn irtt_rs_with_arg0(arg0: &str) -> Command {
command command
} }
/// Copies a dedicated binary to `name` in a temp dir and returns a `Command`
/// invoking it under that name, proving dedicated binaries decide their role
/// from the entry point they were built with, not from argv0.
#[cfg(all(unix, any(feature = "client", feature = "server")))]
fn copied_binary(cargo_bin_exe: &str, name: &str) -> Command {
let dir = std::env::temp_dir().join(format!(
"irtt-copied-binary-test-{}-{}",
std::process::id(),
name
));
std::fs::create_dir_all(&dir).unwrap();
let dest = dir.join(name);
std::fs::copy(cargo_bin_exe, &dest).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&dest).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dest, perms).unwrap();
}
Command::new(dest)
}
fn output_text(output: &Output) -> String { fn output_text(output: &Output) -> String {
let mut text = String::new(); let mut text = String::new();
text.push_str(&String::from_utf8_lossy(&output.stdout)); text.push_str(&String::from_utf8_lossy(&output.stdout));
@@ -97,9 +122,37 @@ fn canonical_server_subcommand_reports_unavailable_when_disabled() {
); );
} }
#[cfg(feature = "client")]
#[test]
fn dedicated_client_binary_always_enters_the_client_role() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible stream client"));
assert!(text.contains("--format <FORMAT>"));
}
#[cfg(feature = "tui")]
#[test]
fn dedicated_tui_binary_always_enters_the_tui_role() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-tui"))
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible TUI client"));
assert!(text.contains("--duration <DURATION>"));
}
#[cfg(feature = "server")] #[cfg(feature = "server")]
#[test] #[test]
fn standalone_server_binary_exposes_the_same_arguments() { fn dedicated_server_binary_always_enters_the_server_role() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-server")) let output = std::process::Command::new(env!("CARGO_BIN_EXE_irtt-server"))
.arg("--help") .arg("--help")
.output() .output()
@@ -111,11 +164,37 @@ fn standalone_server_binary_exposes_the_same_arguments() {
assert!(text.contains("--bind <ADDR>")); assert!(text.contains("--bind <ADDR>"));
} }
#[cfg(all(unix, feature = "client"))]
#[test]
fn renaming_the_dedicated_client_binary_does_not_change_its_role() {
let output = copied_binary(env!("CARGO_BIN_EXE_irtt-client"), "whatever")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible stream client"));
}
#[cfg(all(unix, feature = "server"))]
#[test]
fn renaming_the_dedicated_server_binary_does_not_change_its_role() {
let output = copied_binary(env!("CARGO_BIN_EXE_irtt-server"), "irtt-cli")
.arg("--help")
.output()
.unwrap();
let text = output_text(&output);
assert!(output.status.success(), "{text}");
assert!(text.contains("Minimal IRTT-compatible UDP server"));
}
#[cfg(unix)] #[cfg(unix)]
#[cfg(feature = "client")] #[cfg(feature = "client")]
#[test] #[test]
fn client_applet_name_dispatches_to_client() { fn client_applet_name_dispatches_to_client() {
let output = irtt_rs_with_arg0("irtt-cli") let output = irtt_rs_with_arg0("irtt-client")
.arg("--help") .arg("--help")
.output() .output()
.unwrap(); .unwrap();
@@ -130,7 +209,7 @@ fn client_applet_name_dispatches_to_client() {
#[cfg(all(unix, not(feature = "client")))] #[cfg(all(unix, not(feature = "client")))]
#[test] #[test]
fn client_applet_name_reports_unavailable_when_disabled() { fn client_applet_name_reports_unavailable_when_disabled() {
let output = irtt_rs_with_arg0("irtt-cli") let output = irtt_rs_with_arg0("irtt-client")
.arg("--help") .arg("--help")
.output() .output()
.unwrap(); .unwrap();
@@ -205,7 +284,17 @@ fn unknown_irtt_applet_name_errors() {
assert!(!output.status.success()); assert!(!output.status.success());
assert!(text.contains("unknown applet name 'irtt-typo'"), "{text}"); assert!(text.contains("unknown applet name 'irtt-typo'"), "{text}");
assert!(text.contains("irtt-cli")); assert!(text.contains("irtt-client"));
assert!(text.contains("irtt-tui")); assert!(text.contains("irtt-tui"));
assert!(text.contains("irtt-server")); assert!(text.contains("irtt-server"));
} }
#[cfg(unix)]
#[test]
fn old_irtt_cli_binary_name_is_no_longer_a_recognized_applet() {
let output = irtt_rs_with_arg0("irtt-cli").output().unwrap();
let text = output_text(&output);
assert!(!output.status.success());
assert!(text.contains("unknown applet name 'irtt-cli'"), "{text}");
}
@@ -55,7 +55,7 @@ impl InterruptibleFakeServer {
#[test] #[test]
fn old_target_option_is_rejected_as_an_unknown_argument() { fn old_target_option_is_rejected_as_an_unknown_argument() {
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args(["--target", "eu=127.0.0.1:2112"]) .args(["--target", "eu=127.0.0.1:2112"])
.output() .output()
.unwrap(); .unwrap();
@@ -67,7 +67,7 @@ fn old_target_option_is_rejected_as_an_unknown_argument() {
#[test] #[test]
fn list_columns_succeeds_without_target() { fn list_columns_succeeds_without_target() {
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.arg("--list-columns") .arg("--list-columns")
.output() .output()
.unwrap(); .unwrap();
@@ -88,7 +88,7 @@ fn single_target_default_table_includes_target_and_accepts_pacing() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let target = server.addr.to_string(); let target = server.addr.to_string();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -125,7 +125,7 @@ fn single_target_default_table_includes_target_and_accepts_pacing() {
fn single_labeled_target_default_table_includes_target() { fn single_labeled_target_default_table_includes_target() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -160,7 +160,7 @@ fn single_labeled_target_default_table_includes_target() {
fn single_labeled_target_default_csv_includes_target_and_event_wall_ns() { fn single_labeled_target_default_csv_includes_target_and_event_wall_ns() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -199,7 +199,7 @@ fn single_positional_target_default_csv_includes_target_as_positional_label() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let target = server.addr.to_string(); let target = server.addr.to_string();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -237,7 +237,7 @@ fn multi_target_default_table_includes_both_labels() {
let a = InTreeServer::start(); let a = InTreeServer::start();
let b = InTreeServer::start(); let b = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"40ms", "40ms",
@@ -287,7 +287,7 @@ fn multi_target_final_summary_preserves_cli_argument_order() {
let a = InTreeServer::start(); let a = InTreeServer::start();
let m = InTreeServer::start(); let m = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"40ms", "40ms",
@@ -331,7 +331,7 @@ fn mixed_labeled_and_unlabeled_positional_targets_both_appear_in_output() {
let b = InTreeServer::start(); let b = InTreeServer::start();
let unlabeled_addr = a.addr.to_string(); let unlabeled_addr = a.addr.to_string();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"40ms", "40ms",
@@ -376,7 +376,7 @@ fn single_target_custom_target_column_renders_positional_label() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let target = server.addr.to_string(); let target = server.addr.to_string();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -414,7 +414,7 @@ fn single_target_custom_target_column_renders_positional_label() {
fn custom_columns_remain_authoritative_and_omit_target() { fn custom_columns_remain_authoritative_and_omit_target() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -448,7 +448,7 @@ fn custom_columns_remain_authoritative_and_omit_target() {
fn columns_default_keyword_includes_target_for_single_target() { fn columns_default_keyword_includes_target_for_single_target() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -484,7 +484,7 @@ fn columns_default_keyword_includes_target_for_single_target() {
fn single_labeled_target_default_jsonl_includes_target() { fn single_labeled_target_default_jsonl_includes_target() {
let server = InTreeServer::start(); let server = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"30ms", "30ms",
@@ -517,7 +517,7 @@ fn single_labeled_target_default_jsonl_includes_target() {
#[test] #[test]
fn continuous_single_target_peer_close_exits_nonzero() { fn continuous_single_target_peer_close_exits_nonzero() {
let server = start_peer_close_server(test_params(None, Duration::from_millis(10))); let server = start_peer_close_server(test_params(None, Duration::from_millis(10)));
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
@@ -553,7 +553,7 @@ fn continuous_single_target_peer_close_exits_nonzero() {
fn continuous_single_target_interruption_drains_final_events_and_succeeds() { fn continuous_single_target_interruption_drains_final_events_and_succeeds() {
let params = test_params(None, Duration::from_millis(10)); let params = test_params(None, Duration::from_millis(10));
let server = start_interruptible_echo_server(params); let server = start_interruptible_echo_server(params);
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
@@ -611,7 +611,7 @@ fn multi_target_csv_emits_rows_for_both_labels() {
let a = InTreeServer::start(); let a = InTreeServer::start();
let b = InTreeServer::start(); let b = InTreeServer::start();
let output = Command::new(env!("CARGO_BIN_EXE_irtt-cli")) let output = Command::new(env!("CARGO_BIN_EXE_irtt-client"))
.args([ .args([
"--duration", "--duration",
"40ms", "40ms",
@@ -654,7 +654,7 @@ fn finite_multi_target_peer_close_is_accepted_as_completion() {
let params = test_params(Some(Duration::from_millis(40)), Duration::from_millis(10)); let params = test_params(Some(Duration::from_millis(40)), Duration::from_millis(10));
let a = start_peer_close_server(params.clone()); let a = start_peer_close_server(params.clone());
let b = start_peer_close_server(params); let b = start_peer_close_server(params);
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"40ms", "40ms",
@@ -716,7 +716,7 @@ fn continuous_all_peer_closed_targets_exit_nonzero() {
.send(()) .send(())
.expect("target B stopped waiting for peer-close release"); .expect("target B stopped waiting for peer-close release");
}); });
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
@@ -766,7 +766,7 @@ fn continuous_partial_peer_close_preserves_queued_rows_and_reports_peer_close()
let (healthy_reply_tx, healthy_reply_rx) = mpsc::channel(); let (healthy_reply_tx, healthy_reply_rx) = mpsc::channel();
let healthy = start_echo_server_with_first_reply(params.clone(), healthy_reply_tx); let healthy = start_echo_server_with_first_reply(params.clone(), healthy_reply_tx);
let peer_closed = start_gated_peer_close_server(params, vec![healthy_reply_rx]); let peer_closed = start_gated_peer_close_server(params, vec![healthy_reply_rx]);
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
@@ -820,7 +820,7 @@ fn continuous_mixed_peer_close_and_open_failure_exits_nonzero() {
let failure = start_open_failure_server_with_signal(params.clone(), failure_tx); let failure = start_open_failure_server_with_signal(params.clone(), failure_tx);
let healthy = start_echo_server_with_first_reply(params.clone(), healthy_reply_tx); let healthy = start_echo_server_with_first_reply(params.clone(), healthy_reply_tx);
let peer_closed = start_gated_peer_close_server(params, vec![failure_rx, healthy_reply_rx]); let peer_closed = start_gated_peer_close_server(params, vec![failure_rx, healthy_reply_rx]);
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
@@ -872,7 +872,7 @@ fn explicit_group_interruption_succeeds_without_peer_close_error() {
let params = test_params(None, Duration::from_millis(10)); let params = test_params(None, Duration::from_millis(10));
let a = start_interruptible_echo_server(params.clone()); let a = start_interruptible_echo_server(params.clone());
let b = start_interruptible_echo_server(params); let b = start_interruptible_echo_server(params);
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
@@ -932,7 +932,7 @@ fn all_open_failures_exit_nonzero_with_diagnostics() {
let params = test_params(None, Duration::from_millis(10)); let params = test_params(None, Duration::from_millis(10));
let a = start_open_failure_server(params.clone()); let a = start_open_failure_server(params.clone());
let b = start_open_failure_server(params); let b = start_open_failure_server(params);
let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client"));
command.args([ command.args([
"--duration", "--duration",
"0s", "0s",
-99
View File
@@ -1,99 +0,0 @@
use std::{
env,
ffi::OsString,
process::ExitCode,
sync::{atomic::AtomicBool, Arc},
};
use crate::{
applet::{dispatch_from_argv, AppletDispatch, RequestedApplet},
signal::install_signal_handler,
};
pub fn main() -> ExitCode {
match run_from_env() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("irtt-rs: {err}");
ExitCode::FAILURE
}
}
}
fn run_from_env() -> Result<(), Box<dyn std::error::Error>> {
let argv: Vec<OsString> = env::args_os().collect();
let (requested, argv) = match dispatch_from_argv(argv)? {
AppletDispatch::Run { applet, argv } => (applet, argv),
AppletDispatch::Help(help) => {
print!("{help}");
return Ok(());
}
};
let shutdown_requested = Arc::new(AtomicBool::new(false));
install_signal_handler(Arc::clone(&shutdown_requested))
.map_err(|err| format!("failed to install signal handler: {err}"))?;
match requested {
RequestedApplet::Client => run_client_applet(argv, shutdown_requested.as_ref()),
RequestedApplet::Tui => run_tui_applet(argv, shutdown_requested.as_ref()),
RequestedApplet::Server => run_server_applet(argv, shutdown_requested.as_ref()),
}
}
#[cfg(feature = "client")]
fn run_client_applet(
argv: Vec<OsString>,
shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
let args = crate::cmd::client::ClientArgs::parse_from(argv);
crate::cmd::client::run_stream(args, shutdown_requested)
}
#[cfg(not(feature = "client"))]
fn run_client_applet(
_argv: Vec<OsString>,
_shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
Err("client applet is not available; rebuild with the client feature".into())
}
#[cfg(feature = "server")]
fn run_server_applet(
argv: Vec<OsString>,
shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
let args = crate::cmd::server::ServerArgs::parse_from(argv);
crate::cmd::server::run_server(args, shutdown_requested)
}
#[cfg(not(feature = "server"))]
fn run_server_applet(
_argv: Vec<OsString>,
_shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
Err("server applet is not available; rebuild with the server feature".into())
}
#[cfg(feature = "tui")]
fn run_tui_applet(
argv: Vec<OsString>,
shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
use clap::Parser;
let args = crate::cmd::tui::TuiArgs::parse_from(argv);
crate::cmd::tui::run_tui(args, shutdown_requested)
}
#[cfg(not(feature = "tui"))]
fn run_tui_applet(
_argv: Vec<OsString>,
_shutdown_requested: &AtomicBool,
) -> Result<(), Box<dyn std::error::Error>> {
Err("TUI applet is not available; rebuild with the tui feature".into())
}
+2 -2
View File
@@ -11,7 +11,7 @@ ci = "github"
installers = [] installers = []
# Target platforms to build apps for (Rust target-triple syntax) # Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"] targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
# Scope dist's tag matching to irtt-cli releases only: irtt-proto/irtt-client/ # Scope dist's tag matching to irtt-rs releases only: irtt-proto/irtt-client/
# irtt-server/irtt-stats are libraries published to crates.io and tagged for # irtt-server/irtt-stats are libraries published to crates.io and tagged for
# provenance, but must not trigger cargo-dist or produce a GitHub Release. # provenance, but must not trigger cargo-dist or produce a GitHub Release.
tag-namespace = "irtt-cli" tag-namespace = "irtt-rs"
+3 -3
View File
@@ -13,7 +13,7 @@ upstream executable.
Build `irtt-rs` and ensure an upstream `irtt` executable is available: Build `irtt-rs` and ensure an upstream `irtt` executable is available:
```sh ```sh
cargo build -p irtt-cli cargo build -p irtt-rs --bin irtt-client
go install github.com/heistp/irtt/cmd/irtt@v0.9.1 go install github.com/heistp/irtt/cmd/irtt@v0.9.1
``` ```
@@ -118,13 +118,13 @@ python3 scripts/run_netem_interop_ci.py \
On Linux: On Linux:
```sh ```sh
cargo build -p irtt-cli cargo build -p irtt-rs --bin irtt-client
go install github.com/heistp/irtt/cmd/irtt@v0.9.1 go install github.com/heistp/irtt/cmd/irtt@v0.9.1
python3 scripts/run_netem_interop_ci.py \ python3 scripts/run_netem_interop_ci.py \
--all \ --all \
--skip-build \ --skip-build \
--upstream-irtt "$HOME/go/bin/irtt" \ --upstream-irtt "$HOME/go/bin/irtt" \
--irtt-rs-command "$PWD/target/debug/irtt-cli" --irtt-rs-command "$PWD/target/debug/irtt-client"
``` ```
Run one scenario: Run one scenario:
+1 -1
View File
@@ -21,7 +21,7 @@
"labels": ["dependencies"], "labels": ["dependencies"],
"ignorePaths": [".github/workflows/irtt-cli-release.yml"], "ignorePaths": [".github/workflows/irtt-rs-release.yml"],
"lockFileMaintenance": { "lockFileMaintenance": {
"enabled": true, "enabled": true,
+5 -5
View File
@@ -32,7 +32,7 @@ DEFAULT_REMOTE_TARGETS = [
] ]
DEFAULT_LOCAL_TARGET = ("127.0.0.1", "10s", "100ms", "local") DEFAULT_LOCAL_TARGET = ("127.0.0.1", "10s", "100ms", "local")
DEFAULT_OUTPUT_ROOT = Path("target/interop-comparisons") DEFAULT_OUTPUT_ROOT = Path("target/interop-comparisons")
DEFAULT_IRTT_RS_COMMAND = "target/debug/irtt-cli" DEFAULT_IRTT_RS_COMMAND = "target/debug/irtt-client"
@dataclass @dataclass
@@ -163,7 +163,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--skip-build", "--skip-build",
action="store_true", action="store_true",
help="Do not run cargo build -p irtt-cli before comparisons.", help="Do not run cargo build -p irtt-rs --bin irtt-client before comparisons.",
) )
parser.add_argument( parser.add_argument(
"--localhost-port", "--localhost-port",
@@ -335,8 +335,8 @@ def check_local_server(args: argparse.Namespace) -> tuple[bool, str]:
) )
def preflight_build_irtt_cli(run_dir: Path) -> bool: def preflight_build_irtt_client(run_dir: Path) -> bool:
command = ["cargo", "build", "-p", "irtt-cli"] command = ["cargo", "build", "-p", "irtt-rs", "--bin", "irtt-client"]
started = utc_now() started = utc_now()
print(f"[{started}] preflight build: {shlex.join(command)}", flush=True) print(f"[{started}] preflight build: {shlex.join(command)}", flush=True)
completed = subprocess.run( completed = subprocess.run(
@@ -1117,7 +1117,7 @@ def main() -> int:
print(message, file=sys.stderr) print(message, file=sys.stderr)
cases = [case for case in cases if not case.is_local] cases = [case for case in cases if not case.is_local]
if cases and not args.skip_build and not preflight_build_irtt_cli(run_dir): if cases and not args.skip_build and not preflight_build_irtt_client(run_dir):
write_index(run_dir, [], local_error) write_index(run_dir, [], local_error)
return 1 return 1
+7 -4
View File
@@ -105,13 +105,13 @@ def parse_args() -> argparse.Namespace:
) )
parser.add_argument( parser.add_argument(
"--irtt-rs-command", "--irtt-rs-command",
default=str(REPO_ROOT / "target" / "debug" / "irtt-cli"), default=str(REPO_ROOT / "target" / "debug" / "irtt-client"),
help="Path or command for irtt-rs client (default: target/debug/irtt-cli).", help="Path or command for irtt-rs client (default: target/debug/irtt-client).",
) )
parser.add_argument( parser.add_argument(
"--skip-build", "--skip-build",
action="store_true", action="store_true",
help="Do not run cargo build -p irtt-cli before comparisons.", help="Do not run cargo build -p irtt-rs --bin irtt-client before comparisons.",
) )
parser.add_argument( parser.add_argument(
"--sudo", "--sudo",
@@ -176,7 +176,10 @@ def main() -> int:
upstream_irtt = resolve_command(args.upstream_irtt) upstream_irtt = resolve_command(args.upstream_irtt)
irtt_rs_command = resolve_irtt_rs_command(args.irtt_rs_command) irtt_rs_command = resolve_irtt_rs_command(args.irtt_rs_command)
if not args.skip_build: if not args.skip_build:
run_checked(["cargo", "build", "-p", "irtt-cli"], run_dir / "cargo-build") run_checked(
["cargo", "build", "-p", "irtt-rs", "--bin", "irtt-client"],
run_dir / "cargo-build",
)
cleanup(args, quiet=True) cleanup(args, quiet=True)
setup_namespaces(args, run_dir) setup_namespaces(args, run_dir)