exfiltrate 0.4.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Built-in commands and the registry used by the embedded debug server.
//!
//! # Why a map
//!
//! The registry used to be a `Vec` scanned linearly, taking the first name that
//! matched. Two commands could therefore share a name, with the winner decided
//! by registration order — and since this crate registers its own commands from
//! `begin()`, and sibling crates register theirs from their own initialisers,
//! that order is not something the application author controls. `list` showed
//! both entries while dispatch could only ever reach one, so the description a
//! user read and the code that ran were related only by luck.
//!
//! A map makes the collision impossible to miss: the first registration wins and
//! the second is refused with a diagnostic naming both. First-wins rather than
//! last-wins because it is the stable half of the pair — re-registering does not
//! silently change behaviour that was already observed.

use crate::command::Command;
use std::collections::BTreeMap;
use wasm_lite_std::rwlock::RwLock;

mod build_info;
mod env;
mod help;
mod list;
mod memory;
mod panics;
mod terminate;
mod threads;
pub(crate) mod uptime;

/// The global registry of available commands, keyed by name.
///
/// A `BTreeMap` rather than a `HashMap`: lookup is what matters and both are
/// fast enough at these sizes, but ordered iteration means `list` output is
/// stable across runs, which matters when an agent diffs it or a generated skill
/// file is checked into a repository.
pub(crate) static COMMANDS: RwLock<BTreeMap<&'static str, Box<dyn Command>>> =
    RwLock::new(BTreeMap::new());

/// Why a command could not be registered.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RegisterError {
    /// A command with this name is already registered.
    Duplicate {
        /// The contested name.
        name: &'static str,
        /// The description of the registration that is keeping the name.
        existing_description: String,
        /// The description of the registration that was refused.
        rejected_description: String,
    },
}

impl std::fmt::Display for RegisterError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RegisterError::Duplicate {
                name,
                existing_description,
                rejected_description,
            } => write!(
                f,
                "exfiltrate: refusing to register a second command named {name:?}.\n  \
                 keeping:  {existing_description}\n  \
                 refused:  {rejected_description}\n  \
                 Rename one of them; a dotted prefix such as `mycrate.{name}` is the convention."
            ),
        }
    }
}

impl std::error::Error for RegisterError {}

/// Inserts a command, refusing a name that is already taken.
pub(crate) fn insert(command: Box<dyn Command>) -> Result<(), RegisterError> {
    let name = command.name();
    let mut registry = COMMANDS.lock_sync_write();
    match registry.get(name) {
        Some(existing) => Err(RegisterError::Duplicate {
            name,
            existing_description: existing.short_description().to_string(),
            rejected_description: command.short_description().to_string(),
        }),
        None => {
            registry.insert(name, command);
            Ok(())
        }
    }
}

/// Registers the built-in commands.
///
/// `help` and `list` are unconditional — they are how the rest is discovered.
/// Everything else is governed by [`Batteries`](crate::Batteries).
pub(crate) fn register_commands(config: &crate::Config) {
    let mut refused = Vec::new();
    let mut register = |command: Box<dyn Command>| {
        if let Err(error) = insert(command) {
            refused.push(error);
        }
    };

    register(Box::new(help::Help));
    register(Box::new(list::List));
    // Always registered: it costs nothing until a subsystem registers a
    // provider, and a build where `snapshot` is missing is indistinguishable
    // from one where nothing has registered yet.
    register(Box::new(crate::provider::Snapshot));
    #[cfg(not(target_arch = "wasm32"))]
    register(Box::new(terminate::Terminate));

    let batteries = config.batteries;
    if batteries.build_info {
        register(Box::new(build_info::BuildInfoCommand));
    }
    if batteries.uptime {
        register(Box::new(uptime::Uptime));
    }
    if batteries.panics {
        register(Box::new(panics::Panics));
    }
    // `threads`, `memory` and `env` are meaningless or unavailable in a browser:
    // there is no /proc, no RSS to report, and no process environment. Rather
    // than register a command that always answers "unsupported", they are simply
    // absent — and `list` is the thing that tells you which commands this build
    // actually has, so a target-conditional command is not a surprise.
    #[cfg(not(target_arch = "wasm32"))]
    {
        if batteries.threads {
            register(Box::new(threads::Threads));
        }
        if batteries.memory {
            register(Box::new(memory::Memory));
        }
        if batteries.env {
            register(Box::new(env::Env));
        }
    }

    for error in refused {
        crate::diagnostic(&error.to_string());
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use exfiltrate_internal::command::Response;

    struct Named(&'static str, &'static str);

    impl Command for Named {
        fn name(&self) -> &'static str {
            self.0
        }
        fn short_description(&self) -> &'static str {
            self.1
        }
        fn full_description(&self) -> &'static str {
            self.1
        }
        fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
            Ok(self.1.into())
        }
    }

    // The registry is process-global, so these use names no other test or
    // built-in command uses rather than trying to isolate the map itself.
    #[test]
    fn a_duplicate_registration_is_refused_and_names_both_sides() {
        insert(Box::new(Named("registry_test_dup", "the first one"))).unwrap();
        let error = insert(Box::new(Named("registry_test_dup", "the second one"))).unwrap_err();
        let RegisterError::Duplicate {
            name,
            existing_description,
            rejected_description,
        } = &error;
        assert_eq!(*name, "registry_test_dup");
        assert_eq!(existing_description, "the first one");
        assert_eq!(rejected_description, "the second one");

        let message = error.to_string();
        assert!(message.contains("the first one"), "{message}");
        assert!(message.contains("the second one"), "{message}");
    }

    #[test]
    fn the_first_registration_keeps_the_name() {
        insert(Box::new(Named("registry_test_first", "original"))).unwrap();
        let _ = insert(Box::new(Named("registry_test_first", "impostor")));
        let registry = COMMANDS.lock_sync_read();
        let found = registry.get("registry_test_first").unwrap();
        assert_eq!(found.short_description(), "original");
    }

    #[test]
    fn lookup_does_not_depend_on_registration_order() {
        insert(Box::new(Named("registry_test_zzz", "last alphabetically"))).unwrap();
        insert(Box::new(Named("registry_test_aaa", "first alphabetically"))).unwrap();
        let registry = COMMANDS.lock_sync_read();
        assert_eq!(
            registry
                .get("registry_test_zzz")
                .unwrap()
                .short_description(),
            "last alphabetically"
        );
        assert_eq!(
            registry
                .get("registry_test_aaa")
                .unwrap()
                .short_description(),
            "first alphabetically"
        );
    }
}