use argh::{FromArgs, TopLevelCommand};
use chrono::{DateTime, Utc};
use std::{path::Path, str::FromStr, time::Duration};
use write_metrics::write_metrics;
mod add_battery_reading;
mod config_file;
mod coredump;
mod export;
mod report_sync;
mod session;
mod sync;
mod trace;
mod write_attributes;
mod write_metrics;
use crate::{
cli::version::format_version,
mar::{DeviceAttribute, ExportFormat, MarConfig, Metadata},
metrics::{KeyedMetricReading, SessionName},
reboot::{write_reboot_reason_and_reboot, RebootReason},
service_manager::get_service_manager,
};
use crate::{mar::MarEntryBuilder, util::output_arg::OutputArg};
use crate::cli::init_logger;
use crate::cli::memfaultctl::add_battery_reading::add_battery_reading;
use crate::cli::memfaultctl::config_file::{set_data_collection, set_developer_mode};
use crate::cli::memfaultctl::coredump::{trigger_coredump, ErrorStrategy};
use crate::cli::memfaultctl::export::export;
use crate::cli::memfaultctl::report_sync::report_sync;
use crate::cli::memfaultctl::sync::sync;
use crate::cli::show_settings::show_settings;
use crate::config::Config;
use crate::network::NetworkConfig;
use eyre::{eyre, Context, Result};
use log::LevelFilter;
use self::session::{end_session, start_session};
#[derive(FromArgs)]
struct MemfaultctlArgs {
#[argh(subcommand)]
command: MemfaultctlCommand,
#[argh(option, short = 'c')]
config_file: Option<String>,
#[argh(switch, short = 'v')]
#[allow(dead_code)]
version: bool,
#[argh(switch, short = 'V')]
verbose: bool,
}
pub struct WrappedArgs<T: FromArgs>(pub T);
impl<T: FromArgs> TopLevelCommand for WrappedArgs<T> {}
impl<T: FromArgs> FromArgs for WrappedArgs<T> {
fn from_args(command_name: &[&str], args: &[&str]) -> Result<Self, argh::EarlyExit> {
#[derive(FromArgs)]
struct CommandlikeFlags {
#[argh(switch, short = 'v')]
version: bool,
}
match CommandlikeFlags::from_args(command_name, args) {
Ok(CommandlikeFlags { version: true }) => Err(argh::EarlyExit {
output: format_version(),
status: Ok(()),
}),
_ => T::from_args(command_name, args).map(Self),
}
}
}
pub fn from_env<T: TopLevelCommand>() -> T {
argh::from_env::<WrappedArgs<T>>().0
}
#[derive(FromArgs)]
#[argh(subcommand)]
enum MemfaultctlCommand {
EnableDataCollection(EnableDataCollectionArgs),
DisableDataCollection(DisableDataCollectionArgs),
EnableDevMode(EnableDevModeArgs),
DisableDevMode(DisableDevModeArgs),
Export(ExportArgs),
Reboot(RebootArgs),
RequestMetrics(RequestMetricsArgs),
ShowSettings(ShowSettingsArgs),
Synchronize(SyncArgs),
Upload(UploadArgs),
TriggerCoredump(TriggerCoredumpArgs),
WriteAttributes(WriteAttributesArgs),
AddBatteryReading(AddBatteryReadingArgs),
ReportSyncSuccess(ReportSyncSuccessArgs),
ReportSyncFailure(ReportSyncFailureArgs),
StartSession(StartSessionArgs),
EndSession(EndSessionArgs),
AddCustomDataRecording(AddCustomDataRecordingArgs),
WriteMetrics(WriteMetricsArgs),
SaveTrace(SaveTraceArgs),
}
#[derive(FromArgs)]
#[argh(subcommand, name = "enable-data-collection")]
struct EnableDataCollectionArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "disable-data-collection")]
struct DisableDataCollectionArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "enable-dev-mode")]
struct EnableDevModeArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "disable-dev-mode")]
struct DisableDevModeArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "export")]
pub struct ExportArgs {
#[argh(switch, short = 'n')]
do_not_delete: bool,
#[argh(option, short = 'o')]
output: OutputArg,
#[argh(option, short = 'f', default = "ExportFormat::Mar")]
format: ExportFormat,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "reboot")]
struct RebootArgs {
#[argh(option)]
reason: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "request-metrics")]
struct RequestMetricsArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "show-settings")]
struct ShowSettingsArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "sync")]
struct SyncArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "upload")]
struct UploadArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "trigger-coredump")]
struct TriggerCoredumpArgs {
#[argh(positional, default = "ErrorStrategy::SegFault")]
strategy: ErrorStrategy,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "write-attributes")]
struct WriteAttributesArgs {
#[argh(positional)]
attributes: Vec<DeviceAttribute>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "write-metrics")]
struct WriteMetricsArgs {
#[argh(positional)]
metrics: Vec<KeyedMetricReading>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "add-battery-reading")]
struct AddBatteryReadingArgs {
#[argh(positional)]
reading_string: String,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "report-sync-success")]
struct ReportSyncSuccessArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "report-sync-failure")]
struct ReportSyncFailureArgs {}
#[derive(FromArgs)]
#[argh(subcommand, name = "start-session")]
struct StartSessionArgs {
#[argh(positional)]
session_name: SessionName,
#[argh(positional)]
readings: Vec<KeyedMetricReading>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "end-session")]
struct EndSessionArgs {
#[argh(positional)]
session_name: SessionName,
#[argh(positional)]
readings: Vec<KeyedMetricReading>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "add-custom-data-recording")]
struct AddCustomDataRecordingArgs {
#[argh(positional)]
reason: String,
#[argh(positional)]
file_name: String,
#[argh(positional)]
mime_types: Vec<String>,
#[argh(option, default = "0")]
duration_ms: u64,
#[argh(option)]
start_time: Option<DateTime<Utc>>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "save-trace")]
struct SaveTraceArgs {
#[argh(option)]
program: String,
#[argh(option)]
reason: String,
#[argh(option)]
crash: Option<bool>,
#[argh(option)]
source: Option<String>,
#[argh(option)]
signature: Option<String>,
}
fn check_data_collection_enabled(config: &Config, do_what: &str) -> Result<()> {
match config.config_file.enable_data_collection {
true => Ok(()),
false => {
let msg = format!(
"Cannot {} because data collection is disabled. \
Hint: enable it with 'memfaultctl enable-data-collection'.",
do_what
);
Err(eyre!(msg))
}
}
}
pub fn main() -> Result<()> {
let args: MemfaultctlArgs = from_env();
init_logger(if args.verbose {
LevelFilter::Trace
} else {
LevelFilter::Info
})?;
let config_path = args.config_file.as_ref().map(Path::new);
let warnings_handle_fn = |w: &_| eprintln!("{}", w);
let mut config = Config::read_from_system(config_path, warnings_handle_fn)?;
let network_config = NetworkConfig::from(&config);
let mar_staging_path = config.mar_tmp_staging_path();
let service_manager = get_service_manager();
match args.command {
MemfaultctlCommand::EnableDataCollection(_) => {
set_data_collection(&mut config, &service_manager, true)
}
MemfaultctlCommand::DisableDataCollection(_) => {
set_data_collection(&mut config, &service_manager, false)
}
MemfaultctlCommand::EnableDevMode(_) => {
set_developer_mode(&mut config, &service_manager, true)
}
MemfaultctlCommand::DisableDevMode(_) => {
set_developer_mode(&mut config, &service_manager, false)
}
MemfaultctlCommand::Export(args) => export(&config, &args).wrap_err("Error exporting data"),
MemfaultctlCommand::Reboot(args) => {
let reason = RebootReason::from_str(&args.reason)
.wrap_err(eyre!("Failed to parse {}", args.reason))?;
println!("Rebooting with reason {:?}", reason);
write_reboot_reason_and_reboot(
&config.config_file.reboot.last_reboot_reason_file,
reason,
)
}
MemfaultctlCommand::RequestMetrics(_) => sync(false),
MemfaultctlCommand::ShowSettings(_) => show_settings(config_path),
MemfaultctlCommand::Synchronize(_) => sync(false),
MemfaultctlCommand::Upload(_) => sync(true),
MemfaultctlCommand::TriggerCoredump(TriggerCoredumpArgs { strategy }) => {
trigger_coredump(&config, strategy)
}
MemfaultctlCommand::WriteAttributes(WriteAttributesArgs { attributes }) => {
if attributes.is_empty() {
Err(eyre!(
"No attributes given. Please specify them as KEY=VALUE pairs."
))
} else {
check_data_collection_enabled(&config, "write attributes")?;
let metrics = attributes
.into_iter()
.map(KeyedMetricReading::try_from)
.collect::<Result<Vec<KeyedMetricReading>>>()?;
write_metrics(metrics, &config)
}
}
MemfaultctlCommand::AddBatteryReading(AddBatteryReadingArgs { reading_string }) => {
add_battery_reading(&config, &reading_string)
}
MemfaultctlCommand::ReportSyncSuccess(_) => report_sync(&config, true),
MemfaultctlCommand::ReportSyncFailure(_) => report_sync(&config, false),
MemfaultctlCommand::StartSession(StartSessionArgs {
session_name,
readings,
}) => start_session(&config, session_name, readings),
MemfaultctlCommand::EndSession(EndSessionArgs {
session_name,
readings,
}) => end_session(&config, session_name, readings),
MemfaultctlCommand::AddCustomDataRecording(AddCustomDataRecordingArgs {
reason,
file_name,
duration_ms,
mime_types,
start_time,
}) => {
check_data_collection_enabled(&config, "add custom data recording")?;
let file_path = Path::new(&file_name).to_owned();
if !file_path.is_file() {
return Err(eyre!("{} does not exist", file_name));
}
if !file_path.is_absolute() {
return Err(eyre!("{} is not an absolute path", file_name));
}
let file_name = file_name
.trim()
.split('/')
.next_back()
.ok_or_else(|| eyre!("{} is not a valid file path", file_name))?
.to_string();
let mar_config = MarConfig::from(&config);
MarEntryBuilder::new(&mar_staging_path)?
.set_metadata(Metadata::new_custom_data_recording(
start_time,
Duration::from_millis(duration_ms),
mime_types,
reason,
file_name,
None,
))
.add_copied_attachment(file_path)?
.save(&network_config, &mar_config)
.map(|_entry| ())
}
MemfaultctlCommand::WriteMetrics(WriteMetricsArgs { metrics }) => {
check_data_collection_enabled(&config, "write metrics")?;
write_metrics(metrics, &config)
}
MemfaultctlCommand::SaveTrace(SaveTraceArgs {
program,
reason,
crash,
source,
signature,
}) => trace::save_trace(&config, program, reason, crash, source, signature),
}
}