Skip to content

Commit 1d94040

Browse files
committed
feat(runner): improve error handling for valgrind
1 parent d6b498f commit 1d94040

File tree

1 file changed

+118
-4
lines changed

1 file changed

+118
-4
lines changed

src/run/runner/valgrind/measure.rs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ use crate::run::{config::Config, instruments::mongo_tracer::MongoTracer};
99
use lazy_static::lazy_static;
1010
use std::env;
1111
use std::fs::canonicalize;
12+
use std::io::Write;
13+
use std::os::unix::fs::PermissionsExt;
14+
use std::os::unix::process::ExitStatusExt;
1215
use std::path::Path;
1316
use std::{env::consts::ARCH, process::Command};
17+
use tempfile::TempPath;
1418

1519
lazy_static! {
1620
static ref VALGRIND_BASE_ARGS: Vec<String> = {
@@ -42,6 +46,63 @@ lazy_static! {
4246
};
4347
}
4448

49+
/// Creates the command that executes the specified command safely.
50+
///
51+
/// # Returns
52+
///
53+
/// A tuple containing:
54+
/// 1. The command to execute
55+
/// 2. The path to the wrapper shell script file
56+
/// 3. The path to the file containing the exit code
57+
fn safe_run_cmd(to_execute: &str) -> anyhow::Result<(String, TempPath, TempPath)> {
58+
const WRAPPER_SCRIPT: &str = r#"
59+
#!/bin/sh
60+
61+
safe_run()
62+
{
63+
(eval $1)
64+
status=$?
65+
echo -n "$status" > "$2"
66+
}
67+
68+
# Args:
69+
# 1. Cmd that should be executed
70+
# 2. File to write the status code to
71+
if [ "$#" -ne 2 ]; then
72+
echo "Usage: $0 <cmd> <file>"
73+
exit 1
74+
fi
75+
76+
safe_run "$1" "$2"
77+
"#;
78+
79+
// The command is wrapped in a shell script, which executes it in a
80+
// subprocess and then writes the exit code to a file. The process will
81+
// always exit with status code 0, unless valgrind fails.
82+
let script_path = {
83+
let rwx = std::fs::Permissions::from_mode(0o777);
84+
let mut script_file = tempfile::Builder::new()
85+
.suffix(".sh")
86+
.permissions(rwx)
87+
.tempfile()?;
88+
script_file.write_all(WRAPPER_SCRIPT.as_bytes())?;
89+
90+
// Note: We have to convert the fiel to a path to be able to execute it.
91+
// Otherwise this will fail with 'File is busy' error.
92+
script_file.into_temp_path()
93+
};
94+
95+
let cmd_status_path = tempfile::NamedTempFile::new().unwrap().into_temp_path();
96+
let run_cmd = format!(
97+
"{} \"{}\" \"{}\"",
98+
script_path.to_str().unwrap(),
99+
to_execute,
100+
cmd_status_path.to_str().unwrap()
101+
);
102+
103+
Ok((run_cmd, script_path, cmd_status_path))
104+
}
105+
45106
pub fn measure(
46107
config: &Config,
47108
profile_folder: &Path,
@@ -84,9 +145,9 @@ pub fn measure(
84145
)
85146
.arg(format!("--callgrind-out-file={}", profile_path.to_str().unwrap()).as_str())
86147
.arg(format!("--log-file={}", log_path.to_str().unwrap()).as_str());
87-
88-
// Set the command to execute
89-
cmd.args(["sh", "-c", get_bench_command(config)?.as_str()]);
148+
// Set the command to execute:
149+
let (run_cmd, _script, cmd_status_file) = safe_run_cmd(get_bench_command(config)?.as_str())?;
150+
cmd.args(["sh", "-c", &run_cmd]);
90151

91152
// TODO: refactor and move this to the `Instruments` struct
92153
if let Some(mongo_tracer) = mongo_tracer {
@@ -96,9 +157,62 @@ pub fn measure(
96157
debug!("cmd: {:?}", cmd);
97158
let status = run_command_with_log_pipe(cmd)
98159
.map_err(|e| anyhow!("failed to execute the benchmark process. {}", e))?;
160+
let cmd_status = {
161+
let content = std::fs::read_to_string(&cmd_status_file)?;
162+
content.parse::<u32>()?
163+
};
164+
165+
debug!(
166+
"Valgrind exit code = {:?}, Valgrind signal = {:?}, Program exit code = {}",
167+
status.code(),
168+
status.signal(),
169+
cmd_status
170+
);
171+
172+
// Check the valgrind exit code
99173
if !status.success() {
100-
bail!("failed to execute the benchmark process");
174+
let valgrind_log = profile_folder.join("valgrind.log");
175+
let valgrind_log = std::fs::read_to_string(&valgrind_log).unwrap_or_default();
176+
debug!("valgrind.log: {}", valgrind_log);
177+
178+
bail!("failed to execute valgrind");
179+
}
180+
181+
// Check the exit code which was written to the file by the wrapper script.
182+
if cmd_status != 0 {
183+
bail!(
184+
"failed to execute the benchmark process, exit code: {}",
185+
cmd_status
186+
);
101187
}
102188

103189
Ok(())
104190
}
191+
192+
#[cfg(test)]
193+
mod tests {
194+
use super::*;
195+
196+
fn safe_run(to_execute: &str) -> String {
197+
let (run_cmd, _script, out_status) = safe_run_cmd(to_execute).unwrap();
198+
199+
let mut cmd = Command::new("sh");
200+
cmd.args(["-c", &run_cmd]);
201+
202+
assert!(cmd.output().unwrap().status.success());
203+
204+
std::fs::read_to_string(out_status).unwrap()
205+
}
206+
207+
#[test]
208+
fn test_run_wrapper_script() {
209+
let output = safe_run("ls");
210+
assert_eq!(output, "0");
211+
212+
let output = safe_run("exit 1");
213+
assert_eq!(output, "1");
214+
215+
let output = safe_run("exit 42");
216+
assert_eq!(output, "42");
217+
}
218+
}

0 commit comments

Comments
 (0)