Skip to main content

virtuoso_cli/spectre/
runner.rs

1use crate::error::{Result, VirtuosoError};
2use crate::models::{ExecutionStatus, SimulationResult};
3use crate::transport::ssh::SSHRunner;
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use uuid::Uuid;
9
10pub struct SpectreSimulator {
11    pub spectre_cmd: String,
12    pub spectre_args: Vec<String>,
13    pub timeout: u64,
14    pub work_dir: PathBuf,
15    pub output_format: String,
16    pub remote: bool,
17    pub ssh_runner: Option<SSHRunner>,
18    pub remote_work_dir: Option<String>,
19    pub keep_remote_files: bool,
20}
21
22impl SpectreSimulator {
23    pub fn from_env() -> Result<Self> {
24        let cfg = crate::config::Config::from_env()?;
25        let remote = cfg.is_remote();
26
27        let ssh_runner = if remote {
28            let mut runner = SSHRunner::new(cfg.remote_host.as_deref().unwrap_or(""));
29            if let Some(ref user) = cfg.remote_user {
30                runner = runner.with_user(user);
31            }
32            if let Some(ref jump) = cfg.jump_host {
33                let mut r = runner.with_jump(jump);
34                if let Some(ref user) = cfg.jump_user {
35                    r.jump_user = Some(user.clone());
36                }
37                runner = r;
38            }
39            Some(runner)
40        } else {
41            None
42        };
43
44        Ok(Self {
45            spectre_cmd: cfg.spectre_cmd,
46            spectre_args: cfg.spectre_args,
47            timeout: cfg.timeout,
48            work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
49            output_format: "psfascii".into(),
50            remote,
51            ssh_runner,
52            remote_work_dir: None,
53            keep_remote_files: cfg.keep_remote_files,
54        })
55    }
56
57    pub fn run_simulation(
58        &self,
59        netlist: &str,
60        params: Option<&HashMap<String, String>>,
61    ) -> Result<SimulationResult> {
62        if self.remote {
63            self.run_remote(netlist, params)
64        } else {
65            self.run_local(netlist, params)
66        }
67    }
68
69    pub fn check_license(&self) -> Result<String> {
70        if let Some(ref runner) = self.ssh_runner {
71            let mut cmds = Vec::new();
72            cmds.push("which spectre 2>/dev/null || echo 'not found'");
73            cmds.push("spectre -W 2>/dev/null | head -1 || echo 'unknown'");
74            cmds.push(
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    fn run_local(
95        &self,
96        netlist: &str,
97        _params: Option<&HashMap<String, String>>,
98    ) -> Result<SimulationResult> {
99        let run_id = Uuid::new_v4().to_string();
100        let run_dir = self.work_dir.join(&run_id);
101        fs::create_dir_all(&run_dir).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
102
103        let netlist_path = run_dir.join("input.scs");
104        fs::write(&netlist_path, netlist).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
105
106        let raw_dir = run_dir.join("raw");
107        fs::create_dir_all(&raw_dir).map_err(|e| VirtuosoError::Execution(e.to_string()))?;
108
109        let log_path = run_dir.join("spectre.out");
110
111        let mut cmd = Command::new(&self.spectre_cmd);
112        cmd.arg("-64")
113            .arg(&netlist_path)
114            .arg("+escchars")
115            .arg("+log")
116            .arg(&log_path)
117            .arg("-format")
118            .arg(&self.output_format)
119            .arg("-raw")
120            .arg(&raw_dir)
121            .arg("+lqtimeout")
122            .arg("900")
123            .arg("-maxw")
124            .arg("5")
125            .arg("-maxn")
126            .arg("5")
127            .arg("+logstatus");
128
129        for arg in &self.spectre_args {
130            cmd.arg(arg);
131        }
132
133        let mut child = cmd
134            .stdout(Stdio::null())
135            .stderr(Stdio::null())
136            .spawn()
137            .map_err(|e| VirtuosoError::Execution(format!("spectre failed to start: {e}")))?;
138
139        let deadline =
140            std::time::Instant::now() + std::time::Duration::from_secs(self.timeout);
141        let status = loop {
142            match child.try_wait() {
143                Ok(Some(s)) => break s,
144                Ok(None) => {
145                    if std::time::Instant::now() > deadline {
146                        let _ = child.kill();
147                        let _ = child.wait();
148                        return Err(VirtuosoError::Timeout(self.timeout));
149                    }
150                    std::thread::sleep(std::time::Duration::from_millis(500));
151                }
152                Err(e) => {
153                    return Err(VirtuosoError::Execution(format!(
154                        "failed to wait on spectre: {e}"
155                    )))
156                }
157            }
158        };
159
160        let log_content = fs::read_to_string(&log_path).unwrap_or_default();
161
162        if !status.success() {
163            return Ok(SimulationResult {
164                status: ExecutionStatus::Error,
165                tool_version: None,
166                data: HashMap::new(),
167                errors: vec![format!("spectre exited with code {:?}", status.code())],
168                warnings: Vec::new(),
169                metadata: [("log".into(), log_content)].into_iter().collect(),
170            });
171        }
172
173        let data = if raw_dir.join("psf").exists() || raw_dir.join("results").exists() {
174            crate::spectre::parsers::parse_psf_ascii(&raw_dir)?
175        } else {
176            HashMap::new()
177        };
178
179        Ok(SimulationResult {
180            status: ExecutionStatus::Success,
181            tool_version: None,
182            data,
183            errors: Vec::new(),
184            warnings: Vec::new(),
185            metadata: [("log".into(), log_content), ("run_id".into(), run_id)]
186                .into_iter()
187                .collect(),
188        })
189    }
190
191    fn run_remote(
192        &self,
193        netlist: &str,
194        _params: Option<&HashMap<String, String>>,
195    ) -> Result<SimulationResult> {
196        let runner = self.ssh_runner.as_ref().ok_or_else(|| {
197            VirtuosoError::Execution("no SSH runner available for remote simulation".into())
198        })?;
199
200        let run_id = Uuid::new_v4().to_string();
201        let remote_dir = format!("/tmp/virtuoso_bridge/spectre/{run_id}");
202
203        runner.run_command(&format!("mkdir -p {remote_dir}"), None)?;
204
205        let netlist_content = netlist.to_string();
206        runner.upload_text(&netlist_content, &format!("{remote_dir}/input.scs"))?;
207
208        let spectre_cmd = if self.spectre_args.is_empty() {
209            format!(
210                "{cmd} -64 input.scs +escchars +log spectre.out -format {fmt} -raw raw +lqtimeout 900 -maxw 5 -maxn 5 +logstatus",
211                cmd = self.spectre_cmd,
212                fmt = self.output_format
213            )
214        } else {
215            format!(
216                "{cmd} -64 input.scs +escchars +log spectre.out -format {fmt} -raw raw +lqtimeout 900 -maxw 5 -maxn 5 +logstatus {}",
217                self.spectre_args.join(" "),
218                cmd = self.spectre_cmd,
219                fmt = self.output_format
220            )
221        };
222
223        let sim_cmd = format!("cd {remote_dir} && {spectre_cmd}");
224        let result = runner.run_command(&sim_cmd, Some(self.timeout * 2))?;
225
226        let mut sim_result = SimulationResult {
227            status: if result.success {
228                ExecutionStatus::Success
229            } else {
230                ExecutionStatus::Error
231            },
232            tool_version: None,
233            data: HashMap::new(),
234            errors: if result.success {
235                Vec::new()
236            } else {
237                vec![result.stderr.clone()]
238            },
239            warnings: Vec::new(),
240            metadata: [
241                ("run_id".into(), run_id.clone()),
242                ("remote_dir".into(), remote_dir.clone()),
243            ]
244            .into_iter()
245            .collect(),
246        };
247
248        if result.success {
249            let local_raw = self.work_dir.join(&run_id).join("raw");
250            runner.download(&format!("{remote_dir}/raw"), local_raw.to_str().unwrap())?;
251
252            if let Ok(data) = crate::spectre::parsers::parse_psf_ascii(&local_raw) {
253                sim_result.data = data;
254            }
255        }
256
257        if !self.keep_remote_files {
258            runner.run_command(&format!("rm -rf {remote_dir}"), None)?;
259        }
260
261        Ok(sim_result)
262    }
263}