exfiltrate 0.3.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The thread registry behind the `threads` command.
//!
//! # Why this is not simply "list the threads"
//!
//! No crate-level Rust API enumerates the threads of its own process after the
//! fact. `std::thread` hands out a [`Thread`](std::thread::Thread) for the
//! *current* thread and nothing else, so a debugger linked in as a library can
//! only know about a thread if something told it. There are two sources, and
//! this module uses both:
//!
//! * **The operating system.** Linux exposes `/proc/self/task/<tid>/comm`, which
//!   is a genuine after-the-fact enumeration covering every thread in the
//!   process — including threads spawned by C libraries that have never heard of
//!   this crate. macOS and Windows have no equivalent that does not mean taking
//!   on a platform crate.
//! * **Voluntary registration.** [`register_thread`](crate::register_thread)
//!   adds the calling thread, and a thread-local guard removes it again when the
//!   thread exits, so the list does not accumulate ghosts.
//!
//! The registry is what makes this work on a target with no `/proc`, and it is
//! also the only thing that can ever carry more than a name — a priority, a
//! workload description — because that information exists only at spawn time.

use std::sync::LazyLock;
use wasm_lite_std::Mutex;

/// One thread the registry knows about.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ThreadRecord {
    /// The thread's name, or a placeholder when it has none.
    pub name: String,
    /// The `ThreadId`, rendered — it is deliberately opaque in `std`.
    pub id: String,
    /// A free-form note supplied at registration, such as what the thread does.
    pub note: Option<String>,
}

static REGISTRY: LazyLock<Mutex<Vec<ThreadRecord>>> = LazyLock::new(|| Mutex::new(Vec::new()));

thread_local! {
    /// Removes this thread's registry entry when the thread exits.
    ///
    /// Without this the registry would only ever grow, and a list of threads
    /// that includes ones that finished ten minutes ago is worse than no list:
    /// it is a list you would act on.
    static DEREGISTER: Deregister = const { Deregister };
}

struct Deregister;

impl Drop for Deregister {
    fn drop(&mut self) {
        let id = current_id();
        REGISTRY.with_mut_sync(|registry| registry.retain(|record| record.id != id));
    }
}

fn current_id() -> String {
    format!("{:?}", std::thread::current().id())
}

/// Adds the calling thread to the registry.
///
/// Re-registering the same thread replaces its entry rather than duplicating it.
pub(crate) fn register(note: Option<String>) {
    let id = current_id();
    let name = std::thread::current()
        .name()
        .map(str::to_string)
        .unwrap_or_else(|| "<unnamed>".to_string());
    let record = ThreadRecord { name, id, note };
    REGISTRY.with_mut_sync(|registry| {
        match registry
            .iter_mut()
            .find(|existing| existing.id == record.id)
        {
            Some(existing) => *existing = record,
            None => registry.push(record),
        }
    });
    // Touching the thread-local is what arms the destructor; the value itself
    // does nothing.
    DEREGISTER.with(|_| {});
}

/// Every thread that voluntarily registered, sorted by name for a stable listing.
pub(crate) fn registered() -> Vec<ThreadRecord> {
    let mut records = REGISTRY.with_sync(|registry| registry.clone());
    records.sort_by(|a, b| (&a.name, &a.id).cmp(&(&b.name, &b.id)));
    records
}

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

    #[test]
    fn a_registered_thread_appears_and_leaves_when_it_exits() {
        let handle = std::thread::Builder::new()
            .name("threads_test_worker".to_string())
            .spawn(|| {
                register(Some("does the test's work".to_string()));
                let seen = registered();
                assert!(
                    seen.iter()
                        .any(|record| record.name == "threads_test_worker"),
                    "{seen:?}"
                );
            })
            .unwrap();
        handle.join().unwrap();

        // The thread-local destructor has run by the time join returns.
        let after = registered();
        assert!(
            !after
                .iter()
                .any(|record| record.name == "threads_test_worker"),
            "a finished thread must not linger in the registry: {after:?}"
        );
    }

    #[test]
    fn registering_twice_replaces_rather_than_duplicates() {
        std::thread::Builder::new()
            .name("threads_test_twice".to_string())
            .spawn(|| {
                register(Some("first".to_string()));
                register(Some("second".to_string()));
                let mine: Vec<ThreadRecord> = registered()
                    .into_iter()
                    .filter(|record| record.name == "threads_test_twice")
                    .collect();
                assert_eq!(mine.len(), 1, "{mine:?}");
                assert_eq!(mine[0].note.as_deref(), Some("second"));
            })
            .unwrap()
            .join()
            .unwrap();
    }
}