rust: modes module cleanup

This commit is contained in:
Laszlo Nagy
2024-12-14 21:01:34 +11:00
parent ff7ae7ad7e
commit 6a2b46d4a4
10 changed files with 364 additions and 359 deletions

View File

@@ -1,6 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use bear::modes::{Combined, Intercept, Mode, Semantic};
use bear::modes::Mode;
use bear::modes::intercept::Intercept;
use bear::modes::semantic::Semantic;
use bear::modes::combined::Combined;
use bear::{args, config};
use std::env;
use std::process::ExitCode;
@@ -68,12 +71,9 @@ impl Application {
Application::Semantic(semantic) => semantic.run(),
Application::Combined(all) => all.run(),
};
match status {
Ok(code) => code,
Err(error) => {
log::error!("Run failed: {}", error);
ExitCode::FAILURE
}
}
status.unwrap_or_else(|error| {
log::error!("Bear: {}", error);
ExitCode::FAILURE
})
}
}

View File

@@ -21,7 +21,7 @@ use anyhow::{Context, Result};
use bear::ipc::tcp::ReporterOnTcp;
use bear::ipc::Reporter;
use bear::ipc::{Event, Execution, ProcessId};
use bear::modes::KEY_DESTINATION;
use bear::modes::intercept::KEY_DESTINATION;
use std::path::{Path, PathBuf};
/// Implementation of the wrapper process.

View File

@@ -11,6 +11,7 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::mpsc::Sender;
@@ -81,6 +82,17 @@ pub struct Execution {
pub environment: HashMap<String, String>,
}
impl fmt::Display for Execution {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Execution path={}, args=[{}]",
self.executable.display(),
self.arguments.join(",")
)
}
}
/// Reporter id is a unique identifier for a reporter.
///
/// It is used to identify the process that sends the execution report.

View File

@@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::ipc::Envelope;
use crate::modes::intercept::{CollectorService, InterceptEnvironment};
use crate::modes::semantic::Recognition;
use crate::modes::Mode;
use crate::output::OutputWriter;
use crate::semantic::transformation::Transformation;
use crate::semantic::Transform;
use crate::{args, config};
use anyhow::Context;
use std::process::ExitCode;
/// The all model is combining the intercept and semantic modes.
pub struct Combined {
command: args::BuildCommand,
intercept_config: config::Intercept,
semantic_recognition: Recognition,
semantic_transform: Transformation,
output_writer: OutputWriter,
}
impl Combined {
/// Create a new all mode instance.
pub fn from(
command: args::BuildCommand,
output: args::BuildSemantic,
config: config::Main,
) -> anyhow::Result<Self> {
let semantic_recognition = Recognition::try_from(&config)?;
let semantic_transform = Transformation::from(&config.output);
let output_writer = OutputWriter::configure(&output, &config.output)?;
let intercept_config = config.intercept;
Ok(Self {
command,
intercept_config,
semantic_recognition,
semantic_transform,
output_writer,
})
}
/// Consumer the envelopes for analysis and write the result to the output file.
/// This implements the pipeline of the semantic analysis. Same as the `Semantic` mode.
fn consume_for_analysis(
semantic_recognition: Recognition,
semantic_transform: Transformation,
output_writer: OutputWriter,
envelopes: impl IntoIterator<Item = Envelope>,
) -> anyhow::Result<()> {
let entries = envelopes
.into_iter()
.map(|envelope| envelope.event.execution)
.flat_map(|execution| semantic_recognition.apply(execution))
.flat_map(|semantic| semantic_transform.apply(semantic));
output_writer.run(entries)
}
}
impl Mode for Combined {
/// Run the all mode by setting up the collector service and the intercept environment.
/// The build command is executed in the intercept environment. The collected events are
/// then processed by the semantic recognition and transformation. The result is written
/// to the output file.
///
/// The exit code is based on the result of the build command.
fn run(self) -> anyhow::Result<ExitCode> {
let semantic_recognition = self.semantic_recognition;
let semantic_transform = self.semantic_transform;
let output_writer = self.output_writer;
let service = CollectorService::new(move |envelopes| {
Self::consume_for_analysis(
semantic_recognition,
semantic_transform,
output_writer,
envelopes,
)
})
.with_context(|| "Failed to create the ipc service")?;
let environment = InterceptEnvironment::new(&self.intercept_config, service.address())
.with_context(|| "Failed to create the ipc environment")?;
let status = environment
.execute_build_command(self.command)
.with_context(|| "Failed to execute the build command")?;
Ok(status)
}
}

