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> {
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 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#[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 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 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 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}