diff --git a/README.md b/README.md index 4b392de..2a1f172 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ dosh setup HOST # import SSH config and trust the Dosh host key dosh HOST # connect dosh HOST COMMAND # connect and run a command dosh update # update Dosh +dosh status HOST # show remote tmux sessions and Dosh service status dosh doctor HOST # check config and connectivity dosh recover HOST # clear cached attach state and re-check ``` diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index cd422ce..c3ad8b5 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -213,6 +213,9 @@ async fn main() -> Result<()> { if matches!(args.server.as_deref(), Some("recover" | "repair")) { return run_recover_command(&config, &args).await; } + if matches!(args.server.as_deref(), Some("status" | "sessions")) { + return run_status_command(&config, &args); + } if args.server.as_deref() == Some("forward") { args = rewrite_forward_command(args)?; } @@ -727,6 +730,54 @@ async fn run_recover_command(config: &dosh::config::ClientConfig, args: &Args) - run_doctor_for_host(config, args, &requested).await } +fn run_status_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> { + if args.command.len() != 1 { + return Err(anyhow!("usage: dosh status ")); + } + let requested = args.command[0].clone(); + let hosts = load_hosts_config(None).unwrap_or_default(); + let host_config = hosts.hosts.get(&requested).cloned().unwrap_or_default(); + let server = status_ssh_target(&requested, &host_config); + let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port); + println!("Dosh status for {requested}"); + if is_local_status_target(&server) { + return run_local_sessions_command(); + } + run_sessions_command( + &server, + ssh_port, + args.ssh_key.as_deref(), + args.ssh_known_hosts.as_deref(), + ) +} + +fn status_ssh_target(requested: &str, host_config: &dosh::config::HostConfig) -> String { + let raw_server = host_config + .ssh + .clone() + .unwrap_or_else(|| requested.to_string()); + ssh_with_user(&raw_server, host_config.user.as_deref()) +} + +fn is_local_status_target(server: &str) -> bool { + let host = server.rsplit('@').next().unwrap_or(server); + matches!(host, "localhost" | "127.0.0.1" | "::1") +} + +const SESSIONS_STATUS_SCRIPT: &str = r#"printf 'tmux sessions:\n'; tmux ls 2>/dev/null || printf ' none\n'; printf '\nDosh service:\n'; systemctl --user --no-pager --plain status dosh-server.service 2>/dev/null | sed -n '1,8p' || pgrep -af dosh-server || printf ' not running\n'"#; + +fn run_local_sessions_command() -> Result<()> { + let status = Command::new("sh") + .arg("-c") + .arg(SESSIONS_STATUS_SCRIPT) + .status() + .context("run local status command")?; + if !status.success() { + return Err(anyhow!("local status command failed with status {status}")); + } + Ok(()) +} + async fn run_doctor_for_host( config: &dosh::config::ClientConfig, args: &Args, @@ -1328,8 +1379,7 @@ fn run_sessions_command( .arg("-o") .arg(format!("UserKnownHostsFile={}", known_hosts.display())); } - let script = r#"printf 'tmux sessions:\n'; tmux ls 2>/dev/null || printf ' none\n'; printf '\nDosh service:\n'; systemctl --user --no-pager --plain status dosh-server.service 2>/dev/null | sed -n '1,8p' || pgrep -af dosh-server || printf ' not running\n'"#; - let remote_command = format!("sh -c {}", shell_word(script)); + let remote_command = format!("sh -c {}", shell_word(SESSIONS_STATUS_SCRIPT)); let status = command .arg(server) .arg(remote_command) @@ -4996,13 +5046,13 @@ mod tests { DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, MAX_PENDING_USER_INPUT_BYTES, PredictMode, Predictor, RemoteForward, SshConfig, StatusAction, auth_allows, cache_key, cache_server_prefix, clear_cached_credentials, ensure_tui_safe_status_overlay, - input_matches_escape, latest_release_download_url, load_first_native_identity_with_prompt, - parse_dynamic_forward, parse_escape_key, parse_local_forward, parse_remote_forward, - parse_ssh_config, queue_pending_user_input, raw_contains_host_table, recv_response_until, - render_status_clear, render_status_overlay, requested_env, resolved_startup_command, - rewrite_forward_command, selected_predict_mode, selected_udp_host, ssh_destination_host, - ssh_username, ssh_with_user, startup_command, toml_bare_key_or_quoted, - update_check_requested, valid_forward_host, + input_matches_escape, is_local_status_target, latest_release_download_url, + load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key, + parse_local_forward, parse_remote_forward, parse_ssh_config, queue_pending_user_input, + raw_contains_host_table, recv_response_until, render_status_clear, render_status_overlay, + requested_env, resolved_startup_command, rewrite_forward_command, selected_predict_mode, + selected_udp_host, ssh_destination_host, ssh_username, ssh_with_user, startup_command, + status_ssh_target, toml_bare_key_or_quoted, update_check_requested, valid_forward_host, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -5659,6 +5709,31 @@ mod tests { assert!(update_check_requested(&["--check".to_string(), "extra".to_string()]).is_err()); } + #[test] + fn status_uses_host_alias_and_user() { + let host = HostConfig { + ssh: Some("server.example.com".to_string()), + user: Some("palav".to_string()), + ..HostConfig::default() + }; + assert_eq!( + status_ssh_target("palav", &host), + "palav@server.example.com" + ); + assert_eq!( + status_ssh_target("rawhost", &HostConfig::default()), + "rawhost" + ); + } + + #[test] + fn status_detects_local_targets() { + assert!(is_local_status_target("localhost")); + assert!(is_local_status_target("palav@127.0.0.1")); + assert!(is_local_status_target("::1")); + assert!(!is_local_status_target("server.example.com")); + } + #[test] fn release_download_url_uses_latest_release_asset() { assert_eq!(