View File

@@ -1,54 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use anyhow::Context;
use serde_json::de::IoRead;
use serde_json::{Error, StreamDeserializer};
use std::fs::OpenOptions;
use std::io::BufReader;
use std::path::PathBuf;
use crate::args;
use crate::ipc::{Envelope, Execution};
/// Responsible for reading the build events from the intercept mode.
///
/// The file syntax is defined by the `events` module, and the parsing logic is implemented there.
/// Here we only handle the file opening and the error handling.
pub struct EventFileReader {
stream: Box<dyn Iterator<Item = Result<Envelope, Error>>>,
}
impl TryFrom<args::BuildEvents> for EventFileReader {
type Error = anyhow::Error;
/// Open the file and create a new instance of the event file reader.
///
/// If the file cannot be opened, the error will be logged and escalated.
fn try_from(value: args::BuildEvents) -> Result<Self, Self::Error> {
let file_name = PathBuf::from(value.file_name);
let file = OpenOptions::new()
.read(true)
.open(file_name.as_path())
.map(BufReader::new)
.with_context(|| format!("Failed to open input file: {:?}", file_name))?;
let stream = Box::new(StreamDeserializer::new(IoRead::new(file)));
Ok(EventFileReader { stream })
}
}
impl EventFileReader {
/// Generate the build events from the file.
///
/// Returns an iterator over the build events. Any error during the reading
/// of the file will be logged and the failed entries will be skipped.
pub fn generate(self) -> impl Iterator<Item = Execution> {
self.stream.filter_map(|result| match result {
Ok(value) => Some(value.event.execution),
Err(error) => {
log::error!("Failed to read event: {:?}", error);
None
}
})
}
}

View File

@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use super::{KEY_DESTINATION, KEY_PRELOAD_PATH};
use super::Mode;
use crate::ipc::tcp::CollectorOnTcp;
use crate::ipc::{Collector, Envelope};
use crate::{args, config};
use anyhow::Context;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::sync::mpsc::channel;
@@ -11,6 +13,73 @@ use std::sync::mpsc::Receiver;
use std::sync::Arc;
use std::{env, thread};
/// Declare the environment variables used by the intercept mode.
pub const KEY_DESTINATION: &str = "INTERCEPT_REPORTER_ADDRESS";
pub const KEY_PRELOAD_PATH: &str = "LD_PRELOAD";
/// The intercept mode we are only capturing the build commands
/// and write it into the output file.
pub struct Intercept {
command: args::BuildCommand,
output: args::BuildEvents,
config: config::Intercept,
}
impl Intercept {
/// Create a new intercept mode instance.
pub fn from(
command: args::BuildCommand,
output: args::BuildEvents,
config: config::Main,
) -> anyhow::Result<Self> {
Ok(Self {
command,
output,
config: config.intercept,
})
}
/// Consume events and write them into the output file.
fn write_to_file(
output_file_name: String,
envelopes: impl IntoIterator<Item = Envelope>,
) -> anyhow::Result<()> {
let mut writer = std::fs::File::create(&output_file_name)
.map(BufWriter::new)
.with_context(|| format!("Failed to create output file: {:?}", &output_file_name))?;
for envelope in envelopes {
serde_json::to_writer(&mut writer, &envelope).with_context(|| {
format!("Failed to write execution report: {:?}", &output_file_name)
})?;
// TODO: add a newline character to separate the entries
}
Ok(())
}
}
impl Mode for Intercept {
/// Run the intercept mode by setting up the collector service and
/// the intercept environment. The build command is executed in the
/// intercept environment.
///
/// The exit code is based on the result of the build command.
fn run(self) -> anyhow::Result<ExitCode> {
let output_file_name = self.output.file_name.clone();
let service = CollectorService::new(move |envelopes| {
Self::write_to_file(output_file_name, envelopes)
})
.with_context(|| "Failed to create the ipc service")?;
let environment = InterceptEnvironment::new(&self.config, service.address())
.with_context(|| "Failed to create the ipc environment")?;
let status = environment
.execute_build_command(self.command)
.with_context(|| "Failed to execute the build command")?;
Ok(status)
}
}
/// The service is responsible for collecting the events from the supervised processes.
///
/// The service is implemented as TCP server that listens on a random port on the loopback

