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 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 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 runner.run_command(&format!("mkdir -p {remote_dir}"), None)?;
168 runner.upload_text(netlist, &format!("{remote_dir}/input.scs"))?;
169
170 let extra = if self.spectre_args.is_empty() {
172 String::new()
173 } else {
174 format!(" {}", self.spectre_args.join(" "))
175 };
176 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 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}