summaryrefslogtreecommitdiffhomepage
path: root/mullvad-cli/src/main.rs
blob: 766fb43bad56f775a4b86603d4ac05a565a0b0e8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#![deny(rust_2018_idioms)]

use clap::{crate_authors, crate_description};
use mullvad_management_interface::async_trait;
use std::{collections::HashMap, io};
use talpid_types::ErrorExt;

pub use mullvad_management_interface::{self, new_rpc_client};

mod cmds;
mod format;
mod location;
mod state;

pub const BIN_NAME: &str = "mullvad";
pub const PRODUCT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-version.txt"));

pub type Result<T> = std::result::Result<T, Error>;

#[derive(err_derive::Error, Debug)]
pub enum Error {
    #[error(display = "Failed to connect to daemon")]
    DaemonNotRunning(#[error(source)] io::Error),

    #[error(display = "Management interface error")]
    ManagementInterfaceError(#[error(source)] mullvad_management_interface::Error),

    #[error(display = "RPC failed")]
    RpcFailed(#[error(source)] mullvad_management_interface::Status),

    #[error(display = "RPC failed: {}", _0)]
    RpcFailedExt(
        &'static str,
        #[error(source)] mullvad_management_interface::Status,
    ),

    /// The given command is not correct in some way
    #[error(display = "Invalid command: {}", _0)]
    InvalidCommand(&'static str),

    #[error(display = "Command failed: {}", _0)]
    CommandFailed(&'static str),

    #[error(display = "Failed to listen for status updates")]
    StatusListenerFailed,
}

#[tokio::main]
async fn main() {
    let exit_code = match run().await {
        Ok(_) => 0,
        Err(error) => {
            match &error {
                Error::RpcFailed(status) => {
                    eprintln!("{}: {:?}: {}", error, status.code(), status.message())
                }
                Error::RpcFailedExt(_message, status) => eprintln!(
                    "{}\nCaused by: {:?}: {}",
                    error,
                    status.code(),
                    status.message()
                ),
                error => eprintln!("{}", error.display_chain()),
            }
            1
        }
    };
    std::process::exit(exit_code);
}

async fn run() -> Result<()> {
    env_logger::init();

    let commands = cmds::get_commands();
    let app = build_cli(&commands);

    let app = app.subcommand(
        clap::SubCommand::with_name("shell-completions")
            .about("Generates completion scripts for your shell")
            .arg(
                clap::Arg::with_name("SHELL")
                    .required(true)
                    .possible_values(&clap::Shell::variants()[..])
                    .help("The shell to generate the script for"),
            )
            .arg(
                clap::Arg::with_name("DIR")
                    .default_value("./")
                    .help("Output directory where the shell completions are written"),
            )
            .setting(clap::AppSettings::Hidden),
    );

    let app_matches = app.get_matches();
    match app_matches.subcommand() {
        ("shell-completions", Some(sub_matches)) => {
            let shell = sub_matches
                .value_of("SHELL")
                .unwrap()
                .parse()
                .expect("Invalid shell");
            let out_dir = sub_matches.value_of_os("DIR").unwrap();
            build_cli(&commands).gen_completions(BIN_NAME, shell, out_dir);
            Ok(())
        }
        (sub_name, Some(sub_matches)) => {
            if let Some(cmd) = commands.get(sub_name) {
                cmd.run(sub_matches).await
            } else {
                unreachable!("No command matched");
            }
        }
        (_, None) => {
            unreachable!("No subcommand matches");
        }
    }
}

fn build_cli(commands: &HashMap<&'static str, Box<dyn Command>>) -> clap::App<'static, 'static> {
    clap::App::new(BIN_NAME)
        .version(PRODUCT_VERSION)
        .author(crate_authors!())
        .about(crate_description!())
        .setting(clap::AppSettings::SubcommandRequiredElseHelp)
        .global_settings(&[
            clap::AppSettings::DisableHelpSubcommand,
            clap::AppSettings::VersionlessSubcommands,
        ])
        .subcommands(commands.values().map(|cmd| cmd.clap_subcommand()))
}

#[async_trait]
pub trait Command {
    fn name(&self) -> &'static str;

    fn clap_subcommand(&self) -> clap::App<'static, 'static>;

    async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()>;
}