use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionContext {
pub host: String,
#[serde(default)]
pub ssh_options: Vec<String>,
pub local_dir: PathBuf,
#[serde(default)]
pub control_path: Option<PathBuf>,
}
impl SessionContext {
fn path(session_dir: &Path) -> PathBuf {
session_dir.join("context.toml")
}
pub async fn save(&self, session_dir: &Path) -> Result<()> {
crate::persist::save_toml_atomic(self, &Self::path(session_dir), "ssh session context")
.await
}
#[allow(dead_code)]
pub async fn load(session_dir: &Path) -> Result<Self> {
let path = Self::path(session_dir);
let content = tokio::fs::read_to_string(&path)
.await
.with_context(|| format!("reading {}", path.display()))?;
toml::from_str(&content).context("parsing ssh session context")
}
}
pub fn user_set_control_options(ssh_options: &[String]) -> bool {
ssh_options.iter().any(|opt| {
let body = opt.strip_prefix("-o").filter(|rest| !rest.is_empty());
let value = body.unwrap_or(opt.as_str()).trim();
let lower = value.to_ascii_lowercase();
lower.starts_with("controlmaster") || lower.starts_with("controlpath")
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_split_control_path_option() {
let opts = vec![
"-o".to_string(),
"ControlPath=/tmp/x".to_string(),
"dev".to_string(),
];
assert!(user_set_control_options(&opts));
}
#[test]
fn detects_glued_control_master_option() {
let opts = vec!["-oControlMaster=auto".to_string()];
assert!(user_set_control_options(&opts));
}
#[test]
fn ignores_unrelated_options() {
let opts = vec![
"-p".to_string(),
"2222".to_string(),
"-oPort=22".to_string(),
];
assert!(!user_set_control_options(&opts));
}
#[tokio::test]
async fn round_trips_through_toml() {
let dir = crate::test_support::make_temp_dir("shine-session-context").await;
let ctx = SessionContext {
host: "dev".to_string(),
ssh_options: vec!["-p".to_string(), "2222".to_string()],
local_dir: PathBuf::from("/home/u/proj"),
control_path: Some(PathBuf::from("/home/u/.shine/run/ssh/abc/ctl.sock")),
};
ctx.save(&dir).await.unwrap();
let loaded = SessionContext::load(&dir).await.unwrap();
assert_eq!(ctx, loaded);
tokio::fs::remove_dir_all(&dir).await.unwrap();
}
}