Add public comparison benchmarks and SSH import
This commit is contained in:
+130
-21
@@ -18,6 +18,7 @@ use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use tokio::net::UdpSocket;
|
||||
@@ -49,11 +50,11 @@ struct Args {
|
||||
#[arg(long)]
|
||||
ssh_auth_command: Option<String>,
|
||||
#[arg(long)]
|
||||
ssh_key: Option<std::path::PathBuf>,
|
||||
ssh_key: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
ssh_known_hosts: Option<std::path::PathBuf>,
|
||||
ssh_known_hosts: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
ssh_control_path: Option<std::path::PathBuf>,
|
||||
ssh_control_path: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
dosh_port: Option<u16>,
|
||||
#[arg(long)]
|
||||
@@ -84,6 +85,9 @@ async fn main() -> Result<()> {
|
||||
if args.server.as_deref() == Some("update") {
|
||||
return run_update(&config);
|
||||
}
|
||||
if args.server.as_deref() == Some("import-ssh") {
|
||||
return run_import_ssh(&config, &args.command);
|
||||
}
|
||||
let requested_server = args.server.unwrap_or_else(|| config.server.clone());
|
||||
let hosts = load_hosts_config(None).unwrap_or_default();
|
||||
let host = hosts
|
||||
@@ -105,7 +109,7 @@ async fn main() -> Result<()> {
|
||||
"read-write"
|
||||
}
|
||||
.to_string();
|
||||
let ssh_port = args.ssh_port.or(config.ssh_port);
|
||||
let ssh_port = args.ssh_port.or(host.ssh_port).or(config.ssh_port);
|
||||
let ssh_auth_command = args
|
||||
.ssh_auth_command
|
||||
.clone()
|
||||
@@ -356,6 +360,77 @@ fn run_update(config: &dosh::config::ClientConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_import_ssh(config: &dosh::config::ClientConfig, aliases: &[String]) -> Result<()> {
|
||||
if aliases.is_empty() {
|
||||
return Err(anyhow!("usage: dosh import-ssh <ssh-host> [ssh-host...]"));
|
||||
}
|
||||
let path = expand_tilde("~/.config/dosh/hosts.toml");
|
||||
let mut raw = if path.exists() {
|
||||
fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if !raw.ends_with('\n') && !raw.is_empty() {
|
||||
raw.push('\n');
|
||||
}
|
||||
for alias in aliases {
|
||||
let ssh_config = ssh_config(alias, None)
|
||||
.with_context(|| format!("read SSH config for {alias} with ssh -G"))?;
|
||||
let udp_host = ssh_config
|
||||
.hostname
|
||||
.clone()
|
||||
.unwrap_or_else(|| ssh_destination_host(alias));
|
||||
if raw_contains_host_table(&raw, alias) {
|
||||
eprintln!("dosh import-ssh: [{alias}] already exists, skipping");
|
||||
continue;
|
||||
}
|
||||
raw.push('\n');
|
||||
raw.push_str(&format!("[{}]\n", toml_bare_key_or_quoted(alias)));
|
||||
raw.push_str(&format!("ssh = {}\n", toml_string(alias)));
|
||||
raw.push_str(&format!("dosh_host = {}\n", toml_string(&udp_host)));
|
||||
raw.push_str(&format!("port = {}\n", config.dosh_port));
|
||||
if let Some(port) = ssh_config.port
|
||||
&& port != 22
|
||||
{
|
||||
raw.push_str(&format!("ssh_port = {port}\n"));
|
||||
}
|
||||
raw.push_str("predict = false\n");
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(&path, raw).with_context(|| format!("write {}", path.display()))?;
|
||||
eprintln!("dosh import-ssh: wrote {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn raw_contains_host_table(raw: &str, alias: &str) -> bool {
|
||||
let quoted = toml_string(alias);
|
||||
raw.lines().any(|line| {
|
||||
let line = line.trim();
|
||||
line == format!("[{alias}]") || line == format!("[{quoted}]")
|
||||
})
|
||||
}
|
||||
|
||||
fn toml_string(value: &str) -> String {
|
||||
let escaped = value
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('\n', "\\n");
|
||||
format!("\"{escaped}\"")
|
||||
}
|
||||
|
||||
fn toml_bare_key_or_quoted(value: &str) -> String {
|
||||
if value
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
value.to_string()
|
||||
} else {
|
||||
toml_string(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_base_from_repo(repo: &str) -> String {
|
||||
repo.trim_end_matches(".git").to_string()
|
||||
}
|
||||
@@ -420,9 +495,9 @@ fn ssh_bootstrap(
|
||||
server: &str,
|
||||
ssh_port: Option<u16>,
|
||||
ssh_auth_command: &str,
|
||||
ssh_key: Option<&std::path::Path>,
|
||||
ssh_known_hosts: Option<&std::path::Path>,
|
||||
ssh_control_path: Option<&std::path::Path>,
|
||||
ssh_key: Option<&Path>,
|
||||
ssh_known_hosts: Option<&Path>,
|
||||
ssh_control_path: Option<&Path>,
|
||||
session: &str,
|
||||
mode: &str,
|
||||
cols: u16,
|
||||
@@ -488,6 +563,18 @@ fn ssh_destination_host(server: &str) -> String {
|
||||
}
|
||||
|
||||
fn ssh_config_hostname(server: &str, ssh_port: Option<u16>) -> Result<String> {
|
||||
ssh_config(server, ssh_port)?
|
||||
.hostname
|
||||
.ok_or_else(|| anyhow!("ssh -G did not print hostname"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct SshConfig {
|
||||
hostname: Option<String>,
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
fn ssh_config(server: &str, ssh_port: Option<u16>) -> Result<SshConfig> {
|
||||
let mut command = Command::new("ssh");
|
||||
command.arg("-G");
|
||||
if let Some(ssh_port) = ssh_port {
|
||||
@@ -504,21 +591,24 @@ fn ssh_config_hostname(server: &str, ssh_port: Option<u16>) -> Result<String> {
|
||||
));
|
||||
}
|
||||
let raw = String::from_utf8(output.stdout)?;
|
||||
parse_ssh_config_hostname(&raw).ok_or_else(|| anyhow!("ssh -G did not print hostname"))
|
||||
Ok(parse_ssh_config(&raw))
|
||||
}
|
||||
|
||||
fn parse_ssh_config_hostname(raw: &str) -> Option<String> {
|
||||
raw.lines().find_map(|line| {
|
||||
fn parse_ssh_config(raw: &str) -> SshConfig {
|
||||
let mut config = SshConfig::default();
|
||||
for line in raw.lines() {
|
||||
let line = line.trim();
|
||||
let (key, value) = line.split_once(char::is_whitespace)?;
|
||||
if key.eq_ignore_ascii_case("hostname") {
|
||||
let value = value.trim();
|
||||
if !value.is_empty() {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
let Some((key, value)) = line.split_once(char::is_whitespace) else {
|
||||
continue;
|
||||
};
|
||||
let value = value.trim();
|
||||
if key.eq_ignore_ascii_case("hostname") && !value.is_empty() {
|
||||
config.hostname = Some(value.to_string());
|
||||
} else if key.eq_ignore_ascii_case("port") {
|
||||
config.port = value.parse().ok();
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
|
||||
@@ -1146,7 +1236,8 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
FrameBuffer, Predictor, parse_ssh_config_hostname, ssh_destination_host, startup_command,
|
||||
FrameBuffer, Predictor, parse_ssh_config, raw_contains_host_table, ssh_destination_host,
|
||||
startup_command, toml_bare_key_or_quoted,
|
||||
};
|
||||
use dosh::protocol::Frame;
|
||||
|
||||
@@ -1154,7 +1245,7 @@ mod tests {
|
||||
fn parses_ssh_config_hostname() {
|
||||
let raw = "user palav\nhostname palav.dev\nport 22\n";
|
||||
assert_eq!(
|
||||
parse_ssh_config_hostname(raw),
|
||||
parse_ssh_config(raw).hostname,
|
||||
Some("palav.dev".to_string())
|
||||
);
|
||||
}
|
||||
@@ -1163,11 +1254,29 @@ mod tests {
|
||||
fn parses_ssh_config_hostname_case_insensitively() {
|
||||
let raw = "HostName 192.0.2.10\n";
|
||||
assert_eq!(
|
||||
parse_ssh_config_hostname(raw),
|
||||
parse_ssh_config(raw).hostname,
|
||||
Some("192.0.2.10".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ssh_config_port() {
|
||||
let parsed = parse_ssh_config("hostname example.com\nport 2222\n");
|
||||
assert_eq!(parsed.hostname, Some("example.com".to_string()));
|
||||
assert_eq!(parsed.port, Some(2222));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_existing_imported_host_tables() {
|
||||
assert!(raw_contains_host_table(
|
||||
"[palav]\nssh = \"palav\"\n",
|
||||
"palav"
|
||||
));
|
||||
assert!(raw_contains_host_table("[\"home box\"]\n", "home box"));
|
||||
assert_eq!(toml_bare_key_or_quoted("palav"), "palav");
|
||||
assert_eq!(toml_bare_key_or_quoted("home box"), "\"home box\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_user_from_fallback_udp_host() {
|
||||
assert_eq!(ssh_destination_host("palav@example.com"), "example.com");
|
||||
|
||||
Reference in New Issue
Block a user