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