Skip to main content

virtuoso_cli/spectre/
runner.rs

1#![allow(dead_code)]
2
3use crate::error::{Result, VirtuosoError};
4use crate::models::{ExecutionStatus, SimulationResult};
5use crate::spectre::jobs::{Job, JobStatus};
6use crate::transport::ssh::SSHRunner;
7use std::collections::HashMap;
8use std::fs;
9use std::path::PathBuf;
10use std::process::{Command, Stdio};
11use uuid::Uuid;
12
13pub struct SpectreSimulator {
14    pub spectre_cmd: String,
15    pub spectre_args: Vec<String>,
16    pub timeout: u64,
17    pub work_dir: PathBuf,
18    pub output_format: String,
19    pub remote: bool,
20    pub ssh_runner: Option<SSHRunner>,
21    pub remote_work_dir: Option<String>,
22    pub keep_remote_files: bool,
23}
24
25impl SpectreSimulator {
26    pub fn from_env() -> Result<Self> {
27        let cfg = crate::config::Config::from_env()?;
28        let remote = cfg.is_remote();
29
30        let ssh_runner = if remote {
31            let mut runner = SSHRunner::new(cfg.remote_host.as_deref().unwrap_or(""));
32            if let Some(ref user) = cfg.remote_user {
33                runner = runner.with_user(user);
34            }
35            if let Some(ref jump) = cfg.jump_host {
36                let mut r = runner.with_jump(jump);
37                if let Some(ref user) = cfg.jump_user {
38                    r.jump_user = Some(user.clone());
39                }
40                runner = r;
41            }
42            Some(runner)
43        } else {
44            None
45        };
46
47        Ok(Self {
48            spectre_cmd: cfg.spectre_cmd,
49            spectre_args: cfg.spectre_args,
50            timeout: cfg.timeout,
51            work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
52            output_format: "psfascii".into(),
53            remote,
54            ssh_runner,
55            remote_work_dir: None,
56            keep_remote_files: cfg.keep_remote_files,
57        })
58    }
59
60    pub fn run_simulation(
61        &self,
62        netlist: &str,
63        params: Option<&HashMap<String, String>>,
64    ) -> Result<SimulationResult> {
65        if self.remote {
66            self.run_remote(netlist, params)
67        } else {
68            self.run_local(netlist, params)
69        }
70    }
71
72    pub fn check_license(&self) -> Result<String> {
73        if let Some(ref runner) = self.ssh_runner {
74            // Combine into one SSH round-trip to avoid repeated handshakes.
75            let cmd = "which spectre 2>/dev/null || echo 'not found'; \
76                       spectre -W 2>/dev/null | head -1 || echo 'unknown'; \
77                       lmstat -a 2>/dev/null | grep -i spectre | head -5 || echo 'lmstat not available'";
78            let result = runner.run_command(cmd, None)?;
79            Ok(result.stdout.trim().to_string())
80        } else {
81            let output = Command::new("sh")
82                .arg("-c")
83                .arg("which spectre 2>/dev/null && spectre -W 2>/dev/null | head -1")
84                .output()
85                .map_err(|e| VirtuosoError::Execution(e.to_string()))?;
86            Ok(String::from_utf8_lossy(&output.stdout).into_owned())
87        }
88    }
89
90    /// Launch simulation in background, return job ID immediately.
91    /// Works for both local and remote (via SSH nohup).
92    pub fn run_async(&self, netlist: &str) -> Result<Job> {
93        if self.remote {
94            return self.run_async_remote(netlist);
95        }
96
97        let run_id = Uuid::new_v4().to_string()[..8].to_string();
98        let run_dir = self.work_dir.join(&run_id);
99        fs::create_dir_all(&run_dir).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
100
101        let netlist_path = run_dir.join("input.scs");
102        fs::write(&netlist_path, netlist).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
103
104        let raw_dir = run_dir.join("raw");
105        fs::create_dir_all(&raw_dir).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
106
107        let log_path = run_dir.join("spectre.out");
108
109        let mut cmd = Command::new(&self.spectre_cmd);
110        cmd.arg("-64")
111            .arg(&netlist_path)
112            .arg("+escchars")
113            .arg("+log")
114            .arg(&log_path)
115            .arg("-format")
116            .arg(&self.output_format)
117            .arg("-raw")
118            .arg(&raw_dir)
119            .arg("+lqtimeout")
120            .arg("900")
121            .arg("-maxw")
122            .arg("5")
123            .arg("-maxn")
124            .arg("5")
125            .arg("+logstatus");
126
127        for arg in &self.spectre_args {
128            cmd.arg(arg);
129        }
130
131        let child = cmd
132            .stdout(Stdio::null())
133            .stderr(Stdio::null())
134            .spawn()
135            .map_err(|e| VirtuosoError::Execution(format!("spectre failed to start: {e}")))?;
136
137        let job = Job {
138            id: run_id,
139            status: JobStatus::Running,
140            netlist_path: netlist_path.to_string_lossy().into(),
141            raw_dir: Some(raw_dir.to_string_lossy().into()),
142            pid: Some(child.id()),
143            created: chrono::Local::now().to_rfc3339(),
144            finished: None,
145            error: None,
146            remote_host: None,
147            remote_dir: None,
148        };
149        job.save()?;
150        // Process runs detached — status checked lazily via Job::refresh()
151        Ok(job)
152    }
153
154    fn run_async_remote(&self, netlist: &str) -> Result<Job> {
155        let runner = self.ssh_runner.as_ref().ok_or_else(|| {
156            VirtuosoError::Execution("no SSH runner for remote async simulation".into())
157        })?;
158
159        let run_id = Uuid::new_v4().to_string()[..8].to_string();
160        let remote_dir = format!("/tmp/virtuoso_bridge/spectre/{run_id}");
161
162        // Create dir + upload netlist
163        runner.run_command(&format!("mkdir -p {remote_dir}"), None)?;
164        runner.upload_text(netlist, &format!("{remote_dir}/input.scs"))?;
165
166        // Build spectre command
167        let extra = if self.spectre_args.is_empty() {
168            String::new()
169        } else {
170            format!(" {}", self.spectre_args.join(" "))
171        };
172        // Source login profile for PATH + license env (non-interactive SSH lacks them).
173        // Covers bash (.bash_profile/.bashrc) and sh (.profile).
174        // Use VB_SPECTRE_CMD=<absolute path> if spectre is still not found.
175        let spectre_cmd = format!(
176            ". /etc/profile 2>/dev/null; . ~/.bash_profile 2>/dev/null; . ~/.bashrc 2>/dev/null; \
177             cd {remote_dir} && nohup {cmd} -64 input.scs +escchars +log spectre.out \
178             -format {fmt} -raw raw +lqtimeout 900 -maxw 5 -maxn 5 +logstatus{extra} \
179             > /dev/null 2>&1 & echo $!",
180            cmd = self.spectre_cmd,
181            fmt = self.output_format,
182        );
183
184        // Launch and capture PID
185        let result = runner.run_command(&spectre_cmd, Some(10))?;
186        let pid: u32 = result
187            .stdout
188            .trim()
189            .parse()
190            .map_err(|_| VirtuosoError::Execution(format!("bad PID: {}", result.stdout)))?;
191
192        let job = Job {
193            id: run_id,
194            status: JobStatus::Running,
195            netlist_path: format!("{remote_dir}/input.scs"),
196            raw_dir: Some(format!("{remote_dir}/raw")),
197            pid: Some(pid),
198            created: chrono::Local::now().to_rfc3339(),
199            finished: None,
200            error: None,
201            remote_host: Some(runner.remote_target()),
202            remote_dir: Some(remote_dir),
203        };
204        job.save()?;
205        Ok(job)
206    }
207
208    fn run_local(
209        &self,
210        netlist: &str,
211        _params: Option<&HashMap<String, String>>,
212    ) -> Result<SimulationResult> {
213        let run_id = Uuid::new_v4().to_string();
214        let run_dir = self.work_dir.join(&run_id);
215        fs::create_dir_all(&run_dir).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
216
217        let netlist_path = run_dir.join("input.scs");
218        fs::write(&netlist_path, netlist).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
219
220        let raw_dir = run_dir.join("raw");
221        fs::create_dir_all(&raw_dir).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
222
223        let log_path = run_dir.join("spectre.out");
224
225        let mut cmd = Command::new(&self.spectre_cmd);
226        cmd.arg("-64")
227            .arg(&netlist_path)
228            .arg("+escchars")
229            .arg("+log")
230            .arg(&log_path)
231            .arg("-format")
232            .arg(&self.output_format)
233            .arg("-raw")
234            .arg(&raw_dir)
235            .arg("+lqtimeout")
236            .arg("900")
237            .arg("-maxw")
238            .arg("5")
239            .arg("-maxn")
240            .arg("5")
241            .arg("+logstatus");
242
243        for arg in &self.spectre_args {
244            cmd.arg(arg);
245        }
246
247        let mut child = cmd
248            .stdout(Stdio::null())
249            .stderr(Stdio::null())
250            .spawn()
251            .map_err(|e| VirtuosoError::Execution(format!("spectre failed to start: {e}")))?;
252
253        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(self.timeout);
254        let status = loop {
255            match child.try_wait() {
256                Ok(Some(s)) => break s,
257                Ok(None) => {
258                    if std::time::Instant::now() > deadline {
259                        let _ = child.kill();
260                        let _ = child.wait();
261                        return Err(VirtuosoError::Timeout(self.timeout));
262                    }
263                    std::thread::sleep(std::time::Duration::from_millis(500));
264                }
265                Err(e) => {
266                    return Err(VirtuosoError::Execution(format!(
267                        "failed to wait on spectre: {e}"
268                    )))
269                }
270            }
271        };
272
273        let log_content = fs::read_to_string(&log_path).unwrap_or_default();
274
275        if !status.success() {
276            return Ok(SimulationResult {
277                status: ExecutionStatus::Error,
278                tool_version: None,
279                data: HashMap::new(),
280                errors: vec![format!("spectre exited with code {:?}", status.code())],
281                warnings: Vec::new(),
282                metadata: [("log".into(), log_content)].into_iter().collect(),
283            });
284        }
285
286        let data = if raw_dir.join("psf").exists() || raw_dir.join("results").exists() {
287            crate::spectre::parsers::parse_psf_ascii(&raw_dir)?
288        } else {
289            HashMap::new()
290        };
291
292        Ok(SimulationResult {
293            status: ExecutionStatus::Success,
294            tool_version: None,
295            data,
296            errors: Vec::new(),
297            warnings: Vec::new(),
298            metadata: [("log".into(), log_content), ("run_id".into(), run_id)]
299                .into_iter()
300                .collect(),
301        })
302    }
303
304    fn run_remote(
305        &self,
306        netlist: &str,
307        _params: Option<&HashMap<String, String>>,
308    ) -> Result<SimulationResult> {
309        let runner = self.ssh_runner.as_ref().ok_or_else(|| {
310            VirtuosoError::Execution("no SSH runner available for remote simulation".into())
311        })?;
312
313        let run_id = Uuid::new_v4().to_string();
314        let remote_dir = format!("/tmp/virtuoso_bridge/spectre/{run_id}");
315
316        runner.run_command(&format!("mkdir -p {remote_dir}"), None)?;
317
318        let netlist_content = netlist.to_string();
319        runner.upload_text(&netlist_content, &format!("{remote_dir}/input.scs"))?;
320
321        let spectre_cmd = if self.spectre_args.is_empty() {
322            format!(
323                "{cmd} -64 input.scs +escchars +log spectre.out -format {fmt} -raw raw +lqtimeout 900 -maxw 5 -maxn 5 +logstatus",
324                cmd = self.spectre_cmd,
325                fmt = self.output_format
326            )
327        } else {
328            format!(
329                "{cmd} -64 input.scs +escchars +log spectre.out -format {fmt} -raw raw +lqtimeout 900 -maxw 5 -maxn 5 +logstatus {}",
330                self.spectre_args.join(" "),
331                cmd = self.spectre_cmd,
332                fmt = self.output_format
333            )
334        };
335
336        let sim_cmd = format!("cd {remote_dir} && {spectre_cmd}");
337        let result = runner.run_command(&sim_cmd, Some(self.timeout * 2))?;
338
339        let mut sim_result = SimulationResult {
340            status: if result.success {
341                ExecutionStatus::Success
342            } else {
343                ExecutionStatus::Error
344            },
345            tool_version: None,
346            data: HashMap::new(),
347            errors: if result.success {
348                Vec::new()
349            } else {
350                vec![result.stderr.clone()]
351            },
352            warnings: Vec::new(),
353            metadata: [
354                ("run_id".into(), run_id.clone()),
355                ("remote_dir".into(), remote_dir.clone()),
356            ]
357            .into_iter()
358            .collect(),
359        };
360
361        if result.success {
362            let local_raw = self.work_dir.join(&run_id).join("raw");
363            runner.download(&format!("{remote_dir}/raw"), local_raw.to_str().unwrap())?;
364
365            if let Ok(data) = crate::spectre::parsers::parse_psf_ascii(&local_raw) {
366                sim_result.data = data;
367            }
368        }
369
370        if !self.keep_remote_files {
371            runner.run_command(&format!("rm -rf {remote_dir}"), None)?;
372        }
373
374        Ok(sim_result)
375    }
376}