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