diff --git a/README.md b/README.md index 2a1f172..71ebaef 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ 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 restart HOST # restart dosh-server and show 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 c3ad8b5..d0678ee 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -216,6 +216,9 @@ async fn main() -> Result<()> { if matches!(args.server.as_deref(), Some("status" | "sessions")) { return run_status_command(&config, &args); } + if args.server.as_deref() == Some("restart") { + return run_restart_command(&config, &args); + } if args.server.as_deref() == Some("forward") { args = rewrite_forward_command(args)?; } @@ -751,6 +754,29 @@ fn run_status_command(config: &dosh::config::ClientConfig, args: &Args) -> Resul ) } +fn run_restart_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> { + if args.command.len() != 1 { + return Err(anyhow!("usage: dosh restart ")); + } + 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!("Restarting Dosh server for {requested}"); + if is_local_status_target(&server) { + return run_local_script(RESTART_STATUS_SCRIPT, "restart local Dosh server"); + } + run_remote_script( + &server, + ssh_port, + args.ssh_key.as_deref(), + args.ssh_known_hosts.as_deref(), + RESTART_STATUS_SCRIPT, + "restart remote Dosh server", + ) +} + fn status_ssh_target(requested: &str, host_config: &dosh::config::HostConfig) -> String { let raw_server = host_config .ssh @@ -766,14 +792,20 @@ fn is_local_status_target(server: &str) -> bool { 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'"#; +const RESTART_STATUS_SCRIPT: &str = r#"if command -v systemctl >/dev/null 2>&1 && systemctl --user list-unit-files dosh-server.service >/dev/null 2>&1; then systemctl --user restart dosh-server.service; else pkill -f 'dosh-server serve' 2>/dev/null || true; nohup "$HOME/.local/bin/dosh-server" serve >/tmp/dosh-server.log 2>&1 & fi; sleep 1; 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<()> { + run_local_script(SESSIONS_STATUS_SCRIPT, "run local status command") +} + +fn run_local_script(script: &str, context: &str) -> Result<()> { let status = Command::new("sh") .arg("-c") - .arg(SESSIONS_STATUS_SCRIPT) + .arg(script) .status() - .context("run local status command")?; + .with_context(|| context.to_string())?; if !status.success() { - return Err(anyhow!("local status command failed with status {status}")); + return Err(anyhow!("{context} failed with status {status}")); } Ok(()) } @@ -1365,6 +1397,24 @@ fn run_sessions_command( ssh_port: Option, ssh_key: Option<&std::path::Path>, ssh_known_hosts: Option<&std::path::Path>, +) -> Result<()> { + run_remote_script( + server, + ssh_port, + ssh_key, + ssh_known_hosts, + SESSIONS_STATUS_SCRIPT, + "run remote status command", + ) +} + +fn run_remote_script( + server: &str, + ssh_port: Option, + ssh_key: Option<&std::path::Path>, + ssh_known_hosts: Option<&std::path::Path>, + script: &str, + context: &str, ) -> Result<()> { let mut command = Command::new("ssh"); if let Some(ssh_port) = ssh_port { @@ -1379,14 +1429,14 @@ fn run_sessions_command( .arg("-o") .arg(format!("UserKnownHostsFile={}", known_hosts.display())); } - let remote_command = format!("sh -c {}", shell_word(SESSIONS_STATUS_SCRIPT)); + let remote_command = format!("sh -c {}", shell_word(script)); let status = command .arg(server) .arg(remote_command) .status() - .context("run remote sessions command")?; + .with_context(|| context.to_string())?; if !status.success() { - return Err(anyhow!("sessions command failed with status {status}")); + return Err(anyhow!("{context} failed with status {status}")); } Ok(()) } @@ -5044,15 +5094,16 @@ const TERMINAL_CLEANUP: &[u8] = concat!( mod tests { use super::{ 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, 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, + PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, SshConfig, StatusAction, + auth_allows, cache_key, cache_server_prefix, clear_cached_credentials, + ensure_tui_safe_status_overlay, 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; @@ -5734,6 +5785,13 @@ mod tests { assert!(!is_local_status_target("server.example.com")); } + #[test] + fn restart_script_has_systemd_and_nohup_fallback() { + assert!(RESTART_STATUS_SCRIPT.contains("systemctl --user restart dosh-server.service")); + assert!(RESTART_STATUS_SCRIPT.contains("nohup")); + assert!(RESTART_STATUS_SCRIPT.contains("dosh-server")); + } + #[test] fn release_download_url_uses_latest_release_asset() { assert_eq!(