View File

@@ -1,220 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
mod input;
mod intercept;
mod recognition;
pub mod intercept;
pub mod semantic;
pub mod combined;
use crate::ipc::Envelope;
use crate::output::OutputWriter;
use crate::{args, config};
use anyhow::Context;
use input::EventFileReader;
use intercept::{CollectorService, InterceptEnvironment};
use recognition::Recognition;
use std::io::BufWriter;
use std::process::ExitCode;
use crate::semantic::Transform;
use crate::semantic::transformation::Transformation;
/// Declare the environment variables used by the intercept mode.
pub const KEY_DESTINATION: &str = "INTERCEPT_REPORTER_ADDRESS";
pub const KEY_PRELOAD_PATH: &str = "LD_PRELOAD";
/// The mode trait is used to run the application in different modes.
pub trait Mode {
fn run(self) -> anyhow::Result<ExitCode>;
}
/// The intercept mode we are only capturing the build commands
/// and write it into the output file.
pub struct Intercept {
command: args::BuildCommand,
output: args::BuildEvents,
config: config::Intercept,
}
impl Intercept {
/// Create a new intercept mode instance.
pub fn from(
command: args::BuildCommand,
output: args::BuildEvents,
config: config::Main,
) -> anyhow::Result<Self> {
Ok(Self {
command,
output,
config: config.intercept,
})
}
/// Consume events and write them into the output file.
fn write_to_file(
output_file_name: String,
envelopes: impl IntoIterator<Item = Envelope>,
) -> anyhow::Result<()> {
let mut writer = std::fs::File::create(&output_file_name)
.map(BufWriter::new)
.with_context(|| format!("Failed to create output file: {:?}", &output_file_name))?;
for envelope in envelopes {
serde_json::to_writer(&mut writer, &envelope).with_context(|| {
format!("Failed to write execution report: {:?}", &output_file_name)
})?;
// TODO: add a newline character to separate the entries
}
Ok(())
}
}
impl Mode for Intercept {
/// Run the intercept mode by setting up the collector service and
/// the intercept environment. The build command is executed in the
/// intercept environment.
///
/// The exit code is based on the result of the build command.
fn run(self) -> anyhow::Result<ExitCode> {
let output_file_name = self.output.file_name.clone();
let service = CollectorService::new(move |envelopes| {
Self::write_to_file(output_file_name, envelopes)
})
.with_context(|| "Failed to create the ipc service")?;
let environment = InterceptEnvironment::new(&self.config, service.address())
.with_context(|| "Failed to create the ipc environment")?;
let status = environment
.execute_build_command(self.command)
.with_context(|| "Failed to execute the build command")?;
Ok(status)
}
}
/// The semantic mode we are deduct the semantic meaning of the
/// executed commands from the build process.
pub struct Semantic {
event_source: EventFileReader,
semantic_recognition: Recognition,
semantic_transform: Transformation,
output_writer: OutputWriter,
}
impl Semantic {
/// Create a new semantic mode instance.
pub fn from(
input: args::BuildEvents,
output: args::BuildSemantic,
config: config::Main,
) -> anyhow::Result<Self> {
let event_source = EventFileReader::try_from(input)?;
let semantic_recognition = Recognition::try_from(&config)?;
let semantic_transform = Transformation::from(&config.output);
let output_writer = OutputWriter::configure(&output, &config.output)?;
Ok(Self {
event_source,
semantic_recognition,
semantic_transform,
output_writer,
})
}
}
impl Mode for Semantic {
/// Run the semantic mode by generating the compilation database entries
/// from the event source. The entries are then processed by the semantic
/// recognition and transformation. The result is written to the output file.
///
/// The exit code is based on the result of the output writer.
fn run(self) -> anyhow::Result<ExitCode> {
// Set up the pipeline of compilation database entries.
let entries = self
.event_source
.generate()
.flat_map(|execution| self.semantic_recognition.apply(execution))
.flat_map(|semantic| self.semantic_transform.apply(semantic));
// Consume the entries and write them to the output file.
// The exit code is based on the result of the output writer.
match self.output_writer.run(entries) {
Ok(_) => Ok(ExitCode::SUCCESS),
Err(_) => Ok(ExitCode::FAILURE),
}
}
}
/// The all model is combining the intercept and semantic modes.
pub struct Combined {
command: args::BuildCommand,
intercept_config: config::Intercept,
semantic_recognition: Recognition,
semantic_transform: Transformation,
output_writer: OutputWriter,
}
impl Combined {
/// Create a new all mode instance.
pub fn from(
command: args::BuildCommand,
output: args::BuildSemantic,
config: config::Main,
) -> anyhow::Result<Self> {
let semantic_recognition = Recognition::try_from(&config)?;
let semantic_transform = Transformation::from(&config.output);
let output_writer = OutputWriter::configure(&output, &config.output)?;
let intercept_config = config.intercept;
Ok(Self {
command,
intercept_config,
semantic_recognition,
semantic_transform,
output_writer,
})
}
/// Consumer the envelopes for analysis and write the result to the output file.
/// This implements the pipeline of the semantic analysis. Same as the `Semantic` mode.
fn consume_for_analysis(
semantic_recognition: Recognition,
semantic_transform: Transformation,
output_writer: OutputWriter,
envelopes: impl IntoIterator<Item = Envelope>,
) -> anyhow::Result<()> {
let entries = envelopes
.into_iter()
.map(|envelope| envelope.event.execution)
.flat_map(|execution| semantic_recognition.apply(execution))
.flat_map(|semantic| semantic_transform.apply(semantic));
output_writer.run(entries)
}
}
impl Mode for Combined {
/// Run the all mode by setting up the collector service and the intercept environment.
/// The build command is executed in the intercept environment. The collected events are
/// then processed by the semantic recognition and transformation. The result is written
/// to the output file.
///
/// The exit code is based on the result of the build command.
fn run(self) -> anyhow::Result<ExitCode> {
let semantic_recognition = self.semantic_recognition;
let semantic_transform = self.semantic_transform;
let output_writer = self.output_writer;
let service = CollectorService::new(move |envelopes| {
Self::consume_for_analysis(
semantic_recognition,
semantic_transform,
output_writer,
envelopes,
)
})
.with_context(|| "Failed to create the ipc service")?;
let environment = InterceptEnvironment::new(&self.intercept_config, service.address())
.with_context(|| "Failed to create the ipc environment")?;
let status = environment
.execute_build_command(self.command)
.with_context(|| "Failed to execute the build command")?;
Ok(status)
}
}

