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 pub fn ok(&self) -> bool {
27 self.status == ExecutionStatus::Success
28 }
29
30 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#[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 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 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 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}