This commit is contained in:
Ultradesu
2026-07-10 12:45:47 +03:00
commit 057436adb6
17 changed files with 7052 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "federation-net-demo"
version = "0.1.0"
description = "CLI demo for the federation-net P2P engine"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
federation-net = { path = "../../crates/federation-net" }
tokio = { workspace = true }
serde = { workspace = true }
clap = { workspace = true }
tracing-subscriber = { workspace = true }
anyhow = { workspace = true }
+250
View File
@@ -0,0 +1,250 @@
//! CLI demo for the federation-net P2P engine.
//!
//! Starts a peer, prints a shareable ticket and exchanges chat messages with
//! connected peers. Run two instances (optionally on different machines) and
//! pass the first peer's ticket to the second via `--connect`.
use std::io::Write;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Context;
use clap::Parser;
use federation_net::{
ConnectionDirection, NetworkConfig, NetworkEngine, NetworkEvent, NetworkId, PeerTicket,
SchemaId,
};
use tokio::io::{AsyncBufReadExt, BufReader};
/// Domain message type of the demo application.
///
/// The library knows nothing about this type; it is defined entirely here.
#[derive(Debug, serde::Serialize, serde::Deserialize)]
enum DemoMessage {
Text { sender: String, body: String },
Ping { nonce: u64 },
}
/// Schema name of the demo message format.
const DEMO_SCHEMA: &str = "demo-message-v1";
#[derive(Debug, Parser)]
#[command(
name = "federation-net-demo",
about = "P2P chat demo for federation-net"
)]
struct Args {
/// Directory for the persistent peer identity.
#[arg(long)]
data_dir: PathBuf,
/// Human-readable name of the network to join.
#[arg(long)]
network_id: String,
/// Display name used as the sender of chat messages.
#[arg(long)]
name: String,
/// Ticket of a peer to connect to on startup.
#[arg(long)]
connect: Option<String>,
/// Enable verbose logging.
#[arg(long)]
verbose: bool,
}
fn prompt(name: &str) {
print!("{name}> ");
let _ = std::io::stdout().flush();
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let filter = if args.verbose {
"federation_net=debug,federation_net_demo=debug,info"
} else {
"warn"
};
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)),
)
.with_writer(std::io::stderr)
.init();
let config = NetworkConfig::builder()
.data_dir(&args.data_dir)
.network_id(NetworkId::from_name(&args.network_id))
.schema_id(SchemaId::from_name(DEMO_SCHEMA))
.build()
.context("invalid configuration")?;
let (engine, mut events) = NetworkEngine::<DemoMessage>::start(config)
.await
.context("failed to start the network engine")?;
println!("Endpoint ID: {}", engine.endpoint_id());
println!("Network ID: {}", engine.network_id());
println!("Schema ID: {}", engine.schema_id());
match engine.ticket().await {
Ok(ticket) => println!("Ticket: {ticket}"),
Err(err) => eprintln!("Could not create a ticket yet: {err}"),
}
if let Some(ticket) = &args.connect {
let ticket: PeerTicket = ticket.parse().context("invalid ticket")?;
match engine.connect(ticket).await {
Ok(peer_id) => {
println!("Connected to: {peer_id}");
println!("Type a message and press Enter.");
}
Err(err) => {
println!("Connection rejected: {err}");
let _ = engine.shutdown().await;
std::process::exit(1);
}
}
} else {
println!("Waiting for peers...");
}
let name = args.name.clone();
let mut stdin = BufReader::new(tokio::io::stdin()).lines();
prompt(&name);
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
println!();
break;
}
line = stdin.next_line() => {
match line {
Ok(Some(line)) => {
if handle_line(&engine, &name, line.trim()).await {
break;
}
prompt(&name);
}
Ok(None) => break, // stdin closed
Err(err) => {
eprintln!("failed to read stdin: {err}");
break;
}
}
}
event = events.recv() => {
match event {
Some(event) => {
println!();
print_event(&name, event);
prompt(&name);
}
None => {
println!("Engine stopped.");
break;
}
}
}
}
}
engine.shutdown().await.context("shutdown failed")?;
println!("Bye.");
// The blocking thread behind `tokio::io::stdin()` keeps the runtime alive
// until stdin closes; the engine is already shut down, so exit directly.
let _ = std::io::stdout().flush();
std::process::exit(0);
}
/// Handles one line of user input. Returns `true` when the user asked to quit.
async fn handle_line(engine: &NetworkEngine<DemoMessage>, name: &str, line: &str) -> bool {
match line {
"" => false,
"/quit" => true,
"/peers" => {
let peers = engine.connected_peers();
if peers.is_empty() {
println!("No connected peers.");
} else {
for peer in peers {
println!("{peer}");
}
}
false
}
"/ticket" => {
match engine.ticket().await {
Ok(ticket) => println!("Ticket: {ticket}"),
Err(err) => println!("Could not create a ticket: {err}"),
}
false
}
"/ping" => {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or_default();
broadcast(engine, DemoMessage::Ping { nonce }).await;
false
}
body => {
broadcast(
engine,
DemoMessage::Text {
sender: name.to_string(),
body: body.to_string(),
},
)
.await;
false
}
}
}
/// Sends a message to every connected peer.
async fn broadcast(engine: &NetworkEngine<DemoMessage>, message: DemoMessage) {
let peers = engine.connected_peers();
if peers.is_empty() {
println!("No connected peers.");
return;
}
for peer in peers {
if let Err(err) = engine.send(peer, &message).await {
println!("failed to send to {peer}: {err}");
}
}
}
fn print_event(name: &str, event: NetworkEvent<DemoMessage>) {
match event {
NetworkEvent::PeerConnected { peer_id, direction } => {
let direction = match direction {
ConnectionDirection::Incoming => "incoming",
ConnectionDirection::Outgoing => "outgoing",
};
println!("Peer connected ({direction}): {peer_id}");
}
NetworkEvent::PeerDisconnected { peer_id, reason } => match reason {
Some(reason) => println!("Peer disconnected: {peer_id} ({reason})"),
None => println!("Peer disconnected: {peer_id}"),
},
NetworkEvent::MessageReceived { peer_id, message } => match message {
DemoMessage::Text { body, .. } => {
println!("{name} received from {peer_id}: {body}");
}
DemoMessage::Ping { nonce } => {
println!("{name} received ping from {peer_id} (nonce {nonce})");
}
},
NetworkEvent::ProtocolError { peer_id, error } => match peer_id {
Some(peer_id) => println!("Protocol error with {peer_id}: {error}"),
None => println!("Protocol error: {error}"),
},
}
}