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