Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Terminate process on timeout in windows for the coverage task #3529

Merged
merged 7 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/agent/coverage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ symbolic = { version = "12.3", features = [
"symcache",
] }
thiserror = "1.0"
process_control = "4.0"

[target.'cfg(target_os = "windows")'.dependencies]
debugger = { path = "../debugger" }
Expand Down
82 changes: 61 additions & 21 deletions src/agent/coverage/src/record.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::process::{Command, ExitStatus, Stdio};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::Duration;

Expand Down Expand Up @@ -120,32 +120,59 @@ impl CoverageRecorder {

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

let child = Debugger::create_child(self.cmd)?;

// Spawn a thread to wait for the target process to exit.
let taget_process = std::thread::spawn(move || {
let output = child
.controlled_with_output()
.time_limit(self.timeout)
.terminate_for_timeout()
.wait();
output
});

let loader = self.loader.clone();
let mut recorder =
WindowsRecorder::new(&loader, self.module_allowlist, self.cache.as_ref());

crate::timer::timed(self.timeout, move || {
let mut recorder =
WindowsRecorder::new(&loader, self.module_allowlist, self.cache.as_ref());
let (mut dbg, child) = Debugger::init(self.cmd, &mut recorder)?;
dbg.run(&mut recorder)?;

// If the debugger callbacks fail, this may return with a spurious clean exit.
let output = child.wait_with_output()?.into();

// Check if debugging was stopped due to a callback error.
//
// If so, the debugger terminated the target, and the recorded coverage and
// output are both invalid.
if let Some(err) = recorder.stop_error {
return Err(err);
// the debugger is initialized in the same thread that created the target process to be able to receive the debug events
let mut dbg = Debugger::init_debugger(&mut recorder)?;
dbg.run(&mut recorder)?;
tevoinea marked this conversation as resolved.
Show resolved Hide resolved

let output = match taget_process.join() {
Err(err) => {
bail!("failed to launch target thread: {:?}", err)
}
Ok(Err(err)) => {
bail!("failed to launch target process: {:?}", err)
}
Ok(Ok(None)) => {
bail!(crate::timer::TimerError::Timeout(self.timeout))
}
Ok(Ok(Some(output))) => output,
};

// If the debugger callbacks fail, this may return with a spurious clean exit.

let coverage = recorder.coverage;
// Check if debugging was stopped due to a callback error.
//
// If so, the debugger terminated the target, and the recorded coverage and
// output are both invalid.
if let Some(err) = recorder.stop_error {
return Err(err);
}

Ok(Recorded { coverage, output })
})?
let coverage = recorder.coverage;
Ok(Recorded {
coverage,
output: output.into(),
})
}
}

Expand All @@ -157,19 +184,32 @@ pub struct Recorded {

#[derive(Clone, Debug, Default)]
pub struct Output {
pub status: Option<ExitStatus>,
pub status: Option<process_control::ExitStatus>,
pub stderr: String,
pub stdout: String,
}

impl From<process_control::Output> for Output {
fn from(output: process_control::Output) -> Self {
let status = Some(output.status);
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Self {
status,
stdout,
stderr,
}
}
}

impl From<std::process::Output> for Output {
fn from(output: std::process::Output) -> Self {
let status = Some(output.status);
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();

Self {
status,
status: status.map(Into::into),
stdout,
stderr,
}
Expand Down
27 changes: 17 additions & 10 deletions src/agent/debugger/src/debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,7 @@ pub struct Debugger {
}

impl Debugger {
pub fn init(
mut command: Command,
callbacks: &mut impl DebugEventHandler,
) -> Result<(Self, Child)> {
let child = command
.creation_flags(DEBUG_ONLY_THIS_PROCESS.0)
.spawn()
.context("debugee failed to start")?;

pub fn init_debugger(callbacks: &mut impl DebugEventHandler) -> Result<Self> {
unsafe { DebugSetProcessKillOnExit(TRUE) }
.ok()
.context("Setting DebugSetProcessKillOnExit to TRUE")?;
Expand Down Expand Up @@ -186,12 +178,27 @@ impl Debugger {
return Err(last_os_error());
}

Ok((debugger, child))
Ok(debugger)
} else {
anyhow::bail!("Unexpected event: {}", de)
}
}

pub fn create_child(mut command: Command) -> Result<Child> {
let child = command
.creation_flags(DEBUG_ONLY_THIS_PROCESS.0)
.spawn()
.context("debugee failed to start")?;

Ok(child)
}

pub fn init(command: Command, callbacks: &mut impl DebugEventHandler) -> Result<(Self, Child)> {
let child = Self::create_child(command)?;
let debugger = Self::init_debugger(callbacks)?;
Ok((debugger, child))
}

pub fn target(&mut self) -> &mut Target {
&mut self.target
}
Expand Down
Loading