Skip to main content

virtuoso_cli/
models.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
5pub enum ExecutionStatus {
6    Success,
7    Failure,
8    Partial,
9    Error,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct VirtuosoResult {
14    pub status: ExecutionStatus,
15    pub output: String,
16    pub errors: Vec<String>,
17    pub warnings: Vec<String>,
18    pub execution_time: Option<f64>,
19    pub metadata: HashMap<String, String>,
20}
21
22impl VirtuosoResult {
23    /// Transport-level success: bridge returned STX (not NAK/timeout).
24    /// Does NOT mean the SKILL call succeeded — SKILL functions return "nil"
25    /// on failure via STX. Use skill_ok() to check SKILL-level success.
26    pub fn ok(&self) -> bool {
27        self.status == ExecutionStatus::Success
28    }
29
30    /// True when the bridge succeeded AND SKILL returned a non-nil value.
31    /// Use this whenever a SKILL function signals failure by returning nil
32    /// (e.g. design(), dbOpenCellViewByType(), getData()).
33    pub fn skill_ok(&self) -> bool {
34        self.status == ExecutionStatus::Success && self.output.trim() != "nil"
35    }
36
37    pub fn success(output: impl Into<String>) -> Self {
38        Self {
39            status: ExecutionStatus::Success,
40            output: output.into(),
41            errors: Vec::new(),
42            warnings: Vec::new(),
43            execution_time: None,
44            metadata: HashMap::new(),
45        }
46    }
47
48    pub fn error(errors: Vec<String>) -> Self {
49        Self {
50            status: ExecutionStatus::Error,
51            output: String::new(),
52            errors,
53            warnings: Vec::new(),
54            execution_time: None,
55            metadata: HashMap::new(),
56        }
57    }
58
59    pub fn save_json(&self, path: &std::path::Path) -> std::io::Result<()> {
60        let json =
61            serde_json::to_string_pretty(self).map_err(|e| std::io::Error::other(e.to_string()))?;
62        std::fs::write(path, json)
63    }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SimulationResult {
68    pub status: ExecutionStatus,
69    pub tool_version: Option<String>,
70    pub data: HashMap<String, Vec<f64>>,
71    pub errors: Vec<String>,
72    pub warnings: Vec<String>,
73    pub metadata: HashMap<String, String>,
74}
75
76impl SimulationResult {
77    pub fn ok(&self) -> bool {
78        self.status == ExecutionStatus::Success
79    }
80
81    pub fn save_json(&self, path: &std::path::Path) -> std::io::Result<()> {
82        let json =
83            serde_json::to_string_pretty(self).map_err(|e| std::io::Error::other(e.to_string()))?;
84        std::fs::write(path, json)
85    }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct RemoteTaskResult {
90    pub success: bool,
91    pub returncode: i32,
92    pub stdout: String,
93    pub stderr: String,
94    pub remote_dir: Option<String>,
95    pub error: Option<String>,
96    pub timings: HashMap<String, f64>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct RemoteSshEnv {
101    pub remote_host: String,
102    pub remote_user: Option<String>,
103    pub jump_host: Option<String>,
104    pub jump_user: Option<String>,
105}
106
107fn default_version() -> u32 {
108    1
109}
110
111/// Registration record written by bridge.il when a Virtuoso session starts.
112/// Lives at ~/.cache/virtuoso_bridge/sessions/<id>.json
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct SessionInfo {
115    pub id: String,
116    pub port: u16,
117    pub pid: u32,
118    pub host: String,
119    pub user: String,
120    pub created: String,
121}
122
123impl SessionInfo {
124    pub(crate) fn sessions_dir() -> std::path::PathBuf {
125        dirs::cache_dir()
126            .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
127            .join("virtuoso_bridge")
128            .join("sessions")
129    }
130
131    pub fn load(id: &str) -> std::io::Result<Self> {
132        let path = Self::sessions_dir().join(format!("{id}.json"));
133        let json = std::fs::read_to_string(&path)
134            .map_err(|e| std::io::Error::new(e.kind(), format!("session '{id}' not found: {e}")))?;
135        serde_json::from_str(&json).map_err(|e| std::io::Error::other(e.to_string()))
136    }
137
138    pub fn list() -> std::io::Result<Vec<Self>> {
139        let dir = Self::sessions_dir();
140        if !dir.exists() {
141            return Ok(Vec::new());
142        }
143        let mut sessions = Vec::new();
144        for entry in std::fs::read_dir(&dir)? {
145            let entry = entry?;
146            let path = entry.path();
147            if path.extension().is_some_and(|e| e == "json") {
148                if let Ok(json) = std::fs::read_to_string(&path) {
149                    if let Ok(s) = serde_json::from_str::<Self>(&json) {
150                        sessions.push(s);
151                    }
152                }
153            }
154        }
155        sessions.sort_by(|a, b| a.id.cmp(&b.id));
156        Ok(sessions)
157    }
158
159    /// List sessions on a remote host via SSH.
160    /// Reads all session JSON files from `~/.cache/virtuoso_bridge/sessions/`.
161    pub fn list_remote(runner: &crate::transport::ssh::SSHRunner) -> std::io::Result<Vec<Self>> {
162        let script = r#"for f in "$HOME"/.cache/virtuoso_bridge/sessions/*.json; do [ -f "$f" ] && echo "---SESSION---" && cat "$f"; done"#;
163        let result = runner
164            .run_command(script, None)
165            .map_err(|e| std::io::Error::other(e.to_string()))?;
166
167        let mut sessions = Vec::new();
168        for chunk in result.stdout.split("---SESSION---") {
169            let chunk = chunk.trim();
170            if chunk.is_empty() {
171                continue;
172            }
173            if let Ok(s) = serde_json::from_str::<Self>(chunk) {
174                sessions.push(s);
175            }
176        }
177        sessions.sort_by(|a, b| a.id.cmp(&b.id));
178        Ok(sessions)
179    }
180
181    /// Fetch remote sessions and sync them to the local sessions directory.
182    /// Returns the number of sessions synced.
183    pub fn sync_from_remote(runner: &crate::transport::ssh::SSHRunner) -> std::io::Result<usize> {
184        let remote = Self::list_remote(runner)?;
185        let dir = Self::sessions_dir();
186        std::fs::create_dir_all(&dir)?;
187        let mut count = 0;
188        for s in &remote {
189            let path = dir.join(format!("{}.json", s.id));
190            let json = serde_json::to_string_pretty(s)
191                .map_err(|e| std::io::Error::other(e.to_string()))?;
192            std::fs::write(path, json)?;
193            count += 1;
194        }
195        Ok(count)
196    }
197
198    /// Check if the daemon is still alive by checking if the port is bound.
199    pub fn is_alive(&self) -> bool {
200        use std::net::TcpStream;
201        use std::time::Duration;
202        TcpStream::connect_timeout(
203            &format!("127.0.0.1:{}", self.port).parse().unwrap(),
204            Duration::from_millis(200),
205        )
206        .is_ok()
207    }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct TunnelState {
212    #[serde(default = "default_version")]
213    pub version: u32,
214    pub port: u16,
215    pub pid: u32,
216    pub remote_host: String,
217    pub setup_path: Option<String>,
218}
219
220impl TunnelState {
221    fn state_path(profile: Option<&str>) -> std::path::PathBuf {
222        let cache_dir = dirs::cache_dir()
223            .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
224            .join("virtuoso_bridge");
225        let _ = std::fs::create_dir_all(&cache_dir);
226        let filename = match profile {
227            Some(p) if !p.is_empty() => format!("state_{p}.json"),
228            _ => "state.json".into(),
229        };
230        cache_dir.join(filename)
231    }
232
233    pub fn save_with_profile(&self, profile: Option<&str>) -> std::io::Result<()> {
234        let path = Self::state_path(profile);
235        let json =
236            serde_json::to_string_pretty(self).map_err(|e| std::io::Error::other(e.to_string()))?;
237        std::fs::write(path, json)
238    }
239
240    pub fn save(&self) -> std::io::Result<()> {
241        self.save_with_profile(std::env::var("VB_PROFILE").ok().as_deref())
242    }
243
244    pub fn load_with_profile(profile: Option<&str>) -> std::io::Result<Option<Self>> {
245        let path = Self::state_path(profile);
246        if !path.exists() {
247            return Ok(None);
248        }
249        let json = std::fs::read_to_string(path)?;
250        serde_json::from_str(&json)
251            .map(Some)
252            .map_err(|e| std::io::Error::other(e.to_string()))
253    }
254
255    pub fn load() -> std::io::Result<Option<Self>> {
256        Self::load_with_profile(std::env::var("VB_PROFILE").ok().as_deref())
257    }
258
259    pub fn clear_with_profile(profile: Option<&str>) -> std::io::Result<()> {
260        let path = Self::state_path(profile);
261        if path.exists() {
262            std::fs::remove_file(path)?;
263        }
264        Ok(())
265    }
266
267    pub fn clear() -> std::io::Result<()> {
268        Self::clear_with_profile(std::env::var("VB_PROFILE").ok().as_deref())
269    }
270}