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