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)]
130pub struct DaemonStats {
131 pub calls: u64,
132 pub errors: u64,
133 pub uptime_secs: u64,
134}
135
136impl DaemonStats {
137 pub fn path(port: u16) -> String {
138 format!("/tmp/.ramic_stats_{port}")
139 }
140
141 pub fn load(port: u16) -> Option<Self> {
142 let json = std::fs::read_to_string(Self::path(port)).ok()?;
143 serde_json::from_str(&json).ok()
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SessionInfo {
151 pub id: String,
152 pub port: u16,
153 pub pid: u32,
154 pub host: String,
155 pub user: String,
156 pub created: String,
157}
158
159impl SessionInfo {
160 pub(crate) fn sessions_dir() -> std::path::PathBuf {
161 dirs::cache_dir()
162 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
163 .join("virtuoso_bridge")
164 .join("sessions")
165 }
166
167 pub fn load(id: &str) -> std::io::Result<Self> {
168 let path = Self::sessions_dir().join(format!("{id}.json"));
169 let json = std::fs::read_to_string(&path)
170 .map_err(|e| std::io::Error::new(e.kind(), format!("session '{id}' not found: {e}")))?;
171 serde_json::from_str(&json).map_err(|e| std::io::Error::other(e.to_string()))
172 }
173
174 pub fn list() -> std::io::Result<Vec<Self>> {
175 let dir = Self::sessions_dir();
176 if !dir.exists() {
177 return Ok(Vec::new());
178 }
179 let mut sessions = Vec::new();
180 for entry in std::fs::read_dir(&dir)? {
181 let entry = entry?;
182 let path = entry.path();
183 if path.extension().is_some_and(|e| e == "json") {
184 if let Ok(json) = std::fs::read_to_string(&path) {
185 if let Ok(s) = serde_json::from_str::<Self>(&json) {
186 sessions.push(s);
187 }
188 }
189 }
190 }
191 sessions.sort_by(|a, b| a.id.cmp(&b.id));
192 Ok(sessions)
193 }
194
195 pub fn list_remote(runner: &crate::transport::ssh::SSHRunner) -> std::io::Result<Vec<Self>> {
198 let script = r#"for f in "$HOME"/.cache/virtuoso_bridge/sessions/*.json; do [ -f "$f" ] && echo "---SESSION---" && cat "$f"; done"#;
199 let result = runner
200 .run_command(script, None)
201 .map_err(|e| std::io::Error::other(e.to_string()))?;
202
203 let mut sessions = Vec::new();
204 for chunk in result.stdout.split("---SESSION---") {
205 let chunk = chunk.trim();
206 if chunk.is_empty() {
207 continue;
208 }
209 if let Ok(s) = serde_json::from_str::<Self>(chunk) {
210 sessions.push(s);
211 }
212 }
213 sessions.sort_by(|a, b| a.id.cmp(&b.id));
214 Ok(sessions)
215 }
216
217 pub fn sync_from_remote(runner: &crate::transport::ssh::SSHRunner) -> std::io::Result<usize> {
220 let remote = Self::list_remote(runner)?;
221 let dir = Self::sessions_dir();
222 std::fs::create_dir_all(&dir)?;
223 let mut count = 0;
224 for s in &remote {
225 let path = dir.join(format!("{}.json", s.id));
226 let json = serde_json::to_string_pretty(s)
227 .map_err(|e| std::io::Error::other(e.to_string()))?;
228 std::fs::write(path, json)?;
229 count += 1;
230 }
231 Ok(count)
232 }
233
234 pub fn is_alive(&self) -> bool {
236 use std::net::TcpStream;
237 use std::time::Duration;
238 TcpStream::connect_timeout(
239 &format!("127.0.0.1:{}", self.port).parse().unwrap(),
240 Duration::from_millis(200),
241 )
242 .is_ok()
243 }
244
245 pub fn list_alive() -> Vec<Self> {
247 Self::list().unwrap_or_default().into_iter().filter(|s| s.is_alive()).collect()
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct TunnelState {
253 #[serde(default = "default_version")]
254 pub version: u32,
255 pub port: u16,
256 pub pid: u32,
257 pub remote_host: String,
258 pub setup_path: Option<String>,
259}
260
261impl TunnelState {
262 fn state_path(profile: Option<&str>) -> std::path::PathBuf {
263 let cache_dir = dirs::cache_dir()
264 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
265 .join("virtuoso_bridge");
266 let _ = std::fs::create_dir_all(&cache_dir);
267 let filename = match profile {
268 Some(p) if !p.is_empty() => format!("state_{p}.json"),
269 _ => "state.json".into(),
270 };
271 cache_dir.join(filename)
272 }
273
274 pub fn save_with_profile(&self, profile: Option<&str>) -> std::io::Result<()> {
275 let path = Self::state_path(profile);
276 let json =
277 serde_json::to_string_pretty(self).map_err(|e| std::io::Error::other(e.to_string()))?;
278 std::fs::write(path, json)
279 }
280
281 pub fn save(&self) -> std::io::Result<()> {
282 self.save_with_profile(std::env::var("VB_PROFILE").ok().as_deref())
283 }
284
285 pub fn load_with_profile(profile: Option<&str>) -> std::io::Result<Option<Self>> {
286 let path = Self::state_path(profile);
287 if !path.exists() {
288 return Ok(None);
289 }
290 let json = std::fs::read_to_string(path)?;
291 serde_json::from_str(&json)
292 .map(Some)
293 .map_err(|e| std::io::Error::other(e.to_string()))
294 }
295
296 pub fn load() -> std::io::Result<Option<Self>> {
297 Self::load_with_profile(std::env::var("VB_PROFILE").ok().as_deref())
298 }
299
300 pub fn clear_with_profile(profile: Option<&str>) -> std::io::Result<()> {
301 let path = Self::state_path(profile);
302 if path.exists() {
303 std::fs::remove_file(path)?;
304 }
305 Ok(())
306 }
307
308 pub fn clear() -> std::io::Result<()> {
309 Self::clear_with_profile(std::env::var("VB_PROFILE").ok().as_deref())
310 }
311}