use std::convert::TryFrom;
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use storyteller::{EventHandler, EventListener, FinishProcessing};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use cargo_msrv::cli::{CargoCli, CargoMsrvOpts};
use cargo_msrv::error::CargoMSRVError;
use cargo_msrv::exit_code::ExitCode;
use cargo_msrv::reporter::{
DiscardOutputHandler, HumanProgressHandler, JsonHandler, MinimalOutputHandler, ReporterSetup,
};
use cargo_msrv::reporter::{Event, Reporter, TerminateWithFailure};
use cargo_msrv::{run_app, Context, OutputFormat, TracingOptions, TracingTargetOption};
fn main() {
std::process::exit(
match setup_opts_and_tracing(std::env::args_os) {
Ok((_guard, exit_code)) => exit_code,
Err(err) => {
eprintln!("Setup error: {err}");
ExitCode::Failure
}
}
.into(),
);
}
fn setup_opts_and_tracing<I: IntoIterator<Item = OsString>, F: FnOnce() -> I + Clone>(
args: F,
) -> Result<(Option<TracingGuard>, ExitCode), SetupError> {
let matches = CargoCli::parse_args(args());
let opts = matches.to_cargo_msrv_cli().to_opts();
let mut guard = None;
let tracing_is_enabled = !opts.shared_opts.debug_output_opts.no_log;
if tracing_is_enabled {
let options = TracingOptions::new(
opts.shared_opts.debug_output_opts.log_target,
opts.shared_opts.debug_output_opts.log_level,
);
let tracing_config = TracingConfig::try_from_options(&options)?;
guard = Some(init_tracing(&tracing_config)?);
}
setup_reporter(opts).map(|exit_code| (guard, exit_code))
}
fn setup_reporter(opts: CargoMsrvOpts) -> Result<ExitCode, SetupError> {
tracing::info!(
cargo_msrv_version = env!("CARGO_PKG_VERSION"),
"initializing"
);
let setup = ReporterSetup;
let (reporter, listener) = setup.create();
tracing::info!("storyteller channel created");
let output_format = opts.shared_opts.user_output_opts.effective_output_format();
let handler = WrappingHandler::from(output_format);
let finalizer = listener.run_handler(Arc::new(handler));
tracing::info!("storyteller started handler");
tracing::info!("starting execution");
let res = setup_context_and_run(opts, &reporter);
tracing::info!("finished execution");
let exit_code = get_exit_code(res, &reporter)?;
disconnect_reporter(reporter)?;
wait_for_user_output(finalizer)?;
Ok(exit_code)
}
fn setup_context_and_run(
opts: CargoMsrvOpts,
reporter: &impl Reporter,
) -> Result<(), CargoMSRVError> {
let context = Context::try_from(opts)?;
run_app(&context, reporter)
}
fn get_exit_code(
result: Result<(), CargoMSRVError>,
reporter: &impl Reporter,
) -> Result<ExitCode, SetupError> {
Ok(match result {
Ok(_) => ExitCode::Success,
Err(err) => {
reporter
.report_event(TerminateWithFailure::new(err))
.map_err(|_| SetupError::StorytellerSend)?;
ExitCode::Failure
}
})
}
enum WrappingHandler {
HumanProgress(HumanProgressHandler),
Json(JsonHandler<io::Stderr>),
Minimal(MinimalOutputHandler<io::Stdout, io::Stderr>),
DiscardOutput(DiscardOutputHandler),
}
impl EventHandler for WrappingHandler {
type Event = Event;
fn handle(&self, event: Self::Event) {
match self {
WrappingHandler::HumanProgress(inner) => inner.handle(event),
WrappingHandler::Json(inner) => inner.handle(event),
WrappingHandler::Minimal(inner) => inner.handle(event),
WrappingHandler::DiscardOutput(inner) => inner.handle(event),
}
}
fn finish(&self) {
match self {
WrappingHandler::HumanProgress(inner) => inner.finish(),
WrappingHandler::Json(inner) => inner.finish(),
WrappingHandler::Minimal(inner) => inner.finish(),
WrappingHandler::DiscardOutput(inner) => inner.finish(),
}
}
}
impl From<OutputFormat> for WrappingHandler {
fn from(output_format: OutputFormat) -> Self {
match output_format {
OutputFormat::Human => Self::HumanProgress(HumanProgressHandler::default()),
OutputFormat::Json => Self::Json(JsonHandler::stderr()),
OutputFormat::Minimal => Self::Minimal(MinimalOutputHandler::stderr()),
OutputFormat::None => {
Self::DiscardOutput(DiscardOutputHandler)
}
}
}
}
fn disconnect_reporter(reporter: impl Reporter) -> Result<(), SetupError> {
reporter
.disconnect()
.map_err(|_| SetupError::StorytellerDisconnect)?;
tracing::info!("disconnected reporter");
Ok(())
}
fn wait_for_user_output(finalizer: impl FinishProcessing) -> Result<(), SetupError> {
finalizer
.finish_processing()
.map_err(|_| SetupError::StorytellerFinishEventProcessing)?;
tracing::info!("finished processing unprocessed events");
Ok(())
}
fn init_tracing(tracing_config: &TracingConfig) -> Result<TracingGuard, SetupError> {
let level = tracing_config.level;
match &tracing_config.target {
TracingTarget::ToDisk(path) => {
let guard = init_tracing_to_file(path, level);
let folder = format!("{}", path.display());
tracing::debug!(log_folder = folder.as_str());
guard
}
TracingTarget::Stdout => init_tracing_to_stdout(level),
}
}
fn init_tracing_to_file(
log_folder: impl AsRef<Path>,
level: tracing::Level,
) -> Result<TracingGuard, SetupError> {
let file_appender = RollingFileAppender::new(Rotation::DAILY, log_folder, "cargo-msrv-log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::fmt()
.json()
.with_max_level(level)
.with_writer(non_blocking)
.finish();
tracing::subscriber::set_global_default(subscriber)
.map_err(|_| SetupError::UnableToInitTracing)?;
Ok(TracingGuard::NonBlockingGuard(guard))
}
fn init_tracing_to_stdout(level: tracing::Level) -> Result<TracingGuard, SetupError> {
let subscriber = tracing_subscriber::fmt().with_max_level(level).finish();
tracing::subscriber::set_global_default(subscriber)
.map_err(|_| SetupError::UnableToInitTracing)?;
Ok(TracingGuard::None)
}
struct TracingConfig {
level: tracing::Level,
target: TracingTarget,
}
impl TracingConfig {
fn try_from_options(config: &TracingOptions) -> Result<Self, SetupError> {
let target = TracingTarget::try_from_option(config.target())?;
Ok(Self {
level: (*config.level()).into(),
target,
})
}
}
enum TracingTarget {
ToDisk(PathBuf),
Stdout,
}
impl TracingTarget {
fn try_from_option(option: &TracingTargetOption) -> Result<Self, SetupError> {
match option {
TracingTargetOption::File => {
let folder = log_folder()?;
Ok(Self::ToDisk(folder))
}
TracingTargetOption::Stdout => Ok(Self::Stdout),
}
}
}
enum TracingGuard {
NonBlockingGuard(#[allow(dead_code)] tracing_appender::non_blocking::WorkerGuard),
None,
}
fn log_folder() -> Result<PathBuf, SetupError> {
dirs::data_local_dir()
.map(|path| path.join("cargo-msrv"))
.ok_or(SetupError::UnableToAccessLogFolder)
}
#[derive(Debug, thiserror::Error)]
enum SetupError {
#[error("Unable to init logger, run with --no-log to try again without logging.")]
UnableToInitTracing,
#[error("Unable to access log folder, run with --no-log to try again without logging.")]
UnableToAccessLogFolder,
#[error("Failed to disconnect user output channel (storyteller)")]
StorytellerDisconnect,
#[error("Failed to send event to user output channel (storyteller)")]
StorytellerSend,
#[error("Failure while waiting for unprocessed events to be processed")]
StorytellerFinishEventProcessing,
}