Skip to main content

virtuoso_cli/
models.rs

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