mhost 0.11.3

Fast, async DNS lookup library and CLI -- modern dig/host replacement with parallel multi-server queries, DoH, DoT, subdomain discovery, and zone verification
Documentation
// Copyright 2017-2021 Lukas Pustina <lukas@pustina.de>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use std::convert::TryInto;
use std::env;

use anyhow::{Context, Result};
use clap::ArgMatches;
use tokio::runtime::Runtime;
use tracing::info;

use mhost::app::console::{Console, ConsoleOpts};
use mhost::app::logging::Logging;
use mhost::app::modules::{self, PartialError};
use mhost::app::AppConfig;
use mhost::app::{cli_parser, ExitStatus};
use mhost::nameserver::predefined;

fn main() {
    let res = do_main();

    let exit_status = match res {
        Ok(exit_status) => exit_status,
        Err(err) => match err.downcast_ref::<PartialError>() {
            Some(PartialError::Failed(exit_status)) => exit_status.to_owned(),
            _ => {
                let console = Console::new(ConsoleOpts::default());
                console.error(format!("Error: {:#}", err));
                ExitStatus::UnrecoverableError
            }
        },
    };

    std::process::exit(exit_status as i32);
}

fn exit_subcommand_invalid() -> ExitStatus {
    // Showing help when no valid subcommand is given is a successful operation,
    // consistent with --help behavior.
    ExitStatus::Ok
}

fn do_main() -> Result<ExitStatus> {
    let args = cli_parser::create_parser().get_matches();
    let color = !args.get_flag("no-color");
    let debug = args.get_flag("debug");

    setup_terminal(&args, color);
    setup_logging(&args, color, debug);
    info!("Set up logging.");
    let app_config = match parse_global_args(&args) {
        Ok(app_config) => app_config,
        Err(err) => {
            let console = Console::new(ConsoleOpts::default());
            console.error(format!("Invalid configuration: {:#}", err));
            return Ok(ExitStatus::ConfigParsingFailed);
        }
    };
    info!("Parsed global args.");
    let console = setup_console(&app_config);
    let runtime = setup_tokio(app_config.max_worker_threads())?;
    info!(
        "Started Async Runtime with {} worker threads.",
        app_config
            .max_worker_threads()
            .map(|x| x.to_string())
            .unwrap_or_else(|| "Tokio default number of".to_string())
    );

    info!("Running command");
    let res = runtime.block_on(async { run_command(&args, &app_config, &console).await });
    info!("Finished command.");

    res
}

/// Start Tokio runtime, use either with default or explicitly set number of work threads
fn setup_tokio(num_threads: Option<usize>) -> Result<Runtime> {
    let mut builder = tokio::runtime::Builder::new_multi_thread();
    builder.enable_all();
    if let Some(num_threads) = num_threads {
        builder.worker_threads(num_threads);
    }
    builder.build().context("Failed to build Async Runtime")
}

fn setup_terminal(args: &ArgMatches, color: bool) {
    if !color {
        mhost::app::common::styles::no_color_mode();
    }
    if args.get_flag("ascii") {
        mhost::app::common::styles::ascii_mode();
    }
}

fn setup_logging(args: &ArgMatches, color: bool, debug: bool) {
    Logging::new(args.get_count("v"), env::var_os("RUST_LOG"), color, debug)
        .start()
        .expect("failed to initialize logging");
}

fn parse_global_args(args: &ArgMatches) -> Result<AppConfig> {
    let app_config: AppConfig = args.try_into()?;

    Ok(app_config)
}

fn setup_console(app_config: &AppConfig) -> Console {
    let console_opts = ConsoleOpts::from(app_config);

    Console::new(console_opts)
}

async fn run_command(args: &ArgMatches, app_config: &AppConfig, console: &Console) -> Result<ExitStatus> {
    if app_config.list_predefined {
        list_predefined_nameservers(console);
        return Ok(ExitStatus::Ok);
    }

    match args.subcommand_name() {
        Some("check") => modules::check::run(args, app_config).await,
        Some("diff") => modules::diff::run(args, app_config).await,
        Some("discover") => modules::discover::run(args, app_config).await,
        Some("dnssec") => modules::dnssec::run(args, app_config).await,
        Some("domain-lookup") => modules::domain_lookup::run(args, app_config).await,
        Some("completions") => modules::completions::run(args),
        Some("info") => modules::info::run(args),
        Some("server-lists") => modules::get_server_lists::run(args, app_config).await,
        Some("lookup") => modules::lookup::run(args, app_config).await,
        Some("propagation") => modules::propagation::run(args, app_config).await,
        Some("trace") => modules::trace::run(args, app_config).await,
        Some("verify") => modules::verify::run(args, app_config).await,
        _ => {
            cli_parser::show_commands();
            Ok(exit_subcommand_invalid())
        }
    }
}

pub fn list_predefined_nameservers(console: &Console) {
    console.caption("Predefined servers:");
    for ns in predefined::nameserver_configs() {
        console.itemize(format!("{}", ns));
    }
}