feat(cli): add server applet #83

Merged
Lochnair merged 2 commits from feat/cli-server-applet into main 2026-08-13 12:49:54 +00:00
Lochnair commented 2026-08-13 12:04:00 +00:00 (Migrated from github.com)

Summary

Exposes the already-complete irtt_server::Server runtime as a real command-line applet. This is CLI orchestration only; no protocol, session or runtime behavior is added or changed.

  • Adds a server feature to irtt-cli, enabling optional irtt-server and tokio dependencies.
  • Makes server part of the default feature set, so a normal install now ships a working server.
  • Adds a standalone irtt-server binary target, gated on that feature.
  • Makes irtt-rs server ... and argv0 == irtt-server reach the same parser and runner; the dispatcher's help now reports the applet as available rather than unconditionally unavailable.
  • Maps CLI policy options directly onto ServerConfig builders, starting from ServerConfig::default() so library defaults are never restated in clap.
  • Runs one reusable irtt_server::Server on one current-thread Tokio runtime with one explicitly bound socket.
  • Reuses the existing process-wide shutdown flag installed by the shared signal handler; no second handler and no change to the client/TUI signal model.

CLI surface

irtt-server --bind 127.0.0.1:2112
irtt-rs server --bind 192.0.2.10:2112 \
    --max-sessions 512 \
    --idle-timeout 30s

Options: --bind, --hmac, --max-sessions, --max-packet-length, --min-interval, --burst, --idle-timeout, --max-duration.

  • One listener per process. Serving a second address or family means a second process.
  • The bind address is required and parsed as a SocketAddr; host names are not resolved and there is no implicit wildcard default.
  • Wildcard binds are accepted, but reply source-address selection is then left to the kernel: per-packet destination-address handling for multi-homed hosts remains future work, and the help text says so.
  • The actual bound endpoint is printed to stderr on startup, which also resolves --bind 127.0.0.1:0.
  • Ctrl-C returns from Server::run normally: no close packets, exit status 0.

Zero is preserved as meaningful operator policy where ServerConfig defines it (--min-interval 0, --idle-timeout 0, --burst 0, --max-sessions 0, --max-packet-length 0). --max-duration is positive-only; omitting it is how "no maximum" is expressed, so a zero maximum is rejected rather than silently treated as absence.

Feature model

  • default = ["client", "server", "stats"]; full = ["client", "server", "stats", "tui"].
  • The TUI stays optional.
  • The feature boundary is real: --no-default-features --features server builds the server applet without irtt-client, irtt-stats, ratatui or crossterm, and a bare --no-default-features build still produces the irtt-rs dispatcher, which reports the server applet as needing the server feature.

Binaries: --no-default-features → irtt-rs; + --features server → irtt-rs, irtt-server; default → irtt-rs, irtt-cli, irtt-server; --all-features → those plus irtt-tui.

Scope exclusions

No protocol or server-core behavior changes; irtt-proto, irtt-client and irtt-server production code are untouched. Not in this change: DSCP socket policy, the full ServerFill policy, pktinfo/source-address selection, daemonization, config files, a logging framework, hostname resolution, and multi-listener orchestration.

Verification

All of the following ran and passed on this branch:

  • cargo fmt --all -- --check
  • cargo check --workspace --all-targets --all-features --locked
  • cargo test --workspace --all-targets --all-features --locked (634 tests)
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
  • cargo check -p irtt-cli --no-default-features --all-targets --locked
  • cargo check -p irtt-cli --no-default-features --features server --all-targets --locked
  • cargo test -p irtt-cli --no-default-features --features server --locked
  • cargo test -p irtt-cli --locked and cargo test -p irtt-cli --all-features --locked
  • cargo tree for the server-only and bare-dispatcher builds, confirming irtt-server + tokio present and no client/stats/TUI dependencies leaking in

Manual smoke: irtt-server --bind 127.0.0.1:0 printed the resolved ephemeral endpoint and exited 0 on SIGINT; irtt-rs server --help and irtt-server --help produce byte-identical output.