View File

@@ -1,83 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//! Responsible for recognizing the semantic meaning of the executed commands.
//!
//! The recognition logic is implemented in the `interpreters` module.
//! Here we only handle the errors and logging them to the console.
use super::super::ipc;
use super::super::semantic;
use super::config;
use std::convert::TryFrom;
pub struct Recognition {
interpreter: Box<dyn semantic::Interpreter>,
}
impl TryFrom<&config::Main> for Recognition {
type Error = anyhow::Error;
/// Creates an interpreter to recognize the compiler calls.
///
/// Using the configuration we can define which compilers to include and exclude.
/// Also read the environment variables to detect the compiler to include (and
/// make sure those are not excluded either).
// TODO: Use the CC or CXX environment variables to detect the compiler to include.
// Use the CC or CXX environment variables and make sure those are not excluded.
// Make sure the environment variables are passed to the method.
fn try_from(config: &config::Main) -> Result<Self, Self::Error> {
let compilers_to_include = match &config.intercept {
config::Intercept::Wrapper { executables, .. } => executables.clone(),
_ => vec![],
};
let compilers_to_exclude = match &config.output {
config::Output::Clang { compilers, .. } => compilers
.iter()
.filter(|compiler| compiler.ignore == config::Ignore::Always)
.map(|compiler| compiler.path.clone())
.collect(),
_ => vec![],
};
let interpreter = semantic::interpreters::Builder::new()
.compilers_to_recognize(compilers_to_include.as_slice())
.compilers_to_exclude(compilers_to_exclude.as_slice())
.build();
Ok(Recognition {
interpreter: Box::new(interpreter),
})
}
}
impl Recognition {
/// Simple call the semantic module to recognize the execution.
/// Forward only the compiler calls, and log each recognition result.
pub fn apply(&self, execution: ipc::Execution) -> Option<semantic::CompilerCall> {
match self.interpreter.recognize(&execution) {
semantic::Recognition::Success(semantic) => {
log::debug!(
"execution recognized as compiler call, {:?} : {:?}",
semantic,
execution
);
Some(semantic)
}
semantic::Recognition::Ignored => {
log::debug!("execution recognized, but ignored: {:?}", execution);
None
}
semantic::Recognition::Error(reason) => {
log::debug!(
"execution recognized with failure, {:?} : {:?}",
reason,
execution
);
None
}
semantic::Recognition::Unknown => {
log::debug!("execution not recognized: {:?}", execution);
None
}
}
}
}

