(pid: i32, seconds: u64)
| 395 | } |
| 396 | |
| 397 | pub async fn sample_stack(pid: i32, seconds: u64) -> Result<StackSampleReport, AppError> { |
| 398 | if pid <= 0 { |
| 399 | return Err(AppError::bad_request("Process id must be positive.")); |
| 400 | } |
| 401 | let seconds = seconds.clamp(1, 30); |
| 402 | let sampled_at = now_ms(); |
| 403 | let report_path = std::env::temp_dir().join(format!( |
| 404 | "simdeck-sample-{pid}-{}-{}.txt", |
| 405 | std::process::id(), |
| 406 | sampled_at |
| 407 | )); |
| 408 | let result = timeout( |
| 409 | Duration::from_secs(seconds + 20), |
| 410 | Command::new("sample") |
| 411 | .arg(pid.to_string()) |
| 412 | .arg(seconds.to_string()) |
| 413 | .arg("-file") |
| 414 | .arg(&report_path) |
| 415 | .output(), |
| 416 | ) |
| 417 | .await |
| 418 | .map_err(|_| AppError::native("Timed out sampling process stack."))? |
| 419 | .map_err(|error| AppError::native(format!("Unable to run sample: {error}")))?; |
| 420 | |
| 421 | let stderr = String::from_utf8_lossy(&result.stderr).trim().to_owned(); |
| 422 | let mut report = tokio::fs::read(&report_path).await.unwrap_or_default(); |
| 423 | let _ = tokio::fs::remove_file(&report_path).await; |
| 424 | let truncated = report.len() > STACK_SAMPLE_MAX_BYTES; |
| 425 | if truncated { |
| 426 | report.truncate(STACK_SAMPLE_MAX_BYTES); |
| 427 | } |
| 428 | |
| 429 | if !result.status.success() { |
| 430 | return Err(AppError::native(if stderr.is_empty() { |
| 431 | format!("sample exited with status {}", result.status) |
| 432 | } else { |
| 433 | stderr |
| 434 | })); |
| 435 | } |
| 436 | |
| 437 | Ok(StackSampleReport { |
| 438 | pid, |
| 439 | seconds, |
| 440 | sampled_at, |
| 441 | report: String::from_utf8_lossy(&report).into_owned(), |
| 442 | stderr, |
| 443 | truncated, |
| 444 | }) |
| 445 | } |
| 446 | |
| 447 | async fn sample_process( |
| 448 | process: PerformanceProcess, |
no test coverage detected