Skip to main content

virtuoso_cli/spectre/
runner.rs

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