breakmancer 0.9.0

Drop a breakpoint into any shell.
Documentation
use std::{borrow::Cow, str::FromStr, time::Duration};

use magic_wormhole::{
    rendezvous::{RendezvousError, DEFAULT_RENDEZVOUS_SERVER},
    AppConfig, AppID, Code, MailboxConnection, Wormhole, WormholeError,
};
use serde::{Deserialize, Serialize};
use tracing::Level;
use tracing_subscriber::FmtSubscriber;

#[derive(Clone, Serialize, Deserialize)]
pub struct WormholeAppVersion {
    bm_exp_ver: u8,
}

pub const APP_CONFIG: AppConfig<WormholeAppVersion> = AppConfig {
    id: AppID(Cow::Borrowed("org.timmc.breakmancer.experiments")),
    rendezvous_url: Cow::Borrowed(DEFAULT_RENDEZVOUS_SERVER),
    app_version: WormholeAppVersion { bm_exp_ver: 1 },
};

pub const PREDEFINED_CODE: &str = "12097-abcw3i2tyiv";

/// Connect to a predefined code, allocating the Nameplate.
///
/// Note that when run with multiple parties, *all* attempt to allocate the
/// Nameplate. This makes it an inaccurate representation of C/B interaction.
///
/// What this demonstrates:
///
/// - Server determines the Mailbox string when connecting via Nameplate, and
///   it is alphanumeric in the default rendezvous server.
/// - The Mailbox string is independent of the Nameplate and the Password;
///   clients only receive the same Mailbox if they connect to the same Nameplate
///   *while that nameplate is claimed* (before it is released *or* expired).
/// - When the first party is waiting in the Nameplate for the second party
///   to connect, and has a dirty disconnect, a zombie connection is left
///   behind in the Nameplate. If the first party reconnects, there are now
///   two connections (one dead) and the second party will be unable to join
///   the Nameplate ("crowded" error).
/// - On successive runs, even several minutes after all clients have exited,
///   it's pretty common to get "crowded" errors from the Nameplate. The server
///   doesn't readily detect zombie connections.
async fn experiment_connect1() {
    let code = Code::from_str(PREDEFINED_CODE).unwrap();
    println!("Making mailbox connection...");
    let mc = MailboxConnection::connect(APP_CONFIG, code, true)
        .await
        .unwrap();
    println!("...done");

    // At this point, the client has joined the Mailbox.

    tokio::time::sleep(Duration::from_secs(5)).await;

    // Waits for other party to connect to the Mailbox, which
    // completes the Wormhole.
    println!("Making wormhole...");
    let w = Wormhole::connect(mc).await.unwrap();
    println!("...done");
    dbg!(w.peer_version());

    tokio::time::sleep(Duration::from_secs(20)).await;
    w.close().await.unwrap();
}

// Controller/breakpoint pair. Connect to a predefined code, allocating
// only on controller side. Both sides loop and tolerate some errors.
//
// What this demonstrates:
//
// - Nameplate works as a mutex to let one Breakpoint through at a time.
// - Controller *sometimes* fails to reconnect to Nameplate, getting a
//   "crowded" message, despite having released the Nameplate.

/// Connect to a predefined code as controller, allocating nameplate.
///
/// Try reconnecting after each breakpoint exits.
async fn experiment_connect2_controller() {
    loop {
        let code = Code::from_str(PREDEFINED_CODE).unwrap();
        println!("Making mailbox connection (allocate if necessary)...");
        let mc = MailboxConnection::connect(APP_CONFIG, code, true)
            .await
            .unwrap();
        println!("...done");

        // At this point, the client has joined the Mailbox.

        tokio::time::sleep(Duration::from_secs(5)).await;

        // Waits for other party to connect to the Mailbox, which
        // completes the Wormhole.
        println!("Making wormhole...");
        let w = Wormhole::connect(mc).await.unwrap();
        println!("...done");

        tokio::time::sleep(Duration::from_secs(20)).await;
        w.close().await.unwrap();
        println!("Closed wormhole.");

        println!("Will try to connect to another breakpoint.");
    }
}

/// Connect to a predefined code as breakpoint, waiting for nameplate allocation.
///
/// Try connecting until nameplate allocation succeeds.
async fn experiment_connect2_breakpoint() {
    let mc = loop {
        let code = Code::from_str(PREDEFINED_CODE).unwrap();
        println!("Attempting mailbox connection (no allocate)...");
        match MailboxConnection::connect(APP_CONFIG, code, false).await {
            Ok(mc) => break mc,
            Err(err) => match err {
                WormholeError::UnclaimedNameplate(_) => {
                    println!("Nameplate not yet allocated by controller; sleeping.");
                    tokio::time::sleep(Duration::from_secs(5)).await;
                    continue;
                }
                WormholeError::ServerError(RendezvousError::Server(err)) => {
                    if &*err == "crowded" {
                        println!("Nameplate busy with another breakpoint; sleeping.");
                        tokio::time::sleep(Duration::from_secs(5)).await;
                        continue;
                    } else {
                        panic!("Unexpected server error: {err}");
                    }
                }
                err => panic!("Unexpected error: {err:?}"),
            },
        }
    };
    println!("...done");

    // At this point, the client has joined the Mailbox.

    tokio::time::sleep(Duration::from_secs(5)).await;

    // Waits for other party to connect to the Mailbox, which
    // completes the Wormhole.
    println!("Making wormhole...");
    let w = Wormhole::connect(mc).await.unwrap();
    println!("...done");

    tokio::time::sleep(Duration::from_secs(20)).await;
    w.close().await.unwrap();
}

#[tokio::main]
async fn main() {
    // Turn on debug logging in wormhole
    let trace_subscriber = FmtSubscriber::builder()
        .with_max_level(Level::DEBUG)
        .finish();
    tracing::subscriber::set_global_default(trace_subscriber).unwrap();

    let args: Vec<String> = std::env::args().collect();
    match args[1].as_str() {
        "1" => experiment_connect1().await,
        "2" => match args[2].as_str() {
            "c" => experiment_connect2_controller().await,
            "b" => experiment_connect2_breakpoint().await,
            _ => unimplemented!(),
        },
        _ => unimplemented!(),
    }
}