mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Big rewrite.
This commit is contained in:
@@ -1,413 +0,0 @@
|
||||
//! Protocol benchmark driver.
|
||||
//!
|
||||
//! Running the example normally prints the in-process benchmark table. Running it with `tools`
|
||||
//! builds the standalone operation binaries and feeds them to external profiling tools.
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
use unshell::protocol::tree::{
|
||||
ChildRoute, Endpoint, EndpointOutcome, Ingress, LeafSpec, LocalEvent, ProtocolEndpoint,
|
||||
};
|
||||
use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet};
|
||||
|
||||
const SAMPLES: usize = 500;
|
||||
const ITERS: usize = 10_000;
|
||||
const TOOL_ITERS: usize = 10_000;
|
||||
|
||||
fn main() {
|
||||
if std::env::args().nth(1).as_deref() == Some("tools") {
|
||||
run_external_tools();
|
||||
return;
|
||||
}
|
||||
|
||||
println!("protocol benchmark");
|
||||
println!("samples: {SAMPLES}");
|
||||
println!("iterations/sample: {ITERS}");
|
||||
println!();
|
||||
|
||||
let benches = [
|
||||
bench_encode_call(),
|
||||
bench_decode_call(),
|
||||
bench_forward_call_receive(),
|
||||
bench_local_call_receive(),
|
||||
bench_hook_data_receive(),
|
||||
];
|
||||
|
||||
println!(
|
||||
"{:32} {:>14} {:>14} {:>14}",
|
||||
"benchmark", "mean ns/op", "stddev", "samples"
|
||||
);
|
||||
for bench in benches {
|
||||
println!(
|
||||
"{:32} {:>14.2} {:>14.2} {:>14}",
|
||||
bench.name, bench.mean_ns, bench.stddev_ns, bench.samples
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Run `cargo run --example bench -- tools` to build and execute");
|
||||
println!("the standalone operation binaries under strace, perf, and heaptrack.");
|
||||
}
|
||||
|
||||
struct BenchResult {
|
||||
name: &'static str,
|
||||
mean_ns: f64,
|
||||
stddev_ns: f64,
|
||||
samples: usize,
|
||||
}
|
||||
|
||||
fn bench_encode_call() -> BenchResult {
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: path(&["root"]),
|
||||
dst_path: path(&["root", "worker"]),
|
||||
dst_leaf: Some(String::from("service")),
|
||||
hook_id: None,
|
||||
};
|
||||
let message = CallMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![7; 64],
|
||||
response_hook: None,
|
||||
};
|
||||
|
||||
run_bench("encode_call", || {
|
||||
let frame =
|
||||
encode_packet(black_box(&header), black_box(&message)).expect("encode should work");
|
||||
black_box(frame.len());
|
||||
})
|
||||
}
|
||||
|
||||
fn bench_decode_call() -> BenchResult {
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: path(&["root"]),
|
||||
dst_path: path(&["root", "worker"]),
|
||||
dst_leaf: Some(String::from("service")),
|
||||
hook_id: None,
|
||||
};
|
||||
let message = CallMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![9; 64],
|
||||
response_hook: None,
|
||||
};
|
||||
let frame = encode_packet(&header, &message).expect("seed frame should encode");
|
||||
|
||||
run_bench("decode_call", || {
|
||||
let parsed = decode_frame(black_box(frame.as_slice())).expect("decode should work");
|
||||
let call = parsed.deserialize_call().expect("call should deserialize");
|
||||
black_box(call.data.len());
|
||||
})
|
||||
}
|
||||
|
||||
fn bench_forward_call_receive() -> BenchResult {
|
||||
run_prebuilt_bench(
|
||||
"forward_call_receive",
|
||||
build_forward_call_cases,
|
||||
|(mut root, frame)| {
|
||||
let outcome = root
|
||||
.receive(&Ingress::Local, frame)
|
||||
.expect("forward receive should work");
|
||||
black_box(matches!(outcome, EndpointOutcome::Forward { .. }));
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn bench_local_call_receive() -> BenchResult {
|
||||
run_prebuilt_bench(
|
||||
"local_call_receive",
|
||||
build_local_call_cases,
|
||||
|(mut endpoint, frame)| {
|
||||
let outcome = endpoint
|
||||
.receive(&Ingress::Parent, frame)
|
||||
.expect("local call should work");
|
||||
match black_box(outcome) {
|
||||
EndpointOutcome::Local(LocalEvent::Call { .. }) => {}
|
||||
other => panic!("expected local call event, got {other:?}"),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn bench_hook_data_receive() -> BenchResult {
|
||||
run_prebuilt_bench(
|
||||
"hook_data_receive",
|
||||
build_hook_data_cases,
|
||||
|(mut host, frame)| {
|
||||
let outcome = host
|
||||
.receive(&Ingress::Child(path(&["worker"])), frame)
|
||||
.expect("hook data should work");
|
||||
match black_box(outcome) {
|
||||
EndpointOutcome::Local(LocalEvent::Data { .. }) => {}
|
||||
other => panic!("expected local data event, got {other:?}"),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn run_bench(name: &'static str, mut op: impl FnMut()) -> BenchResult {
|
||||
let mut samples = Vec::with_capacity(SAMPLES);
|
||||
for _ in 0..SAMPLES {
|
||||
let start = Instant::now();
|
||||
for _ in 0..ITERS {
|
||||
op();
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos() as f64 / ITERS as f64;
|
||||
samples.push(elapsed);
|
||||
}
|
||||
summarize(name, &samples)
|
||||
}
|
||||
|
||||
fn run_prebuilt_bench<T, F>(
|
||||
name: &'static str,
|
||||
mut build_cases: F,
|
||||
mut op: impl FnMut(T),
|
||||
) -> BenchResult
|
||||
where
|
||||
F: FnMut() -> Vec<T>,
|
||||
{
|
||||
let mut repeated = Vec::with_capacity(SAMPLES);
|
||||
for _ in 0..SAMPLES {
|
||||
let mut cases = build_cases();
|
||||
assert_eq!(cases.len(), ITERS);
|
||||
let start = Instant::now();
|
||||
for case in cases.drain(..) {
|
||||
op(case);
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos() as f64 / ITERS as f64;
|
||||
repeated.push(elapsed);
|
||||
}
|
||||
summarize(name, &repeated)
|
||||
}
|
||||
|
||||
fn build_forward_call_cases() -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> {
|
||||
(0..ITERS)
|
||||
.map(|_| {
|
||||
let mut root = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute::registered(path(&["edge"]))],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = root.allocate_hook_id();
|
||||
let frame = root
|
||||
.make_call(
|
||||
path(&["edge", "worker"]),
|
||||
Some(String::from("service")),
|
||||
String::from("example.service.v1.invoke"),
|
||||
Some(hook_id),
|
||||
vec![1; 32],
|
||||
)
|
||||
.expect("seed call should encode");
|
||||
(root, frame)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_local_call_cases() -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> {
|
||||
(0..ITERS)
|
||||
.map(|_| {
|
||||
let endpoint = ProtocolEndpoint::new(
|
||||
path(&["worker"]),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
vec![LeafSpec {
|
||||
name: String::from("service"),
|
||||
procedures: vec![String::from("example.service.v1.invoke")],
|
||||
}],
|
||||
);
|
||||
let frame = encode_packet(
|
||||
&PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: Vec::new(),
|
||||
dst_path: path(&["worker"]),
|
||||
dst_leaf: Some(String::from("service")),
|
||||
hook_id: None,
|
||||
},
|
||||
&CallMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![2; 32],
|
||||
response_hook: Some(unshell::protocol::HookTarget {
|
||||
hook_id: 42,
|
||||
return_path: Vec::new(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("seed local call should encode");
|
||||
(endpoint, frame)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_hook_data_cases() -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> {
|
||||
(0..ITERS)
|
||||
.map(|_| {
|
||||
let mut host = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute::registered(path(&["worker"]))],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = host.allocate_hook_id();
|
||||
host.make_call(
|
||||
path(&["worker"]),
|
||||
None,
|
||||
String::from("example.service.v1.invoke"),
|
||||
Some(hook_id),
|
||||
vec![3; 8],
|
||||
)
|
||||
.expect("seed active hook should encode");
|
||||
let frame = encode_packet(
|
||||
&PacketHeader {
|
||||
packet_type: PacketType::Data,
|
||||
src_path: path(&["worker"]),
|
||||
dst_path: Vec::new(),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(hook_id),
|
||||
},
|
||||
&unshell::protocol::DataMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![4; 16],
|
||||
end_hook: false,
|
||||
},
|
||||
)
|
||||
.expect("seed data should encode");
|
||||
(host, frame)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn summarize(name: &'static str, samples: &[f64]) -> BenchResult {
|
||||
let mean = samples.iter().sum::<f64>() / samples.len() as f64;
|
||||
let variance = samples
|
||||
.iter()
|
||||
.map(|sample| {
|
||||
let delta = sample - mean;
|
||||
delta * delta
|
||||
})
|
||||
.sum::<f64>()
|
||||
/ samples.len() as f64;
|
||||
|
||||
BenchResult {
|
||||
name,
|
||||
mean_ns: mean,
|
||||
stddev_ns: variance.sqrt(),
|
||||
samples: samples.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(parts: &[&str]) -> Vec<String> {
|
||||
parts.iter().map(|part| String::from(*part)).collect()
|
||||
}
|
||||
|
||||
fn run_external_tools() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
build_examples(root);
|
||||
|
||||
let ops = [
|
||||
("encode_call", "op_encode_call"),
|
||||
("decode_call", "op_decode_call"),
|
||||
("forward_call_receive", "op_forward_call_receive"),
|
||||
("local_call_receive", "op_local_call_receive"),
|
||||
("hook_data_receive", "op_hook_data_receive"),
|
||||
];
|
||||
|
||||
let heap_dir = root.join("heaptrack-cli");
|
||||
std::fs::create_dir_all(&heap_dir).expect("heaptrack-cli directory should be creatable");
|
||||
|
||||
for (name, binary) in ops {
|
||||
let binary_path = root.join("target/debug/examples").join(binary);
|
||||
println!();
|
||||
println!("=== {name} ===");
|
||||
run_binary(&binary_path, TOOL_ITERS, "direct run");
|
||||
run_strace(&binary_path, TOOL_ITERS);
|
||||
run_perf(&binary_path, TOOL_ITERS);
|
||||
run_heaptrack(root, &heap_dir, name, &binary_path, TOOL_ITERS);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_examples(root: &Path) {
|
||||
run_command(
|
||||
"cargo build --examples",
|
||||
Command::new("cargo")
|
||||
.arg("build")
|
||||
.arg("--examples")
|
||||
.current_dir(root),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_binary(binary: &Path, iterations: usize, label: &str) {
|
||||
run_command(label, Command::new(binary).arg(iterations.to_string()));
|
||||
}
|
||||
|
||||
fn run_strace(binary: &Path, iterations: usize) {
|
||||
run_command(
|
||||
"strace -c memory syscalls",
|
||||
Command::new("strace")
|
||||
.arg("-qq")
|
||||
.arg("-c")
|
||||
.arg("-e")
|
||||
.arg("trace=brk,mmap,mremap,munmap,mprotect,madvise")
|
||||
.arg(binary)
|
||||
.arg(iterations.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_perf(binary: &Path, iterations: usize) {
|
||||
run_command(
|
||||
"perf stat",
|
||||
Command::new("perf")
|
||||
.arg("stat")
|
||||
.arg("-e")
|
||||
.arg("task-clock,cycles,instructions,branches,branch-misses,cache-references,cache-misses")
|
||||
.arg(binary)
|
||||
.arg(iterations.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_heaptrack(root: &Path, heap_dir: &Path, name: &str, binary: &Path, iterations: usize) {
|
||||
let prefix = heap_dir.join(format!("{name}.zst"));
|
||||
run_command(
|
||||
"heaptrack --record-only",
|
||||
Command::new("heaptrack")
|
||||
.arg("--record-only")
|
||||
.arg("-o")
|
||||
.arg(&prefix)
|
||||
.arg(binary)
|
||||
.arg(iterations.to_string())
|
||||
.current_dir(root),
|
||||
);
|
||||
|
||||
let recorded = PathBuf::from(format!("{}.zst", prefix.display()));
|
||||
run_command(
|
||||
"heaptrack_print summary",
|
||||
Command::new("heaptrack_print")
|
||||
.arg("-f")
|
||||
.arg(recorded)
|
||||
.arg("-n")
|
||||
.arg("4")
|
||||
.arg("-s")
|
||||
.arg("2")
|
||||
.current_dir(root),
|
||||
);
|
||||
}
|
||||
|
||||
fn run_command(label: &str, command: &mut Command) {
|
||||
println!("--- {label} ---");
|
||||
let output = command
|
||||
.output()
|
||||
.unwrap_or_else(|error| panic!("{label} failed to launch: {error}"));
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{label} failed with status {}",
|
||||
output.status
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//! Standalone benchmark binary for `decode_call`.
|
||||
|
||||
#[path = "support/bench_common.rs"]
|
||||
mod common;
|
||||
|
||||
fn main() {
|
||||
let iterations = common::iterations_from_args(1_000);
|
||||
let checksum = common::run_decode_call(iterations);
|
||||
println!("decode_call iterations={iterations} checksum={checksum}");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//! Standalone benchmark binary for `encode_call`.
|
||||
|
||||
#[path = "support/bench_common.rs"]
|
||||
mod common;
|
||||
|
||||
fn main() {
|
||||
let iterations = common::iterations_from_args(1_000);
|
||||
let checksum = common::run_encode_call(iterations);
|
||||
println!("encode_call iterations={iterations} checksum={checksum}");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//! Standalone benchmark binary for `forward_call_receive`.
|
||||
|
||||
#[path = "support/bench_common.rs"]
|
||||
mod common;
|
||||
|
||||
fn main() {
|
||||
let iterations = common::iterations_from_args(1_000);
|
||||
let checksum = common::run_forward_call_receive(iterations);
|
||||
println!("forward_call_receive iterations={iterations} checksum={checksum}");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//! Standalone benchmark binary for `hook_data_receive`.
|
||||
|
||||
#[path = "support/bench_common.rs"]
|
||||
mod common;
|
||||
|
||||
fn main() {
|
||||
let iterations = common::iterations_from_args(1_000);
|
||||
let checksum = common::run_hook_data_receive(iterations);
|
||||
println!("hook_data_receive iterations={iterations} checksum={checksum}");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//! Standalone benchmark binary for `local_call_receive`.
|
||||
|
||||
#[path = "support/bench_common.rs"]
|
||||
mod common;
|
||||
|
||||
fn main() {
|
||||
let iterations = common::iterations_from_args(1_000);
|
||||
let checksum = common::run_local_call_receive(iterations);
|
||||
println!("local_call_receive iterations={iterations} checksum={checksum}");
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
//! Shared helpers for the standalone benchmark operation binaries.
|
||||
//!
|
||||
//! These helpers keep each operation binary tiny while still exposing the same setup and checksum
|
||||
//! logic to strace, perf, and heaptrack.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::hint::black_box;
|
||||
|
||||
use unshell::protocol::tree::{
|
||||
ChildRoute, Endpoint, EndpointOutcome, Ingress, LeafSpec, LocalEvent, ProtocolEndpoint,
|
||||
};
|
||||
use unshell::protocol::{CallMessage, PacketHeader, PacketType, decode_frame, encode_packet};
|
||||
|
||||
pub fn iterations_from_args(default: usize) -> usize {
|
||||
std::env::args()
|
||||
.nth(1)
|
||||
.map(|value| {
|
||||
value
|
||||
.parse::<usize>()
|
||||
.expect("iterations must be a positive integer")
|
||||
})
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn run_encode_call(iterations: usize) -> usize {
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: path(&["root"]),
|
||||
dst_path: path(&["root", "worker"]),
|
||||
dst_leaf: Some(String::from("service")),
|
||||
hook_id: None,
|
||||
};
|
||||
let message = CallMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![7; 64],
|
||||
response_hook: None,
|
||||
};
|
||||
|
||||
let mut checksum = 0usize;
|
||||
for _ in 0..iterations {
|
||||
let frame =
|
||||
encode_packet(black_box(&header), black_box(&message)).expect("encode should work");
|
||||
checksum = checksum.wrapping_add(frame.len());
|
||||
}
|
||||
black_box(checksum)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn run_decode_call(iterations: usize) -> usize {
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: path(&["root"]),
|
||||
dst_path: path(&["root", "worker"]),
|
||||
dst_leaf: Some(String::from("service")),
|
||||
hook_id: None,
|
||||
};
|
||||
let message = CallMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![9; 64],
|
||||
response_hook: None,
|
||||
};
|
||||
let frame = encode_packet(&header, &message).expect("seed frame should encode");
|
||||
|
||||
let mut checksum = 0usize;
|
||||
for _ in 0..iterations {
|
||||
let parsed = decode_frame(black_box(frame.as_slice())).expect("decode should work");
|
||||
let call = parsed.deserialize_call().expect("call should deserialize");
|
||||
checksum = checksum
|
||||
.wrapping_add(call.data.len())
|
||||
.wrapping_add(call.procedure_id.len())
|
||||
.wrapping_add(call.response_hook.is_some() as usize);
|
||||
}
|
||||
black_box(checksum)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn run_forward_call_receive(iterations: usize) -> usize {
|
||||
let cases = build_forward_call_cases(iterations);
|
||||
run_cases(cases, |(mut root, frame)| {
|
||||
let outcome = root
|
||||
.receive(&Ingress::Local, frame)
|
||||
.expect("forward receive should work");
|
||||
match outcome {
|
||||
EndpointOutcome::Forward { route, frame } => {
|
||||
route_value(route).wrapping_add(frame.len())
|
||||
}
|
||||
EndpointOutcome::Local(_) => 0,
|
||||
EndpointOutcome::Dropped => usize::from(true),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn run_local_call_receive(iterations: usize) -> usize {
|
||||
let cases = build_local_call_cases(iterations);
|
||||
run_cases(cases, |(mut endpoint, frame)| {
|
||||
let outcome = endpoint
|
||||
.receive(&Ingress::Parent, frame)
|
||||
.expect("local call should work");
|
||||
match outcome {
|
||||
EndpointOutcome::Local(LocalEvent::Call { header, message }) => header
|
||||
.dst_path
|
||||
.len()
|
||||
.wrapping_add(header.src_path.len())
|
||||
.wrapping_add(header.dst_leaf.as_ref().map_or(0, String::len))
|
||||
.wrapping_add(message.data.len())
|
||||
.wrapping_add(message.procedure_id.len()),
|
||||
other => panic!("expected local call event, got {other:?}"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
pub fn run_hook_data_receive(iterations: usize) -> usize {
|
||||
let cases = build_hook_data_cases(iterations);
|
||||
run_cases(cases, |(mut host, frame)| {
|
||||
let outcome = host
|
||||
.receive(&Ingress::Child(path(&["worker"])), frame)
|
||||
.expect("hook data should work");
|
||||
match outcome {
|
||||
EndpointOutcome::Local(LocalEvent::Data {
|
||||
header, message, ..
|
||||
}) => (header.hook_id.unwrap_or_default() as usize)
|
||||
.wrapping_add(message.data.len())
|
||||
.wrapping_add(message.procedure_id.len())
|
||||
.wrapping_add(message.end_hook as usize),
|
||||
other => panic!("expected local data event, got {other:?}"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn run_cases<T>(cases: Vec<T>, mut op: impl FnMut(T) -> usize) -> usize {
|
||||
let mut checksum = 0usize;
|
||||
for case in cases {
|
||||
checksum = checksum.wrapping_add(op(case));
|
||||
}
|
||||
black_box(checksum)
|
||||
}
|
||||
|
||||
fn build_forward_call_cases(
|
||||
iterations: usize,
|
||||
) -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> {
|
||||
(0..iterations)
|
||||
.map(|_| {
|
||||
let mut root = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute::registered(path(&["edge"]))],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = root.allocate_hook_id();
|
||||
let frame = root
|
||||
.make_call(
|
||||
path(&["edge", "worker"]),
|
||||
Some(String::from("service")),
|
||||
String::from("example.service.v1.invoke"),
|
||||
Some(hook_id),
|
||||
vec![1; 32],
|
||||
)
|
||||
.expect("seed call should encode");
|
||||
(root, frame)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_local_call_cases(
|
||||
iterations: usize,
|
||||
) -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> {
|
||||
(0..iterations)
|
||||
.map(|_| {
|
||||
let endpoint = ProtocolEndpoint::new(
|
||||
path(&["worker"]),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
vec![LeafSpec {
|
||||
name: String::from("service"),
|
||||
procedures: vec![String::from("example.service.v1.invoke")],
|
||||
}],
|
||||
);
|
||||
let frame = encode_packet(
|
||||
&PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: Vec::new(),
|
||||
dst_path: path(&["worker"]),
|
||||
dst_leaf: Some(String::from("service")),
|
||||
hook_id: None,
|
||||
},
|
||||
&CallMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![2; 32],
|
||||
response_hook: Some(unshell::protocol::HookTarget {
|
||||
hook_id: 42,
|
||||
return_path: Vec::new(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("seed local call should encode");
|
||||
(endpoint, frame)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_hook_data_cases(
|
||||
iterations: usize,
|
||||
) -> Vec<(ProtocolEndpoint, unshell::protocol::FrameBytes)> {
|
||||
(0..iterations)
|
||||
.map(|_| {
|
||||
let mut host = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute::registered(path(&["worker"]))],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = host.allocate_hook_id();
|
||||
host.make_call(
|
||||
path(&["worker"]),
|
||||
None,
|
||||
String::from("example.service.v1.invoke"),
|
||||
Some(hook_id),
|
||||
vec![3; 8],
|
||||
)
|
||||
.expect("seed active hook should encode");
|
||||
let frame = encode_packet(
|
||||
&PacketHeader {
|
||||
packet_type: PacketType::Data,
|
||||
src_path: path(&["worker"]),
|
||||
dst_path: Vec::new(),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(hook_id),
|
||||
},
|
||||
&unshell::protocol::DataMessage {
|
||||
procedure_id: String::from("example.service.v1.invoke"),
|
||||
data: vec![4; 16],
|
||||
end_hook: false,
|
||||
},
|
||||
)
|
||||
.expect("seed data should encode");
|
||||
(host, frame)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn path(parts: &[&str]) -> Vec<String> {
|
||||
parts.iter().map(|part| String::from(*part)).collect()
|
||||
}
|
||||
|
||||
fn route_value(route: unshell::protocol::tree::RouteDecision) -> usize {
|
||||
match route {
|
||||
unshell::protocol::tree::RouteDecision::Child(index) => index,
|
||||
unshell::protocol::tree::RouteDecision::Local => usize::MAX - 2,
|
||||
unshell::protocol::tree::RouteDecision::Parent => usize::MAX - 1,
|
||||
unshell::protocol::tree::RouteDecision::Drop => usize::MAX,
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
//! Crossbeam-channel router leaf example.
|
||||
//!
|
||||
//! This example wires a root controller to an `agent` node, promotes a staged
|
||||
//! child connection on that agent via the `add_connection` procedure, and then
|
||||
//! queries the grandchild's connection snapshot through a fully routed call/reply
|
||||
//! exchange.
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
||||
use unshell::leaves::crossbeam_channel::{
|
||||
ConnectionRequest, ConnectionSnapshot, CrossbeamChannelLeaf, CrossbeamEnvelope,
|
||||
};
|
||||
use unshell::protocol::tree::ProtocolEndpoint;
|
||||
use unshell::protocol::tree::{
|
||||
ChildRoute, Endpoint, EndpointOutcome, Ingress, LeafRuntime, decode_call_input,
|
||||
encode_call_reply,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut network = ChannelNetwork::new()?;
|
||||
|
||||
network.call_root(
|
||||
path(&["agent"]),
|
||||
CrossbeamChannelLeaf::protocol_procedure_id("add_connection").expect("procedure exists"),
|
||||
encode_call_reply(&ConnectionRequest {
|
||||
peer_path: path(&["agent", "child"]),
|
||||
})?,
|
||||
)?;
|
||||
|
||||
let reply = network.call_root(
|
||||
path(&["agent", "child"]),
|
||||
CrossbeamChannelLeaf::protocol_procedure_id("get_connections").expect("procedure exists"),
|
||||
encode_call_reply(&())?,
|
||||
)?;
|
||||
let snapshot = decode_call_input::<ConnectionSnapshot>(reply.as_slice())?;
|
||||
|
||||
println!("child parent: {:?}", snapshot.parent);
|
||||
println!("child children: {:?}", snapshot.children);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ChannelNetwork {
|
||||
root: ProtocolEndpoint,
|
||||
root_to_agent: Sender<CrossbeamEnvelope>,
|
||||
root_rx: Receiver<CrossbeamEnvelope>,
|
||||
agent: ChannelNode,
|
||||
child: ChannelNode,
|
||||
}
|
||||
|
||||
impl ChannelNetwork {
|
||||
fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let (mut agent, root_to_agent) = ChannelNode::new(path(&["agent"]));
|
||||
let (mut child, agent_to_child) = ChannelNode::new(path(&["agent", "child"]));
|
||||
let (agent_to_root, root_rx) = unbounded();
|
||||
|
||||
let root = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute::registered(path(&["agent"]))],
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
agent.stage_connection(Vec::new(), agent_to_root);
|
||||
agent.connect_staged(Vec::new())?;
|
||||
|
||||
child.stage_connection(path(&["agent"]), root_to_agent.clone());
|
||||
child.connect_staged(path(&["agent"]))?;
|
||||
|
||||
agent.stage_connection(path(&["agent", "child"]), agent_to_child);
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
root_to_agent,
|
||||
root_rx,
|
||||
agent,
|
||||
child,
|
||||
})
|
||||
}
|
||||
|
||||
fn call_root(
|
||||
&mut self,
|
||||
dst_path: Vec<String>,
|
||||
procedure_id: String,
|
||||
data: Vec<u8>,
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let hook_id = self.root.allocate_hook_id();
|
||||
let outcome = self.root.send_call(
|
||||
dst_path,
|
||||
Some(CrossbeamChannelLeaf::protocol_leaf_name()),
|
||||
procedure_id,
|
||||
Some(hook_id),
|
||||
data,
|
||||
)?;
|
||||
let EndpointOutcome::Forward { frame, .. } = outcome else {
|
||||
return Err("root call did not forward".into());
|
||||
};
|
||||
self.root_to_agent.send(CrossbeamEnvelope {
|
||||
ingress: Ingress::Parent,
|
||||
frame,
|
||||
})?;
|
||||
|
||||
for _ in 0..16 {
|
||||
let mut progress = 0usize;
|
||||
progress += self.agent.drain()?;
|
||||
progress += self.child.drain()?;
|
||||
|
||||
while let Ok(envelope) = self.root_rx.try_recv() {
|
||||
progress += 1;
|
||||
let outcome = self.root.receive(&envelope.ingress, envelope.frame)?;
|
||||
if let EndpointOutcome::Local(event) = outcome {
|
||||
match event {
|
||||
unshell::protocol::tree::LocalEvent::Data { message, .. } => {
|
||||
return Ok(message.data);
|
||||
}
|
||||
unshell::protocol::tree::LocalEvent::Fault { message, .. } => {
|
||||
return Err(format!("routed call faulted: {:?}", message.fault).into());
|
||||
}
|
||||
unshell::protocol::tree::LocalEvent::Call { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if progress == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Err("timed out waiting for routed reply".into())
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelNode {
|
||||
runtime: LeafRuntime<CrossbeamChannelLeaf>,
|
||||
rx: Receiver<CrossbeamEnvelope>,
|
||||
}
|
||||
|
||||
impl ChannelNode {
|
||||
fn new(path: Vec<String>) -> (Self, Sender<CrossbeamEnvelope>) {
|
||||
let (tx, rx) = unbounded();
|
||||
let endpoint = ProtocolEndpoint::new(
|
||||
path,
|
||||
None,
|
||||
Vec::new(),
|
||||
vec![CrossbeamChannelLeaf::protocol_leaf_spec()],
|
||||
);
|
||||
(
|
||||
Self {
|
||||
runtime: LeafRuntime::new(endpoint, CrossbeamChannelLeaf::default()),
|
||||
rx,
|
||||
},
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
fn stage_connection(&mut self, peer_path: Vec<String>, sender: Sender<CrossbeamEnvelope>) {
|
||||
let _ = self.runtime.leaf_mut().stage_connection(peer_path, sender);
|
||||
}
|
||||
|
||||
fn connect_staged(&mut self, peer_path: Vec<String>) -> Result<(), Box<dyn Error>> {
|
||||
let runtime = &mut self.runtime;
|
||||
let mut leaf = core::mem::take(runtime.leaf_mut());
|
||||
let result = leaf.connect_staged(runtime.endpoint_mut(), peer_path);
|
||||
*runtime.leaf_mut() = leaf;
|
||||
result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn drain(&mut self) -> Result<usize, Box<dyn Error>> {
|
||||
let mut processed = 0usize;
|
||||
while let Ok(envelope) = self.rx.try_recv() {
|
||||
let outcome = self
|
||||
.runtime
|
||||
.receive_routed(&envelope.ingress, envelope.frame)?;
|
||||
self.runtime.route_forwarded(outcome.forwarded)?;
|
||||
processed += 1;
|
||||
}
|
||||
Ok(processed)
|
||||
}
|
||||
}
|
||||
|
||||
fn path(parts: &[&str]) -> Vec<String> {
|
||||
parts.iter().map(|part| (*part).to_owned()).collect()
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
//! Small end-to-end example for the `leaf!` and `Procedure` macros.
|
||||
//!
|
||||
//! This stays entirely local. A controller endpoint opens one hook-backed procedure against a
|
||||
//! single in-process leaf runtime, and the example decodes the returned reply payload.
|
||||
|
||||
use std::error::Error;
|
||||
use std::{collections::BTreeMap, convert::Infallible, string::String};
|
||||
|
||||
use rkyv::{Archive, Deserialize, Serialize};
|
||||
use unshell::protocol::tree::{
|
||||
Call, ChildRoute, EndpointOutcome, HookKey, Ingress, OutgoingData, Procedure, ProcedureEffect,
|
||||
ProcedureRuntime, ProcedureStore, ProtocolEndpoint,
|
||||
};
|
||||
use unshell::protocol::{PacketType, decode_frame};
|
||||
use unshell::{Procedure, leaf};
|
||||
|
||||
#[derive(Default)]
|
||||
struct EchoLeaf {
|
||||
sessions: BTreeMap<HookKey, EchoOpen>,
|
||||
}
|
||||
|
||||
#[leaf(id = "org.example.v1.echo", procedures = [EchoOpen], endpoint_struct = EchoLeaf)]
|
||||
struct Echo;
|
||||
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct EchoRequest {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct EchoResponse {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Procedure)]
|
||||
#[procedure(leaf = EchoLeaf, name = "echo")]
|
||||
struct EchoOpen {
|
||||
prefix: String,
|
||||
return_path: Vec<String>,
|
||||
hook_id: u64,
|
||||
sent_reply: bool,
|
||||
}
|
||||
|
||||
impl ProcedureStore<EchoOpen> for EchoLeaf {
|
||||
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, EchoOpen> {
|
||||
&mut self.sessions
|
||||
}
|
||||
}
|
||||
|
||||
impl Procedure<EchoLeaf> for EchoOpen {
|
||||
type Error = Infallible;
|
||||
type Input = EchoRequest;
|
||||
|
||||
fn open(_leaf: &mut EchoLeaf, call: Call<Self::Input>) -> Result<Self, Self::Error> {
|
||||
let response_hook = call
|
||||
.response_hook
|
||||
.expect("example call declares a response hook");
|
||||
Ok(Self {
|
||||
prefix: call.input.text,
|
||||
return_path: response_hook.return_path,
|
||||
hook_id: response_hook.hook_id,
|
||||
sent_reply: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn poll(_leaf: &mut EchoLeaf, session: &mut Self) -> Result<ProcedureEffect, Self::Error> {
|
||||
if session.sent_reply {
|
||||
return Ok(ProcedureEffect::default());
|
||||
}
|
||||
session.sent_reply = true;
|
||||
Ok(ProcedureEffect::close(vec![OutgoingData {
|
||||
dst_path: session.return_path.clone(),
|
||||
hook_id: session.hook_id,
|
||||
procedure_id: EchoOpen::protocol_procedure_id(),
|
||||
data: unshell::protocol::tree::encode_call_reply(&EchoResponse {
|
||||
text: format!("echo: {}", session.prefix),
|
||||
})
|
||||
.expect("response should encode"),
|
||||
end_hook: true,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
fn path(parts: &[&str]) -> Vec<String> {
|
||||
parts.iter().map(|part| (*part).to_owned()).collect()
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let endpoint = ProtocolEndpoint::new(
|
||||
path(&["agent"]),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
vec![EchoLeaf::protocol_leaf_spec()],
|
||||
);
|
||||
let mut runtime = ProcedureRuntime::<EchoLeaf, EchoOpen>::new(endpoint, EchoLeaf::default());
|
||||
|
||||
let mut controller = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute {
|
||||
path: path(&["agent"]),
|
||||
registered: true,
|
||||
}],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = controller.allocate_hook_id();
|
||||
let controller_outcome = controller.send_call(
|
||||
path(&["agent"]),
|
||||
Some(EchoLeaf::protocol_leaf_name()),
|
||||
EchoOpen::protocol_procedure_id(),
|
||||
Some(hook_id),
|
||||
unshell::protocol::tree::encode_call_reply(&EchoRequest {
|
||||
text: String::from("hello leaf"),
|
||||
})?,
|
||||
)?;
|
||||
let EndpointOutcome::Forward { frame, .. } = controller_outcome else {
|
||||
return Err("expected controller to forward call".into());
|
||||
};
|
||||
|
||||
let receive_outcome = runtime.receive(&Ingress::Parent, frame)?;
|
||||
assert!(receive_outcome.frames.is_empty());
|
||||
let outcome = runtime.poll()?;
|
||||
let [response_frame] = outcome.frames.as_slice() else {
|
||||
return Err("expected one response frame".into());
|
||||
};
|
||||
let parsed = decode_frame(response_frame.as_slice())?;
|
||||
assert_eq!(parsed.packet_type(), PacketType::Data);
|
||||
let response = unshell::protocol::tree::decode_call_input::<EchoResponse>(
|
||||
parsed.deserialize_data()?.data.as_slice(),
|
||||
)?;
|
||||
|
||||
assert_eq!(EchoLeaf::protocol_leaf_name(), "org.example.v1.echo");
|
||||
assert_eq!(response.text, "echo: hello leaf");
|
||||
|
||||
println!(
|
||||
"leaf={} procedure={} response={}",
|
||||
EchoLeaf::protocol_leaf_name(),
|
||||
EchoOpen::protocol_procedure_id(),
|
||||
response.text,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
//! Remote shell endpoint example.
|
||||
//!
|
||||
//! This binary acts as the single remote-shell endpoint process. It connects to the controller
|
||||
//! example over TCP, feeds inbound frames into the `ProcedureRuntime`, and flushes any resulting
|
||||
//! protocol frames back to the controller.
|
||||
|
||||
use std::error::Error;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::mpsc::RecvTimeoutError;
|
||||
use std::time::Duration;
|
||||
|
||||
use unshell::leaves::remote_shell;
|
||||
use unshell::protocol::tree::{Ingress, ProcedureRuntime, ProtocolEndpoint};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut stream = TcpStream::connect(remote_shell::endpoint::LISTEN_ADDR)?;
|
||||
let frame_rx = remote_shell::endpoint::spawn_frame_reader(stream.try_clone()?);
|
||||
let endpoint = ProtocolEndpoint::new(
|
||||
agent_path(),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
vec![remote_shell::endpoint::RemoteShell::protocol_leaf_spec()],
|
||||
);
|
||||
let mut runtime = ProcedureRuntime::<
|
||||
remote_shell::endpoint::RemoteShell,
|
||||
remote_shell::endpoint::Open,
|
||||
>::new(endpoint, remote_shell::endpoint::RemoteShell::default());
|
||||
|
||||
println!(
|
||||
"connected to controller at {}",
|
||||
remote_shell::endpoint::LISTEN_ADDR
|
||||
);
|
||||
|
||||
loop {
|
||||
match frame_rx.recv_timeout(Duration::from_millis(25)) {
|
||||
Ok(result) => {
|
||||
let frame = result?;
|
||||
let outcome = runtime.receive(&Ingress::Parent, frame)?;
|
||||
remote_shell::endpoint::write_frames(&mut stream, &outcome.frames)?;
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {}
|
||||
Err(RecvTimeoutError::Disconnected) => break,
|
||||
}
|
||||
|
||||
let outcome = runtime.poll()?;
|
||||
remote_shell::endpoint::write_frames(&mut stream, &outcome.frames)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn agent_path() -> Vec<String> {
|
||||
vec![String::from("agent")]
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
//! Remote shell controller example.
|
||||
//!
|
||||
//! This binary listens for the endpoint example, opens one remote shell session, sends a few
|
||||
//! commands, and prints returned hook data until the shell closes.
|
||||
|
||||
use std::error::Error;
|
||||
use std::net::TcpListener;
|
||||
|
||||
use unshell::leaves::remote_shell;
|
||||
use unshell::leaves::remote_shell::OpenRequest;
|
||||
use unshell::protocol::tree::encode_call_reply;
|
||||
use unshell::protocol::tree::{
|
||||
ChildRoute, Endpoint, EndpointOutcome, Ingress, LocalEvent, ProtocolEndpoint,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let listener = TcpListener::bind(remote_shell::endpoint::LISTEN_ADDR)?;
|
||||
println!("listening on {}", remote_shell::endpoint::LISTEN_ADDR);
|
||||
|
||||
let (mut stream, peer_addr) = listener.accept()?;
|
||||
println!("accepted endpoint connection from {peer_addr}");
|
||||
|
||||
let frame_rx = remote_shell::endpoint::spawn_frame_reader(stream.try_clone()?);
|
||||
let mut endpoint = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute::registered(agent_path())],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = endpoint.allocate_hook_id();
|
||||
let shell_leaf_name = remote_shell::endpoint::RemoteShell::protocol_leaf_name();
|
||||
let open_procedure = remote_shell::endpoint::Open::protocol_procedure_id();
|
||||
|
||||
remote_shell::endpoint::send_forward(
|
||||
&mut stream,
|
||||
endpoint.send_call(
|
||||
agent_path(),
|
||||
Some(shell_leaf_name),
|
||||
open_procedure.clone(),
|
||||
Some(hook_id),
|
||||
encode_call_reply(&OpenRequest).expect("remote shell open payload should encode"),
|
||||
)?,
|
||||
)?;
|
||||
|
||||
for (index, command) in ["pwd\n", "whoami\n", "exit\n"].iter().enumerate() {
|
||||
remote_shell::endpoint::send_forward(
|
||||
&mut stream,
|
||||
endpoint.send_data(
|
||||
agent_path(),
|
||||
hook_id,
|
||||
open_procedure.clone(),
|
||||
command.as_bytes().to_vec(),
|
||||
index == 2,
|
||||
)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
for result in frame_rx {
|
||||
let frame = result?;
|
||||
let outcome = endpoint.receive(&Ingress::Child(agent_path()), frame)?;
|
||||
let EndpointOutcome::Local(event) = outcome else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match event {
|
||||
LocalEvent::Data { message, .. } => {
|
||||
print!("{}", String::from_utf8_lossy(&message.data));
|
||||
|
||||
if message.end_hook {
|
||||
break;
|
||||
}
|
||||
}
|
||||
LocalEvent::Fault { message, .. } => {
|
||||
eprintln!("received protocol fault: 0x{:02X}", message.fault.0);
|
||||
break;
|
||||
}
|
||||
LocalEvent::Call { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn agent_path() -> Vec<String> {
|
||||
vec![String::from("agent")]
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//! Smallest in-process `remote_shell` declaration example.
|
||||
//!
|
||||
//! This example hosts exactly one protocol endpoint with exactly one leaf and performs a local
|
||||
//! introspection request against that leaf. The important detail is that the endpoint metadata is
|
||||
//! taken from `remote_shell::endpoint::RemoteShellEndpoint::protocol_leaf_spec()`, which is
|
||||
//! generated by the `leaf!` declaration in `unshell-leaves/src/remote_shell/mod.rs`.
|
||||
//!
|
||||
//! It does not open any sockets or spawn a shell process, so it is the easiest place to verify
|
||||
//! that the shared compile-time leaf declaration and the generated endpoint host metadata line up.
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use unshell::create_endpoint;
|
||||
use unshell::leaves::remote_shell;
|
||||
use unshell::protocol::tree::{Endpoint, EndpointOutcome, LocalEvent, ProtocolEndpoint};
|
||||
use unshell::protocol::{INTROSPECTION_PROCEDURE_ID, LeafIntrospection};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut endpoint: ProtocolEndpoint =
|
||||
create_endpoint!("agent", remote_shell::endpoint::RemoteShell::default());
|
||||
let leaf_spec = remote_shell::endpoint::RemoteShell::protocol_leaf_spec();
|
||||
|
||||
let hook_id = endpoint.allocate_hook_id();
|
||||
let outcome = endpoint.send_call(
|
||||
Vec::new(),
|
||||
Some(remote_shell::endpoint::RemoteShell::protocol_leaf_name()),
|
||||
INTROSPECTION_PROCEDURE_ID,
|
||||
Some(hook_id),
|
||||
Vec::new(),
|
||||
)?;
|
||||
|
||||
let EndpointOutcome::Local(LocalEvent::Data { message, .. }) = outcome else {
|
||||
return Err("expected one local introspection response".into());
|
||||
};
|
||||
|
||||
let payload = unshell::protocol::tree::decode_call_input::<LeafIntrospection>(&message.data)?;
|
||||
println!(
|
||||
"remote-shell examples normally listen on {}",
|
||||
remote_shell::endpoint::LISTEN_ADDR
|
||||
);
|
||||
println!("endpoint id: {:?}", endpoint.local_id());
|
||||
println!("endpoint path: {:?}", endpoint.path());
|
||||
println!("declared leaf: {}", leaf_spec.name);
|
||||
println!("leaf: {}", payload.leaf_name);
|
||||
println!("procedures: {:?}", payload.procedures);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user