Tests added: server argument parsing (required bind, IPv4/IPv6, policy mapping, zero-policy values, unit-less durations, zero --max-duration), a drift guard asserting that a bind-only invocation yields exactly ServerConfig::default(), feature-aware subcommand and argv0 dispatch tests, a standalone-binary help test, and one runtime smoke test that binds 127.0.0.1:0 with shutdown already requested and asserts run_server returns Ok.

One incidental fix: the new bin shares its name with the irtt-server library crate, which made cargo doc --workspace emit an output-filename-collision warning, so the bin target sets doc = false.

## Summary Exposes the already-complete `irtt_server::Server` runtime as a real command-line applet. This is CLI orchestration only; no protocol, session or runtime behavior is added or changed. - Adds a `server` feature to `irtt-cli`, enabling optional `irtt-server` and `tokio` dependencies. - Makes `server` part of the default feature set, so a normal install now ships a working server. - Adds a standalone `irtt-server` binary target, gated on that feature. - Makes `irtt-rs server ...` and `argv0 == irtt-server` reach the same parser and runner; the dispatcher's help now reports the applet as available rather than unconditionally unavailable. - Maps CLI policy options directly onto `ServerConfig` builders, starting from `ServerConfig::default()` so library defaults are never restated in clap. - Runs one reusable `irtt_server::Server` on one current-thread Tokio runtime with one explicitly bound socket. - Reuses the existing process-wide shutdown flag installed by the shared signal handler; no second handler and no change to the client/TUI signal model. ## CLI surface ```sh irtt-server --bind 127.0.0.1:2112 ``` ```sh irtt-rs server --bind 192.0.2.10:2112 \ --max-sessions 512 \ --idle-timeout 30s ``` Options: `--bind`, `--hmac`, `--max-sessions`, `--max-packet-length`, `--min-interval`, `--burst`, `--idle-timeout`, `--max-duration`. - One listener per process. Serving a second address or family means a second process. - The bind address is required and parsed as a `SocketAddr`; host names are not resolved and there is no implicit wildcard default. - Wildcard binds are accepted, but reply source-address selection is then left to the kernel: per-packet destination-address handling for multi-homed hosts remains future work, and the help text says so. - The actual bound endpoint is printed to stderr on startup, which also resolves `--bind 127.0.0.1:0`. - `Ctrl-C` returns from `Server::run` normally: no close packets, exit status 0. Zero is preserved as meaningful operator policy where `ServerConfig` defines it (`--min-interval 0`, `--idle-timeout 0`, `--burst 0`, `--max-sessions 0`, `--max-packet-length 0`). `--max-duration` is positive-only; omitting it is how "no maximum" is expressed, so a zero maximum is rejected rather than silently treated as absence. ## Feature model - `default = ["client", "server", "stats"]`; `full = ["client", "server", "stats", "tui"]`. - The TUI stays optional. - The feature boundary is real: `--no-default-features --features server` builds the server applet without `irtt-client`, `irtt-stats`, `ratatui` or `crossterm`, and a bare `--no-default-features` build still produces the `irtt-rs` dispatcher, which reports the server applet as needing the `server` feature. Binaries: `--no-default-features` → `irtt-rs`; `+ --features server` → `irtt-rs`, `irtt-server`; default → `irtt-rs`, `irtt-cli`, `irtt-server`; `--all-features` → those plus `irtt-tui`. ## Scope exclusions No protocol or server-core behavior changes; `irtt-proto`, `irtt-client` and `irtt-server` production code are untouched. Not in this change: DSCP socket policy, the full `ServerFill` policy, pktinfo/source-address selection, daemonization, config files, a logging framework, hostname resolution, and multi-listener orchestration. ## Verification All of the following ran and passed on this branch: - `cargo fmt --all -- --check` - `cargo check --workspace --all-targets --all-features --locked` - `cargo test --workspace --all-targets --all-features --locked` (634 tests) - `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` - `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps` - `cargo check -p irtt-cli --no-default-features --all-targets --locked` - `cargo check -p irtt-cli --no-default-features --features server --all-targets --locked` - `cargo test -p irtt-cli --no-default-features --features server --locked` - `cargo test -p irtt-cli --locked` and `cargo test -p irtt-cli --all-features --locked` - `cargo tree` for the server-only and bare-dispatcher builds, confirming `irtt-server` + `tokio` present and no client/stats/TUI dependencies leaking in Manual smoke: `irtt-server --bind 127.0.0.1:0` printed the resolved ephemeral endpoint and exited 0 on `SIGINT`; `irtt-rs server --help` and `irtt-server --help` produce byte-identical output. Tests added: server argument parsing (required bind, IPv4/IPv6, policy mapping, zero-policy values, unit-less durations, zero `--max-duration`), a drift guard asserting that a bind-only invocation yields exactly `ServerConfig::default()`, feature-aware subcommand and `argv0` dispatch tests, a standalone-binary help test, and one runtime smoke test that binds `127.0.0.1:0` with shutdown already requested and asserts `run_server` returns `Ok`. One incidental fix: the new bin shares its name with the `irtt-server` library crate, which made `cargo doc --workspace` emit an output-filename-collision warning, so the bin target sets `doc = false`.
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-13 12:07:34 +00:00
chatgpt-codex-connector[bot] (Migrated from github.com) left a comment

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a672f78e86

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

