diff --git a/.github/workflows/actions-security.yml b/.github/workflows/actions-security.yml index 56b84e6..2e2c780 100644 --- a/.github/workflows/actions-security.yml +++ b/.github/workflows/actions-security.yml @@ -42,7 +42,7 @@ jobs: - name: actionlint generated release workflow uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 with: - files: .github/workflows/irtt-cli-release.yml + files: .github/workflows/irtt-rs-release.yml shellcheck: false zizmor: name: zizmor diff --git a/.github/workflows/interop-netem.yml b/.github/workflows/interop-netem.yml index 3c9305a..283b619 100644 --- a/.github/workflows/interop-netem.yml +++ b/.github/workflows/interop-netem.yml @@ -46,8 +46,8 @@ jobs: go install "github.com/heistp/irtt/cmd/irtt@${IRTT_UPSTREAM_VERSION}" "$GOBIN/irtt" version - - name: Build irtt-rs CLI - run: cargo build -p irtt-cli --locked + - name: Build irtt-rs client + run: cargo build -p irtt-rs --bin irtt-client --locked - name: Run netem interop comparisons run: | @@ -55,7 +55,7 @@ jobs: --all \ --skip-build \ --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 if: always() diff --git a/.github/workflows/irtt-cli-release.yml b/.github/workflows/irtt-rs-release.yml similarity index 99% rename from .github/workflows/irtt-cli-release.yml rename to .github/workflows/irtt-rs-release.yml index 1f91df0..d1dd67b 100644 --- a/.github/workflows/irtt-cli-release.yml +++ b/.github/workflows/irtt-rs-release.yml @@ -42,7 +42,7 @@ on: pull_request: push: tags: - - 'irtt-cli**[0-9]+.[0-9]+.[0-9]+*' + - 'irtt-rs**[0-9]+.[0-9]+.[0-9]+*' jobs: # Run 'dist plan' (or host) to determine what tasks we need to do diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index c2a47ef..f4f9acd 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -8,7 +8,7 @@ on: - "irtt-client/v*.*.*" - "irtt-server/v*.*.*" - "irtt-stats/v*.*.*" - - "irtt-cli/v*.*.*" + - "irtt-rs/v*.*.*" permissions: contents: read diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index 5fa0f6f..644ad47 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -75,5 +75,5 @@ jobs: - name: actionlint generated release workflow uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 with: - files: .github/workflows/irtt-cli-release.yml + files: .github/workflows/irtt-rs-release.yml shellcheck: false diff --git a/AGENTS.md b/AGENTS.md index 67e76b6..6ea7a88 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ For compatibility questions, the verified spec and black-box observations define 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-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. - `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-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. @@ -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. - 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. -- 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. - 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index d39f614..41ce832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - `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 ### 0.5.0 @@ -125,6 +136,8 @@ rather than in lockstep. Each crate's release is tagged as `/vX.Y.Z` from. `irtt-proto`, `irtt-client`, `irtt-server`, and `irtt-stats` are 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 -GitHub Release. `irtt-cli` is the only crate with prebuilt binary artifacts: -pushing an `irtt-cli/vX.Y.Z` tag triggers [cargo-dist](https://opensource.axo.dev/cargo-dist/) -to build and publish a GitHub Release with platform binaries. +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 +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. diff --git a/Cargo.lock b/Cargo.lock index 8a273f0..ae5a4b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,21 +713,6 @@ dependencies = [ "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]] name = "irtt-client" version = "0.5.0" @@ -762,6 +747,21 @@ dependencies = [ "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]] name = "irtt-server" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index c2d8201..b49baa2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "crates/irtt-client", "crates/irtt-server", "crates/irtt-stats", - "crates/irtt-cli", + "crates/irtt-app", "crates/irtt-integration-tests", ] resolver = "2" diff --git a/README.md b/README.md index e0ab0ae..974cfe2 100644 --- a/README.md +++ b/README.md @@ -31,19 +31,19 @@ From a local checkout: ```sh git clone https://github.com/Lochnair/irtt-rs.git cd irtt-rs -cargo install --path crates/irtt-cli +cargo install --path crates/irtt-app ``` This installs: * `irtt-rs`, the canonical multi-applet dispatcher -* `irtt-cli`, the stream and text client +* `irtt-client`, the stream and text client * `irtt-server`, the UDP server To also install the optional terminal UI: ```sh -cargo install --path crates/irtt-cli --features tui +cargo install --path crates/irtt-app --features tui ``` ## Quick start @@ -51,13 +51,13 @@ cargo install --path crates/irtt-cli --features tui Probe a server using the default settings: ```sh -irtt-cli netperf-eu.bufferbloat.net:2112 +irtt-client netperf-eu.bufferbloat.net:2112 ``` Set the test duration and probe interval: ```sh -irtt-cli netperf-eu.bufferbloat.net:2112 \ +irtt-client netperf-eu.bufferbloat.net:2112 \ --duration 30s \ --interval 100ms ``` @@ -65,7 +65,7 @@ irtt-cli netperf-eu.bufferbloat.net:2112 \ Run continuously until interrupted: ```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. @@ -75,7 +75,7 @@ Use `Ctrl-C` to stop gracefully. Probe several targets concurrently: ```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 @@ -83,19 +83,19 @@ target name used in output and the TUI; without it, the target string itself is stable labels to targets: ```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: ```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: ```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. @@ -112,16 +112,16 @@ The CLI supports four event-row formats: Examples: ```sh -irtt-cli --format table -irtt-cli --format jsonl -irtt-cli --format csv \ +irtt-client --format table +irtt-client --format jsonl +irtt-client --format csv \ --columns event,seq,remote,effective_rtt_us ``` For a stream containing only effective RTT values in microseconds: ```sh -irtt-cli \ +irtt-client \ --format tsv \ --columns effective_rtt_us \ --header never @@ -130,7 +130,7 @@ irtt-cli \ List all available columns with: ```sh -irtt-cli --list-columns +irtt-client --list-columns ``` Useful measurement fields include: @@ -164,7 +164,7 @@ It runs continuously by default. A finite duration can be selected explicitly: irtt-tui --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 irtt-tui host-a:2112 host-b:2112 --pacing burst @@ -315,18 +315,18 @@ synchronous callers. Callers that already own Tokio can drive | ----------------------------------------- | ------------------------------------------------- | | `--no-default-features` | `irtt-rs` | | `--no-default-features --features server` | `irtt-rs`, `irtt-server` | -| Default features | `irtt-rs`, `irtt-cli`, `irtt-server` | -| `--features tui` | `irtt-rs`, `irtt-cli`, `irtt-server`, `irtt-tui` | -| `--all-features` | `irtt-rs`, `irtt-cli`, `irtt-server`, `irtt-tui` | +| Default features | `irtt-rs`, `irtt-client`, `irtt-server` | +| `--features tui` | `irtt-rs`, `irtt-client`, `irtt-server`, `irtt-tui` | +| `--all-features` | `irtt-rs`, `irtt-client`, `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 reports which applets the build actually contains. For available command-line options: ```sh -irtt-cli --help +irtt-client --help irtt-server --help irtt-tui --help ``` @@ -339,7 +339,7 @@ Common verification commands: cargo fmt --check cargo test --workspace 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 diff --git a/crates/irtt-cli/Cargo.toml b/crates/irtt-app/Cargo.toml similarity index 84% rename from crates/irtt-cli/Cargo.toml rename to crates/irtt-app/Cargo.toml index 983d67a..6bd9698 100644 --- a/crates/irtt-cli/Cargo.toml +++ b/crates/irtt-app/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "irtt-cli" -version = "0.5.0" -description = "Command-line IRTT-compatible client and server" +name = "irtt-rs" +version = "0.6.0" +description = "IRTT-compatible client, TUI, and server applications" edition.workspace = true license.workspace = true rust-version.workspace = true @@ -12,6 +12,7 @@ keywords = ["irtt", "networking", "latency", "rtt"] categories = ["command-line-utilities", "network-programming"] [lib] +name = "irtt_app" doc = false [[bin]] @@ -19,9 +20,12 @@ name = "irtt-rs" path = "src/bin/irtt-rs.rs" [[bin]] -name = "irtt-cli" -path = "src/bin/irtt-cli.rs" +name = "irtt-client" +path = "src/bin/irtt-client.rs" 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]] name = "irtt-tui" diff --git a/crates/irtt-cli/src/applet.rs b/crates/irtt-app/src/applet.rs similarity index 92% rename from crates/irtt-cli/src/applet.rs rename to crates/irtt-app/src/applet.rs index 34a8d38..aa89942 100644 --- a/crates/irtt-cli/src/applet.rs +++ b/crates/irtt-app/src/applet.rs @@ -20,7 +20,7 @@ pub enum AppletDispatch { pub fn detect_applet_from_argv0(argv0: &str) -> Option { match applet_basename(argv0) { - "irtt-cli" => Some(RequestedApplet::Client), + "irtt-client" => Some(RequestedApplet::Client), "irtt-tui" => Some(RequestedApplet::Tui), "irttd" | "irtt-server" => Some(RequestedApplet::Server), "irtt-rs" => None, @@ -58,7 +58,7 @@ pub fn dispatch_from_argv(argv: Vec) -> Result "-h" | "--help" => Ok(AppletDispatch::Help(dispatcher_help())), "client" => Ok(AppletDispatch::Run { applet: RequestedApplet::Client, - argv: applet_argv("irtt-cli", &argv[2..]), + argv: applet_argv("irtt-client", &argv[2..]), }), "tui" => Ok(AppletDispatch::Run { applet: RequestedApplet::Tui, @@ -111,7 +111,7 @@ fn server_applet_about() -> &'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 { @@ -119,11 +119,11 @@ fn applet_command_names() -> &'static str { } 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(); if !cfg!(feature = "client") { - missing.push("irtt-cli requires the client feature"); + missing.push("irtt-client requires the client feature"); } if !cfg!(feature = "server") { missing.push("irtt-server requires the server feature"); @@ -188,7 +188,7 @@ mod tests { #[test] fn applet_detection_maps_binary_names() { assert_eq!( - detect_applet_from_argv0("/usr/bin/irtt-cli"), + detect_applet_from_argv0("/usr/bin/irtt-client"), Some(RequestedApplet::Client) ); assert_eq!( @@ -203,6 +203,12 @@ mod tests { 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] fn applet_detection_ignores_the_platform_executable_suffix() { let exe = std::env::consts::EXE_SUFFIX; @@ -212,7 +218,7 @@ mod tests { Some(RequestedApplet::Server) ); assert_eq!( - detect_applet_from_argv0(&format!("irtt-cli{exe}")), + detect_applet_from_argv0(&format!("irtt-client{exe}")), Some(RequestedApplet::Client) ); @@ -288,7 +294,7 @@ mod tests { .unwrap(), AppletDispatch::Run { applet: RequestedApplet::Client, - argv: vec!["irtt-cli".into(), "host:2112".into()], + argv: vec!["irtt-client".into(), "host:2112".into()], } ); assert_eq!( @@ -311,7 +317,7 @@ mod tests { fn unknown_irtt_binary_name_is_an_error() { let err = dispatch_from_argv(vec!["/usr/bin/irtt-typo".into()]).unwrap_err(); assert!(err.contains("unknown applet name 'irtt-typo'")); - assert!(err.contains("irtt-cli")); + assert!(err.contains("irtt-client")); if cfg!(feature = "tui") { assert!(err.contains("irtt-tui")); } diff --git a/crates/irtt-cli/src/bin/irtt-cli.rs b/crates/irtt-app/src/bin/irtt-client.rs similarity index 61% rename from crates/irtt-cli/src/bin/irtt-cli.rs rename to crates/irtt-app/src/bin/irtt-client.rs index 9bab261..df11473 100644 --- a/crates/irtt-cli/src/bin/irtt-cli.rs +++ b/crates/irtt-app/src/bin/irtt-client.rs @@ -1,5 +1,5 @@ use std::process::ExitCode; fn main() -> ExitCode { - irtt_cli::entry::main() + irtt_app::entry::client_main() } diff --git a/crates/irtt-cli/src/bin/irtt-rs.rs b/crates/irtt-app/src/bin/irtt-rs.rs similarity index 58% rename from crates/irtt-cli/src/bin/irtt-rs.rs rename to crates/irtt-app/src/bin/irtt-rs.rs index 9bab261..01d61e5 100644 --- a/crates/irtt-cli/src/bin/irtt-rs.rs +++ b/crates/irtt-app/src/bin/irtt-rs.rs @@ -1,5 +1,5 @@ use std::process::ExitCode; fn main() -> ExitCode { - irtt_cli::entry::main() + irtt_app::entry::dispatcher_main() } diff --git a/crates/irtt-cli/src/bin/irtt-server.rs b/crates/irtt-app/src/bin/irtt-server.rs similarity index 61% rename from crates/irtt-cli/src/bin/irtt-server.rs rename to crates/irtt-app/src/bin/irtt-server.rs index 9bab261..8cd45ca 100644 --- a/crates/irtt-cli/src/bin/irtt-server.rs +++ b/crates/irtt-app/src/bin/irtt-server.rs @@ -1,5 +1,5 @@ use std::process::ExitCode; fn main() -> ExitCode { - irtt_cli::entry::main() + irtt_app::entry::server_main() } diff --git a/crates/irtt-cli/src/bin/irtt-tui.rs b/crates/irtt-app/src/bin/irtt-tui.rs similarity index 63% rename from crates/irtt-cli/src/bin/irtt-tui.rs rename to crates/irtt-app/src/bin/irtt-tui.rs index 9bab261..c807e61 100644 --- a/crates/irtt-cli/src/bin/irtt-tui.rs +++ b/crates/irtt-app/src/bin/irtt-tui.rs @@ -1,5 +1,5 @@ use std::process::ExitCode; fn main() -> ExitCode { - irtt_cli::entry::main() + irtt_app::entry::tui_main() } diff --git a/crates/irtt-cli/src/cmd/client/args.rs b/crates/irtt-app/src/cmd/client/args.rs similarity index 97% rename from crates/irtt-cli/src/cmd/client/args.rs rename to crates/irtt-app/src/cmd/client/args.rs index 6f04f08..f2e542a 100644 --- a/crates/irtt-cli/src/cmd/client/args.rs +++ b/crates/irtt-app/src/cmd/client/args.rs @@ -9,14 +9,14 @@ use clap::{Parser, ValueEnum}; pub const DEFAULT_CLIENT_DURATION: Duration = Duration::from_secs(10); #[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 { /// Server address/host, optionally prefixed with LABEL=. Repeat for multi-target mode. #[arg( value_name = "TARGET", num_args = 0.., value_parser = parse_target, - long_help = "Server address/host, optionally prefixed with LABEL=. 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, @@ -149,7 +149,7 @@ mod tests { use irtt_proto::{Clock, ReceivedStats, StampAt}; fn parse(args: &[&str]) -> Result { - let mut argv = vec!["irtt-cli"]; + let mut argv = vec!["irtt-client"]; argv.extend_from_slice(args); ClientArgs::try_parse_from(argv) } diff --git a/crates/irtt-cli/src/cmd/client/mod.rs b/crates/irtt-app/src/cmd/client/mod.rs similarity index 100% rename from crates/irtt-cli/src/cmd/client/mod.rs rename to crates/irtt-app/src/cmd/client/mod.rs diff --git a/crates/irtt-cli/src/cmd/client/output.rs b/crates/irtt-app/src/cmd/client/output.rs similarity index 100% rename from crates/irtt-cli/src/cmd/client/output.rs rename to crates/irtt-app/src/cmd/client/output.rs diff --git a/crates/irtt-cli/src/cmd/client/run.rs b/crates/irtt-app/src/cmd/client/run.rs similarity index 99% rename from crates/irtt-cli/src/cmd/client/run.rs rename to crates/irtt-app/src/cmd/client/run.rs index 016abd4..7fa385d 100644 --- a/crates/irtt-cli/src/cmd/client/run.rs +++ b/crates/irtt-app/src/cmd/client/run.rs @@ -392,7 +392,7 @@ mod tests { use clap::Parser; ClientArgs::try_parse_from([ - "irtt-cli", + "irtt-client", "--duration", "4200s", "--interval", diff --git a/crates/irtt-cli/src/cmd/client/summary.rs b/crates/irtt-app/src/cmd/client/summary.rs similarity index 100% rename from crates/irtt-cli/src/cmd/client/summary.rs rename to crates/irtt-app/src/cmd/client/summary.rs diff --git a/crates/irtt-cli/src/cmd/format.rs b/crates/irtt-app/src/cmd/format.rs similarity index 100% rename from crates/irtt-cli/src/cmd/format.rs rename to crates/irtt-app/src/cmd/format.rs diff --git a/crates/irtt-cli/src/cmd/mod.rs b/crates/irtt-app/src/cmd/mod.rs similarity index 100% rename from crates/irtt-cli/src/cmd/mod.rs rename to crates/irtt-app/src/cmd/mod.rs diff --git a/crates/irtt-cli/src/cmd/server/args.rs b/crates/irtt-app/src/cmd/server/args.rs similarity index 100% rename from crates/irtt-cli/src/cmd/server/args.rs rename to crates/irtt-app/src/cmd/server/args.rs diff --git a/crates/irtt-cli/src/cmd/server/mod.rs b/crates/irtt-app/src/cmd/server/mod.rs similarity index 100% rename from crates/irtt-cli/src/cmd/server/mod.rs rename to crates/irtt-app/src/cmd/server/mod.rs diff --git a/crates/irtt-cli/src/cmd/server/run.rs b/crates/irtt-app/src/cmd/server/run.rs similarity index 100% rename from crates/irtt-cli/src/cmd/server/run.rs rename to crates/irtt-app/src/cmd/server/run.rs diff --git a/crates/irtt-cli/src/cmd/tui/args.rs b/crates/irtt-app/src/cmd/tui/args.rs similarity index 100% rename from crates/irtt-cli/src/cmd/tui/args.rs rename to crates/irtt-app/src/cmd/tui/args.rs diff --git a/crates/irtt-cli/src/cmd/tui/mod.rs b/crates/irtt-app/src/cmd/tui/mod.rs similarity index 100% rename from crates/irtt-cli/src/cmd/tui/mod.rs rename to crates/irtt-app/src/cmd/tui/mod.rs diff --git a/crates/irtt-cli/src/cmd/tui/run.rs b/crates/irtt-app/src/cmd/tui/run.rs similarity index 100% rename from crates/irtt-cli/src/cmd/tui/run.rs rename to crates/irtt-app/src/cmd/tui/run.rs diff --git a/crates/irtt-cli/src/cmd/tui/ui.rs b/crates/irtt-app/src/cmd/tui/ui.rs similarity index 100% rename from crates/irtt-cli/src/cmd/tui/ui.rs rename to crates/irtt-app/src/cmd/tui/ui.rs diff --git a/crates/irtt-app/src/entry.rs b/crates/irtt-app/src/entry.rs new file mode 100644 index 0000000..6615fb0 --- /dev/null +++ b/crates/irtt-app/src/entry.rs @@ -0,0 +1,136 @@ +use std::{ + env, + ffi::OsString, + process::ExitCode, + sync::{atomic::AtomicBool, Arc}, +}; + +use crate::{ + applet::{dispatch_from_argv, AppletDispatch, RequestedApplet}, + 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>, +) -> ExitCode { + match f() { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("{program}: {err}"); + ExitCode::FAILURE + } + } +} + +fn run_dispatcher_from_env() -> Result<(), Box> { + let argv: Vec = 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`. +fn with_shutdown_flag( + f: impl FnOnce(&AtomicBool) -> Result<(), Box>, +) -> Result<(), Box> { + 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) -> Result<(), Box> { + 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) -> Result<(), Box> { + Err("client applet is not available; rebuild with the client feature".into()) +} + +#[cfg(feature = "server")] +fn run_server_applet(argv: Vec) -> Result<(), Box> { + 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) -> Result<(), Box> { + Err("server applet is not available; rebuild with the server feature".into()) +} + +#[cfg(feature = "tui")] +fn run_tui_applet(argv: Vec) -> Result<(), Box> { + 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) -> Result<(), Box> { + Err("TUI applet is not available; rebuild with the tui feature".into()) +} diff --git a/crates/irtt-cli/src/lib.rs b/crates/irtt-app/src/lib.rs similarity index 100% rename from crates/irtt-cli/src/lib.rs rename to crates/irtt-app/src/lib.rs diff --git a/crates/irtt-cli/src/shared/client/args.rs b/crates/irtt-app/src/shared/client/args.rs similarity index 100% rename from crates/irtt-cli/src/shared/client/args.rs rename to crates/irtt-app/src/shared/client/args.rs diff --git a/crates/irtt-cli/src/shared/client/config.rs b/crates/irtt-app/src/shared/client/config.rs similarity index 100% rename from crates/irtt-cli/src/shared/client/config.rs rename to crates/irtt-app/src/shared/client/config.rs diff --git a/crates/irtt-cli/src/shared/client/mod.rs b/crates/irtt-app/src/shared/client/mod.rs similarity index 100% rename from crates/irtt-cli/src/shared/client/mod.rs rename to crates/irtt-app/src/shared/client/mod.rs diff --git a/crates/irtt-cli/src/shared/client/prepare.rs b/crates/irtt-app/src/shared/client/prepare.rs similarity index 100% rename from crates/irtt-cli/src/shared/client/prepare.rs rename to crates/irtt-app/src/shared/client/prepare.rs diff --git a/crates/irtt-cli/src/shared/client/session.rs b/crates/irtt-app/src/shared/client/session.rs similarity index 100% rename from crates/irtt-cli/src/shared/client/session.rs rename to crates/irtt-app/src/shared/client/session.rs diff --git a/crates/irtt-cli/src/shared/client/targets.rs b/crates/irtt-app/src/shared/client/targets.rs similarity index 100% rename from crates/irtt-cli/src/shared/client/targets.rs rename to crates/irtt-app/src/shared/client/targets.rs diff --git a/crates/irtt-cli/src/shared/mod.rs b/crates/irtt-app/src/shared/mod.rs similarity index 100% rename from crates/irtt-cli/src/shared/mod.rs rename to crates/irtt-app/src/shared/mod.rs diff --git a/crates/irtt-cli/src/signal.rs b/crates/irtt-app/src/signal.rs similarity index 100% rename from crates/irtt-cli/src/signal.rs rename to crates/irtt-app/src/signal.rs diff --git a/crates/irtt-cli/tests/applet_dispatch.rs b/crates/irtt-app/tests/applet_dispatch.rs similarity index 66% rename from crates/irtt-cli/tests/applet_dispatch.rs rename to crates/irtt-app/tests/applet_dispatch.rs index 07d29fa..77539e7 100644 --- a/crates/irtt-cli/tests/applet_dispatch.rs +++ b/crates/irtt-app/tests/applet_dispatch.rs @@ -13,6 +13,31 @@ fn irtt_rs_with_arg0(arg0: &str) -> 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 { let mut text = String::new(); 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 ")); +} + +#[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 ")); +} + #[cfg(feature = "server")] #[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")) .arg("--help") .output() @@ -111,11 +164,37 @@ fn standalone_server_binary_exposes_the_same_arguments() { assert!(text.contains("--bind ")); } +#[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(feature = "client")] #[test] 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") .output() .unwrap(); @@ -130,7 +209,7 @@ fn client_applet_name_dispatches_to_client() { #[cfg(all(unix, not(feature = "client")))] #[test] 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") .output() .unwrap(); @@ -205,7 +284,17 @@ fn unknown_irtt_applet_name_errors() { assert!(!output.status.success()); 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-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}"); +} diff --git a/crates/irtt-cli/tests/client_multi_target.rs b/crates/irtt-app/tests/client_multi_target.rs similarity index 96% rename from crates/irtt-cli/tests/client_multi_target.rs rename to crates/irtt-app/tests/client_multi_target.rs index 23a21b6..2847a2f 100644 --- a/crates/irtt-cli/tests/client_multi_target.rs +++ b/crates/irtt-app/tests/client_multi_target.rs @@ -55,7 +55,7 @@ impl InterruptibleFakeServer { #[test] 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"]) .output() .unwrap(); @@ -67,7 +67,7 @@ fn old_target_option_is_rejected_as_an_unknown_argument() { #[test] 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") .output() .unwrap(); @@ -88,7 +88,7 @@ fn single_target_default_table_includes_target_and_accepts_pacing() { let server = InTreeServer::start(); 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([ "--duration", "30ms", @@ -125,7 +125,7 @@ fn single_target_default_table_includes_target_and_accepts_pacing() { fn single_labeled_target_default_table_includes_target() { 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([ "--duration", "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() { 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([ "--duration", "30ms", @@ -199,7 +199,7 @@ fn single_positional_target_default_csv_includes_target_as_positional_label() { let server = InTreeServer::start(); 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([ "--duration", "30ms", @@ -237,7 +237,7 @@ fn multi_target_default_table_includes_both_labels() { let a = 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([ "--duration", "40ms", @@ -287,7 +287,7 @@ fn multi_target_final_summary_preserves_cli_argument_order() { let a = 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([ "--duration", "40ms", @@ -331,7 +331,7 @@ fn mixed_labeled_and_unlabeled_positional_targets_both_appear_in_output() { let b = InTreeServer::start(); 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([ "--duration", "40ms", @@ -376,7 +376,7 @@ fn single_target_custom_target_column_renders_positional_label() { let server = InTreeServer::start(); 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([ "--duration", "30ms", @@ -414,7 +414,7 @@ fn single_target_custom_target_column_renders_positional_label() { fn custom_columns_remain_authoritative_and_omit_target() { 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([ "--duration", "30ms", @@ -448,7 +448,7 @@ fn custom_columns_remain_authoritative_and_omit_target() { fn columns_default_keyword_includes_target_for_single_target() { 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([ "--duration", "30ms", @@ -484,7 +484,7 @@ fn columns_default_keyword_includes_target_for_single_target() { fn single_labeled_target_default_jsonl_includes_target() { 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([ "--duration", "30ms", @@ -517,7 +517,7 @@ fn single_labeled_target_default_jsonl_includes_target() { #[test] fn continuous_single_target_peer_close_exits_nonzero() { 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([ "--duration", "0s", @@ -553,7 +553,7 @@ fn continuous_single_target_peer_close_exits_nonzero() { fn continuous_single_target_interruption_drains_final_events_and_succeeds() { let params = test_params(None, Duration::from_millis(10)); 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([ "--duration", "0s", @@ -611,7 +611,7 @@ fn multi_target_csv_emits_rows_for_both_labels() { let a = 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([ "--duration", "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 a = start_peer_close_server(params.clone()); 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([ "--duration", "40ms", @@ -716,7 +716,7 @@ fn continuous_all_peer_closed_targets_exit_nonzero() { .send(()) .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([ "--duration", "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 = 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 mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); + let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client")); command.args([ "--duration", "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 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 mut command = Command::new(env!("CARGO_BIN_EXE_irtt-cli")); + let mut command = Command::new(env!("CARGO_BIN_EXE_irtt-client")); command.args([ "--duration", "0s", @@ -872,7 +872,7 @@ fn explicit_group_interruption_succeeds_without_peer_close_error() { let params = test_params(None, Duration::from_millis(10)); let a = start_interruptible_echo_server(params.clone()); 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([ "--duration", "0s", @@ -932,7 +932,7 @@ fn all_open_failures_exit_nonzero_with_diagnostics() { let params = test_params(None, Duration::from_millis(10)); let a = start_open_failure_server(params.clone()); 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([ "--duration", "0s", diff --git a/crates/irtt-cli/tests/support/in_tree_server.rs b/crates/irtt-app/tests/support/in_tree_server.rs similarity index 100% rename from crates/irtt-cli/tests/support/in_tree_server.rs rename to crates/irtt-app/tests/support/in_tree_server.rs diff --git a/crates/irtt-cli/src/entry.rs b/crates/irtt-cli/src/entry.rs deleted file mode 100644 index c01fc95..0000000 --- a/crates/irtt-cli/src/entry.rs +++ /dev/null @@ -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> { - let argv: Vec = 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, - shutdown_requested: &AtomicBool, -) -> Result<(), Box> { - 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, - _shutdown_requested: &AtomicBool, -) -> Result<(), Box> { - Err("client applet is not available; rebuild with the client feature".into()) -} - -#[cfg(feature = "server")] -fn run_server_applet( - argv: Vec, - shutdown_requested: &AtomicBool, -) -> Result<(), Box> { - 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, - _shutdown_requested: &AtomicBool, -) -> Result<(), Box> { - Err("server applet is not available; rebuild with the server feature".into()) -} - -#[cfg(feature = "tui")] -fn run_tui_applet( - argv: Vec, - shutdown_requested: &AtomicBool, -) -> Result<(), Box> { - 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, - _shutdown_requested: &AtomicBool, -) -> Result<(), Box> { - Err("TUI applet is not available; rebuild with the tui feature".into()) -} diff --git a/dist-workspace.toml b/dist-workspace.toml index 23792f2..fc4f86b 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -11,7 +11,7 @@ ci = "github" installers = [] # 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"] -# 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 # provenance, but must not trigger cargo-dist or produce a GitHub Release. -tag-namespace = "irtt-cli" +tag-namespace = "irtt-rs" diff --git a/docs/INTEROP_COMPARISON.md b/docs/INTEROP_COMPARISON.md index 4247003..0b32213 100644 --- a/docs/INTEROP_COMPARISON.md +++ b/docs/INTEROP_COMPARISON.md @@ -13,7 +13,7 @@ upstream executable. Build `irtt-rs` and ensure an upstream `irtt` executable is available: ```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 ``` @@ -118,13 +118,13 @@ python3 scripts/run_netem_interop_ci.py \ On Linux: ```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 python3 scripts/run_netem_interop_ci.py \ --all \ --skip-build \ --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: diff --git a/renovate.json b/renovate.json index 7812930..56614f5 100644 --- a/renovate.json +++ b/renovate.json @@ -21,7 +21,7 @@ "labels": ["dependencies"], - "ignorePaths": [".github/workflows/irtt-cli-release.yml"], + "ignorePaths": [".github/workflows/irtt-rs-release.yml"], "lockFileMaintenance": { "enabled": true, diff --git a/scripts/compare_irtt_clients.py b/scripts/compare_irtt_clients.py index 540a801..1202c2d 100755 --- a/scripts/compare_irtt_clients.py +++ b/scripts/compare_irtt_clients.py @@ -32,7 +32,7 @@ DEFAULT_REMOTE_TARGETS = [ ] DEFAULT_LOCAL_TARGET = ("127.0.0.1", "10s", "100ms", "local") DEFAULT_OUTPUT_ROOT = Path("target/interop-comparisons") -DEFAULT_IRTT_RS_COMMAND = "target/debug/irtt-cli" +DEFAULT_IRTT_RS_COMMAND = "target/debug/irtt-client" @dataclass @@ -163,7 +163,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--skip-build", 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( "--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: - command = ["cargo", "build", "-p", "irtt-cli"] +def preflight_build_irtt_client(run_dir: Path) -> bool: + command = ["cargo", "build", "-p", "irtt-rs", "--bin", "irtt-client"] started = utc_now() print(f"[{started}] preflight build: {shlex.join(command)}", flush=True) completed = subprocess.run( @@ -1117,7 +1117,7 @@ def main() -> int: print(message, file=sys.stderr) 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) return 1 diff --git a/scripts/run_netem_interop_ci.py b/scripts/run_netem_interop_ci.py index 1775810..683fca6 100755 --- a/scripts/run_netem_interop_ci.py +++ b/scripts/run_netem_interop_ci.py @@ -105,13 +105,13 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument( "--irtt-rs-command", - default=str(REPO_ROOT / "target" / "debug" / "irtt-cli"), - help="Path or command for irtt-rs client (default: target/debug/irtt-cli).", + default=str(REPO_ROOT / "target" / "debug" / "irtt-client"), + help="Path or command for irtt-rs client (default: target/debug/irtt-client).", ) parser.add_argument( "--skip-build", 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( "--sudo", @@ -176,7 +176,10 @@ def main() -> int: upstream_irtt = resolve_command(args.upstream_irtt) irtt_rs_command = resolve_irtt_rs_command(args.irtt_rs_command) 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) setup_namespaces(args, run_dir)