Problem / Background
The ping subcommand's help text promises exit codes that the implementation never produces. bssh ping always exits 0, even when every target host is unreachable. Any script or CI job that branches on bssh ping succeeding is silently broken: the check passes unconditionally.
Documented contract (src/cli/bssh.rs, lines 341 to 345):
#[command(
about = "Test connectivity to hosts",
long_about = "Verifies SSH connectivity and authentication to all target hosts.\nReports connection status, authentication success, and response times.\nUseful for validating cluster configuration and SSH key setup.\n\nExit codes: 0 (all reachable), 1 (any unreachable)"
)]
Ping,
Actual behavior, traced across three hops:
src/commands/ping.rs, ping_nodes(...) counts success_count and failed_count, prints them via OutputFormatter::format_summary(nodes.len(), success_count, failed_count), and then returns Ok(()) unconditionally. The counts are used for display only and are discarded.
src/app/dispatcher.rs, the Some(Commands::Ping) arm (around line 208) returns that Result straight through with no exit code translation.
src/main.rs, run_bssh_mode returns dispatch_command(&cli, &ctx).await directly. std::process::exit(1) appears in main.rs only on unrelated early error paths (pdsh query mode with no hosts, no command specified, and similar). Nothing converts a partially or fully failed ping into a nonzero status.
Reproduction:
bssh -H unreachable-host ping; echo $? # prints 0
Current vs Expected Behavior
| Scenario |
Current |
Expected (per current help text) |
| All hosts reachable |
0 |
0 |
| Some hosts unreachable |
0 |
1 |
| No host reachable |
0 |
1 (or 255, see proposal below) |
| bssh failed before attempting any connection |
varies by error path |
255 (proposed) |
Proposed Exit Code Mapping (needs sign off)
This is a proposal, not a settled decision. Two readings are on the table.
Existing in-repo precedent. No new scheme should be invented. src/executor/exit_strategy.rs already defines ExitCodeStrategy with three variants (MainRank, RequireAllSuccess, MainRankWithFailureCheck) and calculate(&self, results: &[ExecutionResult], main_idx: Option<usize>) -> i32. The rationale is documented in docs/architecture/exit-code-strategy.md: the default follows standard MPI tools (mpirun, srun, mpiexec) by returning the main rank's exit code, and --require-all-success returns 0 only if all nodes succeeded and 1 if any node failed. Ping is a health check, so RequireAllSuccess is the semantically correct alignment; MainRank makes no sense here because ping runs no user command whose status could be forwarded.
OpenSSH semantics. ssh exits with the remote command's exit status, and reserves 255 for "ssh itself encountered an error" (connection failure, authentication failure, bad usage). This matters for ping specifically: ping has no remote command whose status could be forwarded, so every ping failure is by definition an ssh-level failure.
Option A, strict reading of the current text (0/1 only):
0: every targeted host connected and authenticated successfully.
1: any host failed, including the case where none succeeded.
This matches RequireAllSuccess exactly and changes no documented behavior.
Option B, OpenSSH-aligned refinement (0/1/255), recommended:
0: every targeted host connected and authenticated successfully.
1: at least one host was reachable but at least one failed. This preserves the currently documented contract for the partial-failure case and matches RequireAllSuccess.
255: no host succeeded, or bssh failed before it could attempt any connection (config load failure, no hosts resolved, bad arguments). This matches OpenSSH's "ssh itself failed" convention and lets callers distinguish "the cluster is partially degraded" from "I could not reach anything / I never got off the ground".
The tension, stated explicitly so a reviewer can decide: the current help text says 1 (any unreachable), which under a strict reading makes the all-unreachable case exit 1, not 255. Adopting 255 for total failure is a deliberate, OpenSSH-compatible refinement that changes documented behavior. Recommendation is Option B, with the caveat that it is a user-visible behavior change and warrants a changelog note. If Option A is chosen instead, the fix reduces to propagating the failure count and the help text needs no change beyond the response-times correction below.
Whichever option is chosen, the implementation must state which ExitCodeStrategy it aligns with and stay consistent with docs/architecture/exit-code-strategy.md.
Secondary Issue: "response times" is also inaccurate
The same long_about claims ping "Reports connection status, authentication success, and response times." The implementation in src/commands/ping.rs prints only Connected or Failed per node, plus the formatted error chain on failure. It prints no timing at all.
Two ways to resolve, and the choice should be made explicitly:
- Add per-host response time measurement to the ping output.
- Correct the help text to drop the "response times" claim.
Recommendation is option 2 (correct the text) unless timing is actually wanted, since it is the smaller change and removes a false claim immediately. If option 1 is chosen, it should be split into a separate issue so it does not block the exit code fix.
Acceptance Criteria
Technical Considerations
- The
Result<()> return type of ping_nodes cannot express "ran fine, but N hosts failed". Either change the signature to return the counts (or a computed exit code) and have the dispatcher translate it, or have ping_nodes call the exit path itself. Returning the counts and translating in one place is preferable to scattering std::process::exit calls, and matches how the dispatcher already owns command-level outcomes.
- Care is needed so that anyhow error propagation (a genuine
Err from executor.execute(...)) and the "connected but some hosts failed" case do not collapse into the same exit code if Option B is chosen. A hard Err before any connection attempt should map to 255, not 1.
- Exit codes are limited to 0 to 255 (POSIX), which
docs/architecture/exit-code-strategy.md already notes.
Related
Found while refreshing #75 (Add network-ping command for fast ICMP connectivity testing). That proposed network-ping command would inherit whatever exit code contract is settled here, so this issue should be resolved first, or at minimum the contract agreed on, before #75 is implemented. Otherwise the two ping-family commands will ship with divergent exit semantics.
Problem / Background
The
pingsubcommand's help text promises exit codes that the implementation never produces.bssh pingalways exits 0, even when every target host is unreachable. Any script or CI job that branches onbssh pingsucceeding is silently broken: the check passes unconditionally.Documented contract (
src/cli/bssh.rs, lines 341 to 345):Actual behavior, traced across three hops:
src/commands/ping.rs,ping_nodes(...)countssuccess_countandfailed_count, prints them viaOutputFormatter::format_summary(nodes.len(), success_count, failed_count), and then returnsOk(())unconditionally. The counts are used for display only and are discarded.src/app/dispatcher.rs, theSome(Commands::Ping)arm (around line 208) returns thatResultstraight through with no exit code translation.src/main.rs,run_bssh_modereturnsdispatch_command(&cli, &ctx).awaitdirectly.std::process::exit(1)appears inmain.rsonly on unrelated early error paths (pdsh query mode with no hosts, no command specified, and similar). Nothing converts a partially or fully failed ping into a nonzero status.Reproduction:
Current vs Expected Behavior
Proposed Exit Code Mapping (needs sign off)
This is a proposal, not a settled decision. Two readings are on the table.
Existing in-repo precedent. No new scheme should be invented.
src/executor/exit_strategy.rsalready definesExitCodeStrategywith three variants (MainRank,RequireAllSuccess,MainRankWithFailureCheck) andcalculate(&self, results: &[ExecutionResult], main_idx: Option<usize>) -> i32. The rationale is documented indocs/architecture/exit-code-strategy.md: the default follows standard MPI tools (mpirun, srun, mpiexec) by returning the main rank's exit code, and--require-all-successreturns 0 only if all nodes succeeded and 1 if any node failed. Ping is a health check, soRequireAllSuccessis the semantically correct alignment;MainRankmakes no sense here because ping runs no user command whose status could be forwarded.OpenSSH semantics.
sshexits with the remote command's exit status, and reserves 255 for "ssh itself encountered an error" (connection failure, authentication failure, bad usage). This matters for ping specifically: ping has no remote command whose status could be forwarded, so every ping failure is by definition an ssh-level failure.Option A, strict reading of the current text (0/1 only):
0: every targeted host connected and authenticated successfully.1: any host failed, including the case where none succeeded.This matches
RequireAllSuccessexactly and changes no documented behavior.Option B, OpenSSH-aligned refinement (0/1/255), recommended:
0: every targeted host connected and authenticated successfully.1: at least one host was reachable but at least one failed. This preserves the currently documented contract for the partial-failure case and matchesRequireAllSuccess.255: no host succeeded, or bssh failed before it could attempt any connection (config load failure, no hosts resolved, bad arguments). This matches OpenSSH's "ssh itself failed" convention and lets callers distinguish "the cluster is partially degraded" from "I could not reach anything / I never got off the ground".The tension, stated explicitly so a reviewer can decide: the current help text says
1 (any unreachable), which under a strict reading makes the all-unreachable case exit 1, not 255. Adopting 255 for total failure is a deliberate, OpenSSH-compatible refinement that changes documented behavior. Recommendation is Option B, with the caveat that it is a user-visible behavior change and warrants a changelog note. If Option A is chosen instead, the fix reduces to propagating the failure count and the help text needs no change beyond the response-times correction below.Whichever option is chosen, the implementation must state which
ExitCodeStrategyit aligns with and stay consistent withdocs/architecture/exit-code-strategy.md.Secondary Issue: "response times" is also inaccurate
The same
long_aboutclaims ping "Reports connection status, authentication success, and response times." The implementation insrc/commands/ping.rsprints onlyConnectedorFailedper node, plus the formatted error chain on failure. It prints no timing at all.Two ways to resolve, and the choice should be made explicitly:
Recommendation is option 2 (correct the text) unless timing is actually wanted, since it is the smaller change and removes a false claim immediately. If option 1 is chosen, it should be split into a separate issue so it does not block the exit code fix.
Acceptance Criteria
ping_nodes(or theSome(Commands::Ping)dispatcher arm) propagates the failure count to the process exit status. Real integration into the code flow is required: a helper that computes a code but is never wired to the process exit does not satisfy this.ExitCodeStrategyvariant it aligns with.tests/exit_code_integration_test.rsis the existing precedent for this style of test.long_abouttext insrc/cli/bssh.rs(lines 341 to 345) is updated so both the exit code list and the "response times" claim match the implementation.docs/man/bssh.1is checked and updated. Note: the.SH COMMANDSentry forping(line 362) currently says only "Test connectivity to hosts" and does not repeat the exit code claim, and the.SH EXIT STATUSsection documents only the generalMainRank/--require-all-successstrategies with no ping-specific case. Whichever option is chosen,.SH EXIT STATUSshould gain the ping contract so the man page and--helpagree.docs/architecture/exit-code-strategy.mdis updated if the ping contract introduces 255, since that value is not currently described there.bssh -H unreachable-host ping; echo $?returns nonzero.Technical Considerations
Result<()>return type ofping_nodescannot express "ran fine, but N hosts failed". Either change the signature to return the counts (or a computed exit code) and have the dispatcher translate it, or haveping_nodescall the exit path itself. Returning the counts and translating in one place is preferable to scatteringstd::process::exitcalls, and matches how the dispatcher already owns command-level outcomes.Errfromexecutor.execute(...)) and the "connected but some hosts failed" case do not collapse into the same exit code if Option B is chosen. A hardErrbefore any connection attempt should map to 255, not 1.docs/architecture/exit-code-strategy.mdalready notes.Related
Found while refreshing #75 (
Add network-ping command for fast ICMP connectivity testing). That proposednetwork-pingcommand would inherit whatever exit code contract is settled here, so this issue should be resolved first, or at minimum the contract agreed on, before #75 is implemented. Otherwise the two ping-family commands will ship with divergent exit semantics.