### 💡 Codex Review Here are some automated review suggestions for this pull request. **Reviewed commit:** `a672f78e86` <details> <summary>ℹ️ About Codex in GitHub</summary> <br/> [Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you - Open a pull request for review - Mark a draft as ready - Comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". </details>
@@ -0,0 +1,5 @@
use std::process::ExitCode;
fn main() -> ExitCode {
irtt_cli::entry::main()
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-13 12:07:35 +00:00

P1 Badge Recognize the .exe suffix before dispatching the server

On Windows, this new binary reaches the shared dispatcher with an argv[0] basename such as irtt-server.exe, but detect_applet_from_argv0 recognizes only irtt-server; the subsequent starts_with("irtt-") branch therefore rejects every invocation as an unknown applet before Clap or the server runner can execute. Strip the platform executable suffix or recognize the .exe applet names so the advertised server applet works on Windows.

AGENTS.md reference: AGENTS.md:L68-L68

Useful? React with 👍 / 👎.

**<sub><sub>![P1 Badge](https://img.shields.io/badge/P1-orange?style=flat)</sub></sub> Recognize the .exe suffix before dispatching the server** On Windows, this new binary reaches the shared dispatcher with an `argv[0]` basename such as `irtt-server.exe`, but `detect_applet_from_argv0` recognizes only `irtt-server`; the subsequent `starts_with("irtt-")` branch therefore rejects every invocation as an unknown applet before Clap or the server runner can execute. Strip the platform executable suffix or recognize the `.exe` applet names so the advertised server applet works on Windows. AGENTS.md reference: [AGENTS.md:L68-L68](https://github.com/Lochnair/irtt-rs/blob/a672f78e861770e339bdff16082986078cb82508/AGENTS.md#L68-L68) Useful? React with 👍 / 👎.
Lochnair (Migrated from github.com) reviewed 2026-08-13 12:17:39 +00:00
@@ -0,0 +1,5 @@
use std::process::ExitCode;
fn main() -> ExitCode {
irtt_cli::entry::main()
Lochnair (Migrated from github.com) commented 2026-08-13 12:17:39 +00:00

Fixed in 565d7fb: applet_basename now strips the platform EXE_SUFFIX (case-insensitively, no-op on Unix), so irtt-server.exe dispatches correctly — the same bug also broke irtt-cli.exe, irtt-tui.exe and even the irtt-rs.exe dispatcher itself, which all fell into the unknown-applet branch, and Windows is a shipped target per dist-workspace.toml.

🤖 Addressed by Claude Code

Fixed in 565d7fb: `applet_basename` now strips the platform `EXE_SUFFIX` (case-insensitively, no-op on Unix), so `irtt-server.exe` dispatches correctly — the same bug also broke `irtt-cli.exe`, `irtt-tui.exe` and even the `irtt-rs.exe` dispatcher itself, which all fell into the unknown-applet branch, and Windows is a shipped target per `dist-workspace.toml`. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Sign in to join this conversation.