virtuoso_cli/spectre/
runner.rs1use 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}