diff --git a/Cargo.lock b/Cargo.lock index 0a4a212f5..29ff2babd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2080,6 +2080,7 @@ dependencies = [ "humility-cli", "humility-core", "humility-doppel", + "humility-dump", "humility-jefe", "parse_int", ] @@ -2121,6 +2122,7 @@ dependencies = [ "humility-arch-arm", "humility-cli", "humility-core", + "humility-dump", "humility-dump-agent", "humpty", "indexmap 2.14.0", @@ -2271,6 +2273,7 @@ dependencies = [ "humility-arch-arm", "humility-cli", "humility-core", + "humility-dump", "humpty", "serde", "serde_json", @@ -2888,6 +2891,20 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "humility-dump" +version = "0.1.0" +dependencies = [ + "anyhow", + "humility-core", + "humility-dump-agent", + "humility-log", + "humility-net-core", + "humpty", + "slog", + "thiserror 2.0.18", +] + [[package]] name = "humility-dump-agent" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 98e7d960b..83c58ccc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "humility-core", "humility-doppel", "humility-dump-agent", + "humility-dump", "humility-flash", "humility-hexdump", "humility-hiffy", @@ -134,6 +135,7 @@ humility-caboose = { path = "./humility-caboose" } humility-cli = { path = "./humility-cli", default-features = false } humility-doppel = { path = "./humility-doppel" } humility-dump-agent = { path = "./humility-dump-agent" } +humility-dump = { path = "./humility-dump" } humility-flash = { path = "./humility-flash" } humility-hexdump = { path = "./humility-hexdump" } humility-hiffy = { path = "./humility-hiffy" } diff --git a/cmd/diagnose/Cargo.toml b/cmd/diagnose/Cargo.toml index bb666c421..8e1f29ebe 100644 --- a/cmd/diagnose/Cargo.toml +++ b/cmd/diagnose/Cargo.toml @@ -13,3 +13,4 @@ humility.workspace = true humility-cli.workspace = true humility-doppel.workspace = true humility-jefe.workspace = true +humility-dump.workspace = true diff --git a/cmd/diagnose/src/lib.rs b/cmd/diagnose/src/lib.rs index f366cdbb1..30a4c5a4e 100644 --- a/cmd/diagnose/src/lib.rs +++ b/cmd/diagnose/src/lib.rs @@ -327,7 +327,12 @@ fn diagnose( if !subargs.no_dump { section("Generating Coredump"); - let rval = hubris.dump(core, None, None, None, context.log()); + + let (filename, mut file) = + humility_dump::open_dump_file(hubris, None, None)?; + println!("dumping to {filename:?}"); + + let rval = hubris.dump(core, None, &mut file, None, context.log()); if let Err(e) = rval { println!("Coredump failed: {}", e); diff --git a/cmd/dump/Cargo.toml b/cmd/dump/Cargo.toml index 8ac8475cb..0178944be 100644 --- a/cmd/dump/Cargo.toml +++ b/cmd/dump/Cargo.toml @@ -21,3 +21,4 @@ humility.workspace = true humility-cli.workspace = true humility-dump-agent.workspace = true humility-arch-arm.workspace = true +humility-dump.workspace = true diff --git a/cmd/dump/src/lib.rs b/cmd/dump/src/lib.rs index fcf793c3e..71fc932d2 100644 --- a/cmd/dump/src/lib.rs +++ b/cmd/dump/src/lib.rs @@ -69,7 +69,7 @@ use anyhow::{Result, bail}; use clap::{ArgGroup, Parser}; use humility::core::Core; use humility::hubris::*; -use humility::log::{Logger, info, warn}; +use humility::log::{Logger, info}; use humility_arch_arm::ARMRegister; use humility_cli::{ExecutionContext, humility_cmd}; use humility_dump_agent::{ @@ -408,7 +408,8 @@ fn print_dump_breakdown(breakdown: &DumpBreakdown, log: &Logger) { print_val("data expansion", breakdown.inverted); } -fn dump_via_agent( +/// Simulation and testing features +fn simulate_dump_via_agent( hubris: &HubrisArchive, core: &mut dyn Core, subargs: &DumpArgs, @@ -452,7 +453,11 @@ fn dump_via_agent( info!(log, "core halted"); if let Some(ref stock) = subargs.stock_dumpfile { - hubris.dump(core, task, Some(stock), None, log)?; + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(stock)?; + hubris.dump(core, task, &mut file, None, log)?; } match task { @@ -552,135 +557,59 @@ fn dump_via_agent( core.run()?; info!(log, "core resumed"); - } else { + } else if subargs.emulate_dumper { let segments = hubris.dump_segments(core, None, false)?; let mut agent = get_dump_agent(hubris, core, subargs, log)?; let header = agent.read_dump_header()?; - if !subargs.force_read && subargs.extract.is_none() { - if header.dumper != humpty::DUMPER_NONE - && !subargs.initialize_dump_agent - && !subargs.force_overwrite - && task.is_none() - { - bail!( - "there appears to already be one or more dumps in situ; \ + if header.dumper != humpty::DUMPER_NONE + && !subargs.force_overwrite + && task.is_none() + { + bail!( + "there appears to already be one or more dumps in situ; \ list them with --list and extract them with --extract_all" - ) - } - - if task.is_none() || subargs.initialize_dump_agent { - info!(log, "initializing dump agent state"); - agent.initialize_dump()?; - } - - if subargs.initialize_dump_agent { - return Ok(()); - } - - if task.is_none() { - info!(log, "initializing segments"); - agent.initialize_segments(&segments)?; - } + ) } - if subargs.emulate_dumper { - agent.core().halt()?; - info!(log, "core halted"); + if task.is_none() { + info!(log, "initializing segments"); + agent.initialize_segments(&segments)?; + } + agent.core().halt()?; + info!(log, "core halted"); - if let Some(ref stock) = subargs.stock_dumpfile { - hubris.dump(agent.core(), task, Some(stock), None, log)?; - } + if let Some(ref stock) = subargs.stock_dumpfile { + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(stock)?; + hubris.dump(agent.core(), task, &mut file, None, log)?; + } - let base = header.address; - let total = segments.iter().fold(0, |ttl, (_, size)| ttl + size); + let base = header.address; + let total = segments.iter().fold(0, |ttl, (_, size)| ttl + size); - let address = if task.is_some() { - match emulate_task_dump_prep(agent.core(), &segments, base) { - Err(e) => { - agent.core().run()?; - info!(log, "core resumed after failure"); - return Err(e); - } - Ok(address) => { - assert!(area.is_none()); - area = Some(DumpArea::ByAddress(address)); - address - } + let address = if task.is_some() { + match emulate_task_dump_prep(agent.core(), &segments, base) { + Err(e) => { + agent.core().run()?; + info!(log, "core resumed after failure"); + return Err(e); } - } else { - base - }; - - emulate_dump(agent.core(), task, address, total, log)?; - agent.core().run()?; - info!(log, "core resumed"); - } else if !subargs.force_read && subargs.extract.is_none() { - if subargs.force_manual_initiation { - agent.core().halt()?; - info!(log, "leaving core halted"); - let base = header.address; - info!( - log, - "unplug probe and manually \ - initiate dump from address {:#x}", - base - ); - info!( - log, - "e.g., \"humility hiffy --call \ - Dumper.dump -a address={:#x}\"", - base - ); - return Ok(()); - } - - // - // We are about to disappear for -- as the kids say -- a minute. - // Set our timeout to be a literal minute so we don't prematurely - // give up. - // - agent.core().set_timeout(std::time::Duration::new(60, 0))?; - - // - // Tell the thing to take a dump. - // - if let Err(err) = agent.take_dump() { - // - // If that fails, it may be because we ran out of space. Check - // our dump headers; if all of them are consumed, assume - // that we ran out of space -- and if any of them are consumed, - // process whatever we find (some dump is better than none!) and - // warn accordingly. - // - if let Ok(all) = agent.read_dump_headers(true) { - let c = all - .iter() - .filter(|&&(h, _)| h.dumper != humpty::DUMPER_NONE) - .count(); - - if c == all.len() { - warn!( - log, - "dump has indicated failure ({err:?}), but this is \ - likely due to space exhaustion; \ - dump will be extracted but may be incomplete!" - ); - } else if c != 0 { - warn!( - log, - "dump has indicated failure ({err:?}), but some dump \ - contents appear to have been written; \ - dump will be extracted but may be incomplete!" - ); - } else { - return Err(err); - } - } else { - return Err(err); + Ok(address) => { + assert!(area.is_none()); + area = Some(DumpArea::ByAddress(address)); + address } } - } + } else { + base + }; + + emulate_dump(agent.core(), task, address, total, log)?; + agent.core().run()?; + info!(log, "core resumed"); // // If we're here, we have a dump in situ -- time to pull it. @@ -700,9 +629,91 @@ fn dump_via_agent( info!(log, "retaining dump agent state"); } } + + let (filename, mut file) = humility_dump::open_dump_file( + hubris, + task, + subargs.dumpfile.as_deref(), + )?; + info!(log, "dumping to {filename:?}"); + hubris.dump(&mut out, task, &mut file, started, log)?; + } + + Ok(()) +} + +fn extract_or_read_via_agent( + hubris: &HubrisArchive, + core: &mut dyn Core, + subargs: &DumpArgs, + log: &Logger, +) -> Result<()> { + let mut out = DumpAgentCore::new(HubrisFlashMap::new(hubris)?); + let started = Some(Instant::now()); + + let mut agent = get_dump_agent(hubris, core, subargs, log)?; + let area = subargs.extract.map(DumpArea::ByIndex); + + let task = read_dump(&mut agent, area, &mut out, subargs, log)?; + + // + // If this was a whole-system dump, we will leave our state initialized + // to assure that it will be ready to take subsequent task dumps (unless + // explicitly asked not to). + // + if task.is_none() { + if !subargs.retain_state { + info!(log, "resetting dump agent state"); + agent.initialize_dump()?; + } else { + info!(log, "retaining dump agent state"); + } } - hubris.dump(&mut out, task, subargs.dumpfile.as_deref(), started, log)?; + let (filename, mut file) = humility_dump::open_dump_file( + hubris, + task, + subargs.dumpfile.as_deref(), + )?; + info!(log, "dumping to {filename:?}"); + hubris.dump(&mut out, task, &mut file, started, log)?; + + Ok(()) +} + +fn init_dump_agent( + hubris: &HubrisArchive, + core: &mut dyn Core, + subargs: &DumpArgs, + log: &Logger, +) -> Result<()> { + let mut agent = get_dump_agent(hubris, core, subargs, log)?; + + info!(log, "initializing dump agent state"); + agent.initialize_dump()?; + Ok(()) +} + +fn dump_via_agent( + hubris: &HubrisArchive, + core: &mut dyn Core, + subargs: &DumpArgs, + log: &Logger, +) -> Result<()> { + let (filename, mut file) = humility_dump::open_dump_file( + hubris, + None, + subargs.dumpfile.as_deref(), + )?; + info!(log, "dumping to {filename:?}"); + humility_dump::take_system_dump( + hubris, + core, + &mut file, + subargs.force_hiffy_agent, + subargs.force_overwrite, + log, + )?; Ok(()) } @@ -737,7 +748,13 @@ fn dump_task_via_agent( log, )?; assert!(task.is_some()); - hubris.dump(&mut out, task, subargs.dumpfile.as_deref(), started, log)?; + let (filename, mut file) = humility_dump::open_dump_file( + hubris, + task, + subargs.dumpfile.as_deref(), + )?; + info!(log, "dumping to {filename:?}"); + hubris.dump(&mut out, task, &mut file, started, log)?; Ok(()) } @@ -748,43 +765,30 @@ fn dump_list( subargs: &DumpArgs, log: &Logger, ) -> Result<()> { - let mut agent = get_dump_agent(hubris, core, subargs, log)?; + use humility_dump::DumpType; - println!("{:4} {:21} {:10} SIZE", "AREA", "TASK", "TIME"); - let headers = agent.read_dump_headers(false)?; - - if headers.is_empty() || headers[0].0.dumper == humpty::DUMPER_NONE { - return Ok(()); - } - - if headers[0].1.is_none() { - let size = headers - .iter() - .filter(|&(h, _)| h.dumper != humpty::DUMPER_NONE) - .fold(0, |ttl, (h, _)| ttl + h.written); - - println!("{:>4} {:21} {:<10} {size}", 0, "", "-"); - return Ok(()); - } - - let areas = task_areas(&headers); - - for (area, (task, headers)) in &areas { - let size = headers.iter().fold(0, |ttl, h| ttl + h.written); + let dumps = humility_dump::list_dumps_via_agent( + hubris, + subargs.force_hiffy_agent, + core, + log, + )?; - println!( - "{area:>4} {:21} {:<10} {size}", - match hubris.lookup_module(HubrisTask::Task(task.id.into())) { - Ok(module) => match headers[0].contents { - humpty::DUMP_CONTENTS_SINGLETASK => module.name.to_owned(), - humpty::DUMP_CONTENTS_TASKREGION => - format!("{} [region]", module.name.to_owned()), - c => bail!("unknown contents type: {c}"), - }, - _ => "".to_owned(), - }, - task.time, - ); + println!("{:4} {:21} {:10} SIZE", "AREA", "TASK", "TIME"); + match dumps { + DumpType::None => return Ok(()), + DumpType::System { size } => { + println!("{:>4} {:21} {:<10} {size}", 0, "", "-"); + return Ok(()); + } + DumpType::Tasks(tasks) => { + for t in tasks { + println!( + "{:>4} {:21} {:<10} {}", + t.area, t.task_name, t.time, t.size + ); + } + } } Ok(()) @@ -806,9 +810,7 @@ fn dump_all( if headers[0].1.is_none() { // Extract the full-system dump via the usual method drop(agent); - let mut subargs = subargs.clone(); - subargs.force_read = true; - dump_via_agent(hubris, core, &subargs, log) + extract_or_read_via_agent(hubris, core, subargs, log) } else { let areas = task_areas(&headers); for (area, (task, headers)) in &areas { @@ -842,7 +844,11 @@ fn dump_all( )?; assert!(task.is_some()); - hubris.dump(&mut out, task, Some(&dumpfile), started, log)?; + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&dumpfile)?; + hubris.dump(&mut out, task, &mut file, started, log)?; } if !subargs.retain_state { @@ -888,22 +894,28 @@ fn dumpcmd(subargs: DumpArgs, context: &mut ExecutionContext) -> Result<()> { info!(log, "--force-dump-agent is implied by --task"); } dump_task_via_agent(hubris, core, &subargs, log) - } else if core.is_net() - || subargs.force_dump_agent - || subargs.force_read - || subargs.extract.is_some() + } else if subargs.initialize_dump_agent { + init_dump_agent(hubris, core, &subargs, log) + } else if subargs.force_read || subargs.extract.is_some() { + extract_or_read_via_agent(hubris, core, &subargs, log) + } else if subargs.simulate_dumper + || subargs.emulate_dumper + || subargs.simulate_task_dump.is_some() { + simulate_dump_via_agent(hubris, core, &subargs, log) + } else if core.is_net() || subargs.force_dump_agent { dump_via_agent(hubris, core, &subargs, log) } else { - if subargs.initialize_dump_agent { - bail!("must also use --force-dump-agent to initialize dump agent"); - } - core.halt()?; info!(log, "core halted"); - let rval = - hubris.dump(core, None, subargs.dumpfile.as_deref(), None, log); + let (filename, mut file) = humility_dump::open_dump_file( + hubris, + None, + subargs.dumpfile.as_deref(), + )?; + info!(log, "dumping to {filename:?}"); + let rval = hubris.dump(core, None, &mut file, None, log); if !subargs.leave_halted { core.run()?; diff --git a/cmd/hydrate/Cargo.toml b/cmd/hydrate/Cargo.toml index 145d8366d..2c2f4fbc1 100644 --- a/cmd/hydrate/Cargo.toml +++ b/cmd/hydrate/Cargo.toml @@ -8,7 +8,7 @@ description = "Rehydrate a bare memory dump" humility.workspace = true humility-arch-arm.workspace = true humility-cli.workspace = true - +humility-dump.workspace = true hubpack.workspace = true anyhow.workspace = true diff --git a/cmd/hydrate/src/lib.rs b/cmd/hydrate/src/lib.rs index 583625470..d7b783157 100644 --- a/cmd/hydrate/src/lib.rs +++ b/cmd/hydrate/src/lib.rs @@ -175,18 +175,18 @@ fn run(subargs: HydrateArgs, context: &mut ExecutionContext) -> Result<()> { } let mut core = DryCore { mem, flash: HubrisFlashMap::new(archive)? }; - archive.dump( - &mut core, - Some(humpty::DumpTask { - magic: humpty::DUMP_TASK_MAGIC, - id: info.task_index, - pad: 0u32, - time: info.crash_time, - }), - subargs.out.as_deref(), - Some(std::time::Instant::now()), - log, - ) + let t = Some(humpty::DumpTask { + magic: humpty::DUMP_TASK_MAGIC, + id: info.task_index, + pad: 0u32, + time: info.crash_time, + }); + + let (filename, mut file) = + humility_dump::open_dump_file(archive, t, subargs.out.as_deref())?; + + info!(log, "dumping to {filename:?}"); + archive.dump(&mut core, t, &mut file, Some(std::time::Instant::now()), log) } humility_cmd!(HydrateArgs, run); diff --git a/humility-core/src/hubris.rs b/humility-core/src/hubris.rs index dbb60c003..f9a4745e0 100644 --- a/humility-core/src/hubris.rs +++ b/humility-core/src/hubris.rs @@ -11,10 +11,10 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; use std::convert::TryInto; use std::fmt::{self, Write}; -use std::fs::{self, OpenOptions}; +use std::fs::{self}; use std::mem::size_of; use std::num::TryFromIntError; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str::{self, FromStr}; use std::time::Instant; @@ -3240,13 +3240,12 @@ impl HubrisArchive { &self, core: &mut dyn crate::core::Core, task: Option, - dumpfile: Option<&Path>, + mut file: impl std::io::Write, started: Option, log: &Logger, ) -> Result<()> { use indicatif::{HumanBytes, HumanDuration}; use indicatif::{ProgressBar, ProgressStyle}; - use std::io::Write; let segments = self.dump_segments(core, task, true)?; let nsegs = segments.len(); @@ -3317,32 +3316,9 @@ impl HubrisArchive { let mut offset = header.e_phoff as u32 + (header.e_phentsize * header.e_phnum) as u32; - let filename = match dumpfile { - Some(filename) => filename.to_owned(), - None => { - let prefix = match task { - Some(task) => { - let t = HubrisTask::Task(task.id as u32); - format!("hubris.core.{}.", self.lookup_module(t)?.name) - } - None => "hubris.core.".to_string(), - }; - - (0..) - .map(|i| PathBuf::from(format!("{prefix}{i}"))) - .find(|f| !f.exists()) - .unwrap() - } - }; - // // Write our ELF header // - let mut file = - OpenOptions::new().write(true).create_new(true).open(&filename)?; - - info!(log, "dumping to {filename:?}"); - file.iowrite_with(header, ctx)?; let mut bytes = [0x0u8; goblin::elf32::program_header::SIZEOF_PHDR]; diff --git a/humility-dump/Cargo.toml b/humility-dump/Cargo.toml new file mode 100644 index 000000000..029217d68 --- /dev/null +++ b/humility-dump/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "humility-dump" +version = "0.1.0" +edition.workspace = true + +[dependencies] +# anyhow is only for compatibiltiy +anyhow.workspace = true +humility.workspace = true +thiserror.workspace = true +humility-dump-agent.workspace = true +humpty.workspace = true +slog.workspace = true + +[dev-dependencies] +humility.workspace = true +humility-net-core.workspace = true +humility-log.workspace = true diff --git a/humility-dump/examples/system_net_dump.rs b/humility-dump/examples/system_net_dump.rs new file mode 100644 index 000000000..2cbc5834b --- /dev/null +++ b/humility-dump/examples/system_net_dump.rs @@ -0,0 +1,36 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! "Hey can you grab a hubris dump" + +use humility::hubris::HubrisArchive; +use humility::net::ScopedV6Addr; +use humility_net_core::attach_net; + +fn main() { + let log = humility_log::init(false); + + let hubris = std::env::var("HUMILITY_ARCHIVE").unwrap(); + let hubris = HubrisArchive::load_from_path(&hubris, &log).unwrap(); + + let addr = std::env::var("HUMILITY_IP").unwrap(); + let addr: ScopedV6Addr = addr.parse().unwrap(); + + let mut core = + attach_net(addr, &hubris, std::time::Duration::from_secs(5), &log) + .unwrap(); + + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open("my_dump.0") + .unwrap(); + + humility_dump::take_system_dump( + &hubris, &mut core, &mut file, false, // just use UDPRPC + false, // don't force an overwrite + &log, + ) + .unwrap(); +} diff --git a/humility-dump/src/lib.rs b/humility-dump/src/lib.rs new file mode 100644 index 000000000..549ba9568 --- /dev/null +++ b/humility-dump/src/lib.rs @@ -0,0 +1,453 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Library to support hubris dumps +//! +//! These functions can be used to collect a system dump or extract +//! individual task crash dumps + +#![warn(missing_docs)] +use humility::core::Core; +use humility::hubris::{HubrisArchive, HubrisFlashMap, HubrisTask}; +use humility::log::{Logger, warn}; +use humility_dump_agent::{ + DumpAgent, DumpAgentCore, DumpAgentExt, DumpArea, HiffyDumpAgent, + UdpDumpAgent, task_areas, +}; +use std::fs::OpenOptions; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +/// Error returned from dumping +#[derive(Debug, thiserror::Error)] +pub enum DumpError { + /// Tried to take a dump when there were pending dumps. The + /// dumps should be extracted first before trying to take a dump. + #[error("cannot take a dump while one is already in progress")] + DumpAlreadyInProgress, + /// Error creating UDP agent + #[error("error creating UDP agent")] + UdpAgent(#[source] anyhow::Error), + /// Error creating hiffy agent + #[error("error creating hiffy agent")] + HiffyAgent(#[source] anyhow::Error), + /// Error reading dump headers + #[error("error reading dump headers")] + DumpHeaders(#[source] anyhow::Error), + /// Tried to extract a system dump when a task dump was expected + #[error("found system dump, expected task dump(s)")] + SystemDump, + /// Error creating hubris flash map + #[error("error creating hubris flash map")] + FlashMap(#[source] anyhow::Error), + /// Error reading dump via agent + #[error("error reading dump")] + ReadDump(#[source] anyhow::Error), + /// Error writing dump + #[error("error writing dump")] + HubrisDump(#[source] anyhow::Error), + /// Error taking a dump + #[error("error taking a dump")] + TakeDump(#[source] anyhow::Error), + /// Dump has indicated failure but this is likely due to space + /// exhaustion. The dump can be extracted but may be incomplete + #[error("dump space exhausted")] + DumpSpaceExhaustion(#[source] anyhow::Error), + /// Dump has indicated failure but some dump contents appear to + /// have been written. This can be extractec but may be incomplete. + #[error("dump incomplete")] + IncompleteDump(#[source] anyhow::Error), + /// Expected to get a task + #[error("expected task")] + ExpectedTask, + /// Error getting dump segments + #[error("error getting dump segments")] + DumpSegments(#[source] anyhow::Error), + /// Error initializing dump + #[error("error initializing dump")] + InitializeDump(#[source] anyhow::Error), + /// Error initializing segments + #[error("error initialziing segments")] + InitializeSegments(#[source] anyhow::Error), + /// Error setting timeout + #[error("error setting timeout")] + SetTimeout(#[source] anyhow::Error), + /// Unknown dump contents found + #[error("unknown contents type {0}")] + UnknownContents(u8), + /// Error opening dump file for writing + #[error("opening dump file for writing")] + OpenError(#[source] std::io::Error), + /// Error finding task name + #[error("error finding task name")] + TaskName(#[source] anyhow::Error), +} + +/// Opens a file for use in a system dump. This uses the naming scheme +/// previously found in humility core (hubris.core.0, hubris.core.1 etc.) +/// Return the file name along with the file handle +pub fn open_dump_file( + hubris: &HubrisArchive, + task: Option, + default: Option<&Path>, +) -> Result<(PathBuf, std::fs::File), DumpError> { + let filename = match default { + Some(filename) => filename.to_owned(), + None => { + let prefix = match task { + Some(task) => { + let t = HubrisTask::Task(task.id as u32); + format!( + "hubris.core.{}.", + hubris + .lookup_module(t) + .map_err(DumpError::TaskName)? + .name + ) + } + None => "hubris.core.".to_string(), + }; + + (0..) + .map(|i| PathBuf::from(format!("{prefix}{i}"))) + .find(|f| !f.exists()) + .unwrap() + } + }; + + let f = OpenOptions::new() + .write(true) + .create_new(true) + .open(&filename) + .map_err(DumpError::OpenError)?; + + Ok((filename, f)) +} + +fn get_dump_agent<'a>( + hubris: &'a HubrisArchive, + core: &'a mut dyn Core, + force_hiffy_agent: bool, + log: &Logger, +) -> Result, DumpError> { + // Find the dump agent task name. This is usually `dump_agent`, but that's + // not guaranteed; what *is* guaranteed is that it implements the DumpAgent + // interface. + let dump_agent_task = + hubris.lookup_module_by_iface("DumpAgent").map(|t| t.task); + + if core.is_net() + && !force_hiffy_agent + && dump_agent_task + .map(|t| hubris.does_task_have_feature(t, "net").unwrap()) + .unwrap_or(false) + { + let imageid = hubris.image_id(); + + Ok(Box::new( + UdpDumpAgent::new(core, imageid, log) + .map_err(DumpError::UdpAgent)?, + )) + } else { + // This is a mostly uselss timeout because it only gets updated with + // net cores. + let timeout = std::time::Duration::from_millis(5000); + Ok(Box::new( + HiffyDumpAgent::new(hubris, core, timeout, log) + .map_err(DumpError::HiffyAgent)?, + )) + } +} + +/// Extract all task dumps +pub fn extract_all_task_dumps( + hubris: &HubrisArchive, + core: &mut dyn Core, + use_hiffy: bool, + log: &Logger, +) -> Result<(), DumpError> { + let mut agent = get_dump_agent(hubris, core, use_hiffy, log)?; + let headers = + agent.read_dump_headers(false).map_err(DumpError::DumpHeaders)?; + if headers.is_empty() || headers[0].0.dumper == humpty::DUMPER_NONE { + return Ok(()); + } + + if headers[0].1.is_none() { + return Err(DumpError::SystemDump); + } + + let areas = task_areas(&headers); + for (area, (task, headers)) in &areas { + let task_name = + match hubris.lookup_module(HubrisTask::Task(task.id.into())) { + Ok(module) => match headers[0].contents { + humpty::DUMP_CONTENTS_SINGLETASK => module.name.to_owned(), + humpty::DUMP_CONTENTS_TASKREGION => { + format!("{}.region", module.name.to_owned()) + } + c => return Err(DumpError::UnknownContents(c)), + }, + _ => "".to_owned(), + }; + + let filename = (0..) + .map(|i| PathBuf::from(format!("hubris.core.{task_name}.{i}"))) + .find(|f| !f.exists()) + .unwrap(); + + let mut dumpfile = OpenOptions::new() + .write(true) + .create_new(true) + .open(&filename) + .map_err(DumpError::OpenError)?; + + let mut out = DumpAgentCore::new( + HubrisFlashMap::new(hubris).map_err(DumpError::FlashMap)?, + ); + let started = Some(Instant::now()); + + let task = agent + .read_dump(Some(DumpArea::ByIndex(*area)), &mut out, false, log) + .map_err(DumpError::ReadDump)?; + + if task.0.is_none() { + return Err(DumpError::ExpectedTask); + } + hubris + .dump(&mut out, task.0, &mut dumpfile, started, log) + .map_err(DumpError::HubrisDump)?; + } + + Ok(()) +} + +/// Extract an existing system dump +pub fn extract_system_dump( + hubris: &HubrisArchive, + core: &mut dyn Core, + file: impl std::io::Write, + use_hiffy: bool, + log: &Logger, +) -> Result<(), DumpError> { + let started = Instant::now(); + let mut agent = get_dump_agent(hubris, core, use_hiffy, log)?; + extract_system_dump_via_agent(hubris, &mut *agent, file, started, log) +} + +fn extract_system_dump_via_agent( + hubris: &HubrisArchive, + mut agent: &mut dyn DumpAgent, + file: impl std::io::Write, + started: std::time::Instant, + log: &Logger, +) -> Result<(), DumpError> { + let mut out = DumpAgentCore::new( + HubrisFlashMap::new(hubris).map_err(DumpError::FlashMap)?, + ); + + let _ = agent + .read_dump(None, &mut out, false, log) + .map_err(DumpError::ReadDump)?; + + // Explicitly reset the dump state so we can take more + agent.initialize_dump().map_err(DumpError::InitializeDump)?; + + hubris + .dump(&mut out, None, file, Some(started), log) + .map_err(DumpError::HubrisDump)?; + + Ok(()) +} + +/// Take and collect a dump via the agent. If a file is not +/// specified the file will be saved according to the default +/// humility logic (hubris.core.0, hubris.core.1 etc.) +pub fn take_system_dump( + hubris: &HubrisArchive, + core: &mut dyn Core, + file: impl std::io::Write, + use_hiffy: bool, + force_overwrite: bool, + log: &Logger, +) -> Result<(), DumpError> { + let segments = hubris + .dump_segments(core, None, false) + .map_err(DumpError::DumpSegments)?; + let mut agent = get_dump_agent(hubris, core, use_hiffy, log)?; + let header = agent.read_dump_header().map_err(DumpError::DumpHeaders)?; + + if header.dumper != humpty::DUMPER_NONE && !force_overwrite { + return Err(DumpError::DumpAlreadyInProgress); + } + + agent.initialize_dump().map_err(DumpError::InitializeDump)?; + + agent + .initialize_segments(&segments) + .map_err(DumpError::InitializeSegments)?; + + // + // We are about to disappear for -- as the kids say -- a minute. + // Set our timeout to be a literal minute so we don't prematurely + // give up. + // + + let started = Instant::now(); + agent + .core() + .set_timeout(std::time::Duration::new(60, 0)) + .map_err(DumpError::SetTimeout)?; + + let r = agent.take_dump().map_err(|err| { + if let Ok(all) = agent.read_dump_headers(true) { + let c = all + .iter() + .filter(|&&(h, _)| h.dumper != humpty::DUMPER_NONE) + .count(); + if c == all.len() { + DumpError::DumpSpaceExhaustion(err) + } else if c != 0 { + DumpError::IncompleteDump(err) + } else { + DumpError::TakeDump(err) + } + } else { + DumpError::TakeDump(err) + } + }); + + // Not all these are fatal at the moment. Just log a warning for a + // few of these. + match r { + Ok(_) => (), + Err(DumpError::DumpSpaceExhaustion(err)) => { + warn!( + log, + "dump has indicated failure ({err:#}), but this is \ + likely due to space exhaustion; \ + dump will be extracted but may be incomplete!" + ); + } + Err(DumpError::IncompleteDump(err)) => { + warn!( + log, + "dump has indicated failure ({err:#}), but some dump \ + contents appear to have been written; \ + dump will be extracted but may be incomplete!" + ); + } + _ => { + return r; + } + } + + extract_system_dump_via_agent(hubris, &mut *agent, file, started, log)?; + + Ok(()) +} + +/// Representation of a task name from a dump +pub enum TaskDumpName { + /// A single task (e.g. `hf`) + SingleTask(String), + /// A task region + TaskRegion(String), + /// Unknown region + Unknown, +} + +impl std::fmt::Display for TaskDumpName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::SingleTask(s) => s.fmt(f), + Self::TaskRegion(s) => write!(f, "{s} [region]"), + Self::Unknown => "".fmt(f), + } + } +} + +/// Meta information about an available task dump +pub struct TaskDumpData { + /// Area number + pub area: usize, + /// Total size of dump + pub size: u32, + /// Task name or region + pub task_name: TaskDumpName, + /// Time the task was dumped + pub time: u64, +} + +/// Available dump types +pub enum DumpType { + /// No dumps + None, + /// A whole system dump + System { + /// Size of the system dump + size: u32, + }, + /// Individual Hubris tasks + Tasks(Vec), +} + +/// List all available dumps +pub fn list_dumps_via_agent( + hubris: &HubrisArchive, + use_hiffy: bool, + core: &mut dyn Core, + log: &Logger, +) -> Result { + let mut agent = get_dump_agent(hubris, core, use_hiffy, log)?; + + let headers = + agent.read_dump_headers(false).map_err(DumpError::DumpHeaders)?; + + // raw: false should mean we never get humpty::DUMPER_NONE here but + // double check this later? + if headers.is_empty() || headers[0].0.dumper == humpty::DUMPER_NONE { + return Ok(DumpType::None); + } + + if headers[0].1.is_none() { + let size = headers + .iter() + .filter(|&(h, _)| h.dumper != humpty::DUMPER_NONE) + .fold(0, |ttl, (h, _)| ttl + h.written); + + return Ok(DumpType::System { size }); + } + + let areas = task_areas(&headers); + + let mut tasks = vec![]; + + for (area, (task, headers)) in &areas { + let size = headers.iter().fold(0, |ttl, h| ttl + h.written); + + let task_name = + match hubris.lookup_module(HubrisTask::Task(task.id.into())) { + Ok(module) => match headers[0].contents { + humpty::DUMP_CONTENTS_SINGLETASK => { + TaskDumpName::SingleTask(module.name.to_owned()) + } + humpty::DUMP_CONTENTS_TASKREGION => { + TaskDumpName::TaskRegion(module.name.to_owned()) + } + c => return Err(DumpError::UnknownContents(c)), + }, + _ => TaskDumpName::Unknown, + }; + + tasks.push(TaskDumpData { + area: *area, + size, + task_name, + time: task.time, + }); + } + + Ok(DumpType::Tasks(tasks)) +}