Skip to content

Commit 041d69d

Browse files
committed
Terminate process on timeout in windows for the coverage task (microsoft#3529)
* Terminate process on timeout in windows for the coverage task * set the timeout before we start the debugger * split the target launch from the debugger initialization wait for the process to finish on a separate thread * fix build * move comments
1 parent 2187934 commit 041d69d

File tree

5 files changed

+80
-31
lines changed

5 files changed

+80
-31
lines changed

src/agent/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/agent/coverage/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ symbolic = { version = "12.3", features = [
2020
"symcache",
2121
] }
2222
thiserror = "1.0"
23+
process_control = "4.0"
2324

2425
[target.'cfg(target_os = "windows")'.dependencies]
2526
debugger = { path = "../debugger" }

src/agent/coverage/src/record.rs

+60-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use std::process::{Command, ExitStatus, Stdio};
4+
use std::process::{Command, Stdio};
55
use std::sync::Arc;
66
use std::time::Duration;
77

@@ -120,32 +120,58 @@ impl CoverageRecorder {
120120

121121
#[cfg(target_os = "windows")]
122122
pub fn record(self) -> Result<Recorded> {
123+
use anyhow::bail;
123124
use debugger::Debugger;
125+
use process_control::{ChildExt, Control};
124126
use windows::WindowsRecorder;
125127

128+
let child = Debugger::create_child(self.cmd)?;
129+
130+
// Spawn a thread to wait for the target process to exit.
131+
let taget_process = std::thread::spawn(move || {
132+
let output = child
133+
.controlled_with_output()
134+
.time_limit(self.timeout)
135+
.terminate_for_timeout()
136+
.wait();
137+
output
138+
});
139+
126140
let loader = self.loader.clone();
141+
let mut recorder =
142+
WindowsRecorder::new(&loader, self.module_allowlist, self.cache.as_ref());
127143

128-
crate::timer::timed(self.timeout, move || {
129-
let mut recorder =
130-
WindowsRecorder::new(&loader, self.module_allowlist, self.cache.as_ref());
131-
let (mut dbg, child) = Debugger::init(self.cmd, &mut recorder)?;
132-
dbg.run(&mut recorder)?;
133-
134-
// If the debugger callbacks fail, this may return with a spurious clean exit.
135-
let output = child.wait_with_output()?.into();
136-
137-
// Check if debugging was stopped due to a callback error.
138-
//
139-
// If so, the debugger terminated the target, and the recorded coverage and
140-
// output are both invalid.
141-
if let Some(err) = recorder.stop_error {
142-
return Err(err);
144+
// The debugger is initialized in the same thread that created the target process to be able to receive the debug events
145+
let mut dbg = Debugger::init_debugger(&mut recorder)?;
146+
dbg.run(&mut recorder)?;
147+
148+
// If the debugger callbacks fail, this may return with a spurious clean exit.
149+
let output = match taget_process.join() {
150+
Err(err) => {
151+
bail!("failed to launch target thread: {:?}", err)
152+
}
153+
Ok(Err(err)) => {
154+
bail!("failed to launch target process: {:?}", err)
143155
}
156+
Ok(Ok(None)) => {
157+
bail!(crate::timer::TimerError::Timeout(self.timeout))
158+
}
159+
Ok(Ok(Some(output))) => output,
160+
};
144161

145-
let coverage = recorder.coverage;
162+
// Check if debugging was stopped due to a callback error.
163+
//
164+
// If so, the debugger terminated the target, and the recorded coverage and
165+
// output are both invalid.
166+
if let Some(err) = recorder.stop_error {
167+
return Err(err);
168+
}
146169

147-
Ok(Recorded { coverage, output })
148-
})?
170+
let coverage = recorder.coverage;
171+
Ok(Recorded {
172+
coverage,
173+
output: output.into(),
174+
})
149175
}
150176
}
151177

@@ -157,19 +183,32 @@ pub struct Recorded {
157183

158184
#[derive(Clone, Debug, Default)]
159185
pub struct Output {
160-
pub status: Option<ExitStatus>,
186+
pub status: Option<process_control::ExitStatus>,
161187
pub stderr: String,
162188
pub stdout: String,
163189
}
164190

191+
impl From<process_control::Output> for Output {
192+
fn from(output: process_control::Output) -> Self {
193+
let status = Some(output.status);
194+
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
195+
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
196+
Self {
197+
status,
198+
stdout,
199+
stderr,
200+
}
201+
}
202+
}
203+
165204
impl From<std::process::Output> for Output {
166205
fn from(output: std::process::Output) -> Self {
167206
let status = Some(output.status);
168207
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
169208
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
170209

171210
Self {
172-
status,
211+
status: status.map(Into::into),
173212
stdout,
174213
stderr,
175214
}

src/agent/coverage/src/timer.rs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::time::Duration;
77

88
use thiserror::Error;
99

10+
#[allow(dead_code)]
1011
pub fn timed<F, T>(timeout: Duration, function: F) -> Result<T, TimerError>
1112
where
1213
T: Send + 'static,

src/agent/debugger/src/debugger.rs

+17-10
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,7 @@ pub struct Debugger {
134134
}
135135

136136
impl Debugger {
137-
pub fn init(
138-
mut command: Command,
139-
callbacks: &mut impl DebugEventHandler,
140-
) -> Result<(Self, Child)> {
141-
let child = command
142-
.creation_flags(DEBUG_ONLY_THIS_PROCESS.0)
143-
.spawn()
144-
.context("debugee failed to start")?;
145-
137+
pub fn init_debugger(callbacks: &mut impl DebugEventHandler) -> Result<Self> {
146138
unsafe { DebugSetProcessKillOnExit(TRUE) }
147139
.ok()
148140
.context("Setting DebugSetProcessKillOnExit to TRUE")?;
@@ -186,12 +178,27 @@ impl Debugger {
186178
return Err(last_os_error());
187179
}
188180

189-
Ok((debugger, child))
181+
Ok(debugger)
190182
} else {
191183
anyhow::bail!("Unexpected event: {}", de)
192184
}
193185
}
194186

187+
pub fn create_child(mut command: Command) -> Result<Child> {
188+
let child = command
189+
.creation_flags(DEBUG_ONLY_THIS_PROCESS.0)
190+
.spawn()
191+
.context("debugee failed to start")?;
192+
193+
Ok(child)
194+
}
195+
196+
pub fn init(command: Command, callbacks: &mut impl DebugEventHandler) -> Result<(Self, Child)> {
197+
let child = Self::create_child(command)?;
198+
let debugger = Self::init_debugger(callbacks)?;
199+
Ok((debugger, child))
200+
}
201+
195202
pub fn target(&mut self) -> &mut Target {
196203
&mut self.target
197204
}

0 commit comments

Comments
 (0)