View File

@@ -0,0 +1,167 @@
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::modes::Mode;
use crate::output::OutputWriter;
use crate::semantic::transformation::Transformation;
use crate::semantic::Transform;
use crate::{args, config};
use super::super::ipc;
use super::super::semantic;
use crate::ipc::{Envelope, Execution};
use std::process::ExitCode;
use serde_json::de::IoRead;
use serde_json::{Error, StreamDeserializer};
use std::convert::TryFrom;
use std::fs::OpenOptions;
use std::io::BufReader;
use std::path::PathBuf;
use anyhow::Context;
/// The semantic mode we are deduct the semantic meaning of the
/// executed commands from the build process.
pub struct Semantic {
event_source: EventFileReader,
semantic_recognition: Recognition,
semantic_transform: Transformation,
output_writer: OutputWriter,
}
impl Semantic {
/// Create a new semantic mode instance.
pub fn from(
input: args::BuildEvents,
output: args::BuildSemantic,
config: config::Main,
) -> anyhow::Result<Self> {
let event_source = EventFileReader::try_from(input)?;
let semantic_recognition = Recognition::try_from(&config)?;
let semantic_transform = Transformation::from(&config.output);
let output_writer = OutputWriter::configure(&output, &config.output)?;
Ok(Self {
event_source,
semantic_recognition,
semantic_transform,
output_writer,
})
}
}
impl Mode for Semantic {
/// Run the semantic mode by generating the compilation database entries
/// from the event source. The entries are then processed by the semantic
/// recognition and transformation. The result is written to the output file.
///
/// The exit code is based on the result of the output writer.
fn run(self) -> anyhow::Result<ExitCode> {
// Set up the pipeline of compilation database entries.
let entries = self
.event_source
.generate()
.inspect(|execution| log::debug!("execution: {}", execution))
.flat_map(|execution| self.semantic_recognition.apply(execution))
.inspect(|semantic| log::debug!("semantic: {:?}", semantic))
.flat_map(|semantic| self.semantic_transform.apply(semantic));
// Consume the entries and write them to the output file.
// The exit code is based on the result of the output writer.
match self.output_writer.run(entries) {
Ok(_) => Ok(ExitCode::SUCCESS),
Err(_) => Ok(ExitCode::FAILURE),
}
}
}
/// Responsible for recognizing the semantic meaning of the executed commands.
///
/// The recognition logic is implemented in the `interpreters` module.
/// Here we only handle the errors and logging them to the console.
pub struct Recognition {
interpreter: Box<dyn semantic::Interpreter>,
}
impl TryFrom<&config::Main> for Recognition {
type Error = anyhow::Error;
/// Creates an interpreter to recognize the compiler calls.
///
/// Using the configuration we can define which compilers to include and exclude.
/// Also read the environment variables to detect the compiler to include (and
/// make sure those are not excluded either).
// TODO: Use the CC or CXX environment variables to detect the compiler to include.
// Use the CC or CXX environment variables and make sure those are not excluded.
// Make sure the environment variables are passed to the method.
fn try_from(config: &config::Main) -> Result<Self, Self::Error> {
let compilers_to_include = match &config.intercept {
config::Intercept::Wrapper { executables, .. } => executables.clone(),
_ => vec![],
};
let compilers_to_exclude = match &config.output {
config::Output::Clang { compilers, .. } => compilers
.iter()
.filter(|compiler| compiler.ignore == config::Ignore::Always)
.map(|compiler| compiler.path.clone())
.collect(),
_ => vec![],
};
let interpreter = semantic::interpreters::Builder::new()
.compilers_to_recognize(compilers_to_include.as_slice())
.compilers_to_exclude(compilers_to_exclude.as_slice())
.build();
Ok(Recognition {
interpreter: Box::new(interpreter),
})
}
}
impl Recognition {
/// Simple call the semantic module to recognize the execution.
/// Forward only the compiler calls, and log each recognition result.
pub fn apply(&self, execution: ipc::Execution) -> semantic::Recognition<semantic::CompilerCall> {
self.interpreter.recognize(&execution)
}
}
/// Responsible for reading the build events from the intercept mode.
///
/// The file syntax is defined by the `events` module, and the parsing logic is implemented there.
/// Here we only handle the file opening and the error handling.
pub struct EventFileReader {
stream: Box<dyn Iterator<Item = Result<Envelope, Error>>>,
}
impl TryFrom<args::BuildEvents> for EventFileReader {
type Error = anyhow::Error;
/// Open the file and create a new instance of the event file reader.
///
/// If the file cannot be opened, the error will be logged and escalated.
fn try_from(value: args::BuildEvents) -> Result<Self, Self::Error> {
let file_name = PathBuf::from(value.file_name);
let file = OpenOptions::new()
.read(true)
.open(file_name.as_path())
.map(BufReader::new)
.with_context(|| format!("Failed to open input file: {:?}", file_name))?;
let stream = Box::new(StreamDeserializer::new(IoRead::new(file)));
Ok(EventFileReader { stream })
}
}
impl EventFileReader {
/// Generate the build events from the file.
///
/// Returns an iterator over the build events. Any error during the reading
/// of the file will be logged and the failed entries will be skipped.
pub fn generate(self) -> impl Iterator<Item = Execution> {
self.stream.filter_map(|result| match result {
Ok(value) => Some(value.event.execution),
Err(error) => {
log::error!("Failed to read event: {:?}", error);
None
}
})
}
}

View File

@@ -15,7 +15,6 @@ pub mod transformation;
use super::ipc::Execution;
use std::path::PathBuf;
use crate::semantic;
/// Represents an executed command semantic.
#[derive(Debug, PartialEq)]
@@ -66,6 +65,18 @@ pub enum Recognition<T> {
Unknown,
}
impl <T> IntoIterator for Recognition<T> {
type Item = T;
type IntoIter = std::option::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
match self {
Recognition::Success(value) => Some(value).into_iter(),
_ => None.into_iter(),
}
}
}
/// Responsible to transform the semantic of an executed command.
///
/// It conditionally removes compiler calls based on compiler names or flags.