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