diff options
-rw-r--r-- | Cargo.lock | 120 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/lib.rs | 68 | ||||
-rw-r--r-- | src/main.rs | 12 | ||||
-rw-r--r-- | src/ssh.rs | 45 | ||||
-rw-r--r-- | src/tmux.rs | 4 |
6 files changed, 226 insertions, 24 deletions
@@ -9,6 +9,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -140,6 +146,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f2cb48b81b1dc9f39676bf99f5499babfec7cd8fe14307f7b3d747208fb5690" [[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] name = "io-lifetimes" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -175,12 +190,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" [[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "linux-raw-sys" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -215,6 +266,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] +name = "openssl-sys" +version = "0.9.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176be2629957c157240f68f61f2d0053ad3a4ecfdd9ebf1e6521d18d9635cf67" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "os_str_bytes" version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -227,12 +291,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -344,6 +439,12 @@ dependencies = [ ] [[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] name = "serde" version = "1.0.156" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -379,6 +480,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] +name = "ssh2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fe461910559f6d5604c3731d00d2aafc4a83d1665922e280f42f9a168d5455" +dependencies = [ + "bitflags", + "libc", + "libssh2-sys", + "parking_lot", +] + +[[package]] name = "sshr" version = "0.1.0" dependencies = [ @@ -388,6 +501,7 @@ dependencies = [ "indoc", "ron", "serde", + "ssh2", "thiserror", "tracing", "tracing-subscriber", @@ -535,6 +649,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -15,3 +15,4 @@ directories = "4.0.1" tracing = "0.1.37" thiserror = "1.0.40" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +ssh2 = "0.9.4" @@ -1,6 +1,11 @@ -use std::{collections::HashMap, fmt::Display, iter::IntoIterator}; +use std::{ + collections::{hash_map::Entry, HashMap}, + fmt::Display, + iter::IntoIterator, + time::Duration, +}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; pub use config::Config; pub use history::History; @@ -8,10 +13,9 @@ pub use tmux::Tmux; mod config; mod history; +mod ssh; mod tmux; -pub type Timestamp = Option<u64>; - pub trait SessionSource { type Error: std::error::Error; @@ -21,28 +25,60 @@ pub trait SessionSource { fn update( &self, - mut hash_map: HashMap<String, Timestamp>, - ) -> Result<HashMap<String, Timestamp>, Self::Error> { + mut hash_map: HashMap<String, Session>, + ) -> Result<HashMap<String, Session>, Self::Error> { for session in self.sessions()? { - hash_map - .entry(session.name) - .and_modify(|o| { - if let Some(timestamp) = session.timestamp { - *o = o.map(|t| timestamp.max(t)); - } - }) - .or_insert(session.timestamp); + match hash_map.entry(session.name.to_owned()) { + Entry::Occupied(mut o) if &session > o.get() => { + o.insert(session); + } + Entry::Vacant(v) => { + v.insert(session); + } + _ => {} + } } Ok(hash_map) } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum State { + Discovered, + Connected, + Updated, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Session { - pub timestamp: Timestamp, + pub state: State, + + #[serde(with = "epoch_timestamp")] + pub timestamp: Duration, + pub name: String, } +mod epoch_timestamp { + use std::time::Duration; + + use serde::{self, Deserialize, Deserializer, Serializer}; + + pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_u64(duration.as_secs()) + } + + pub fn deserialize<'de, D>(d: D) -> Result<Duration, D::Error> + where + D: Deserializer<'de>, + { + u64::deserialize(d).map(Duration::from_secs) + } +} + impl Display for Session { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.name) diff --git a/src/main.rs b/src/main.rs index 7534bb1..2357ab2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use clap::Parser; -use sshr::{Config, History, Session, SessionSource, Timestamp, Tmux}; +use sshr::{Config, History, Session, SessionSource, Tmux}; fn main() -> anyhow::Result<()> { let config = Config::parse(); @@ -20,7 +20,10 @@ fn main() -> anyhow::Result<()> { } fn list_sessions(config: Config) -> anyhow::Result<()> { - let mut sessions: HashMap<String, Timestamp> = HashMap::new(); + let mut sessions = HashMap::new(); + + let ssh = ssh2::Session::new()?; + sessions = ssh.update(sessions)?; let tmux = Tmux::new(config.socket_name); sessions = tmux.update(sessions)?; @@ -31,10 +34,7 @@ fn list_sessions(config: Config) -> anyhow::Result<()> { } } - let mut sessions: Vec<Session> = sessions - .into_iter() - .map(|(name, timestamp)| Session { name, timestamp }) - .collect(); + let mut sessions: Vec<Session> = sessions.into_values().collect(); sessions.sort(); diff --git a/src/ssh.rs b/src/ssh.rs new file mode 100644 index 0000000..29c6de3 --- /dev/null +++ b/src/ssh.rs @@ -0,0 +1,45 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use directories::UserDirs; +use ssh2::KnownHostFileKind; + +use crate::{Session, SessionSource, State}; + +impl SessionSource for ssh2::Session { + type Error = ssh2::Error; + + type Iter = Vec<Session>; + + fn sessions(&self) -> Result<Self::Iter, Self::Error> { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Current time is pre-epoch. (Time traveler?)"); + + let mut known_hosts = self.known_hosts()?; + + let file = UserDirs::new() + .ok_or_else(ssh2::Error::unknown)? + .home_dir() + .join(".ssh/known_hosts"); + + known_hosts.read_file(&file, KnownHostFileKind::OpenSSH)?; + + let sessions = known_hosts + .hosts()? + .into_iter() + .filter_map(|h| match h.name() { + Some(name) => Some(Session { + state: State::Discovered, + timestamp, + name: name.to_owned(), + }), + None => { + tracing::warn!("Invalid host: No plain text host name exists"); + None + } + }) + .collect(); + + Ok(sessions) + } +} diff --git a/src/tmux.rs b/src/tmux.rs index c6e83b1..6c53831 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -12,7 +12,7 @@ pub struct Tmux { } impl Tmux { - const SESSION_FORMAT: &str = r##"Session( name: "#S", timestamp: Some(#{?session_last_attached,#{session_last_attached},#{session_created}}))"##; + const SESSION_FORMAT: &str = r##"Session(name: "#S", #{?session_last_attached,timestamp: #{session_last_attached}#, state: Updated,timestamp: #{session_created}#, state: Connected})"##; pub fn new(socket_name: String) -> Self { Self { socket_name } @@ -39,7 +39,7 @@ impl SessionSource for Tmux { .filter_map(|s| match ron::from_str(s) { Ok(session) => Some(session), Err(err) => { - tracing::warn!(?err, "Invalid session format"); + tracing::warn!(%err, "Invalid session format"); None } }) |