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 pub fn ok(&self) -> bool {
28 self.status == ExecutionStatus::Success
29 }
30
31 pub fn skill_ok(&self) -> bool {
35 self.status == ExecutionStatus::Success && self.output.trim() != "nil"
36 }
37
38 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 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#[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 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 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 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}