use std::io::{Read as _, Write as _}; use std::net::{SocketAddr, TcpListener as StdTcpListener}; use std::thread; use std::time::{SystemTime, UNIX_EPOCH}; use ruin_app::prelude::*; const SNAPSHOT_PATH: &str = "target/ruin-example05-manifest-snapshot.toml"; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{EnvFilter, fmt}; fn install_tracing() { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { EnvFilter::new( "info,\ ruin_runtime::runtime=debug,\ ruin_runtime::scheduler=debug,\ ruin_reactivity::graph=debug,\ ruin_reactivity::effect=debug,\ ruin_reactivity::event=debug", ) }); let fmt_layer = fmt::layer() .with_target(true) .with_thread_ids(true) .with_thread_names(true) .compact(); let _ = tracing_subscriber::registry() .with(filter) .with(fmt_layer) .try_init(); } #[ruin_runtime::async_main] async fn main() -> ruin_app::Result<()> { install_tracing(); let demo_server_addr = spawn_demo_server()?; App::new() .window( Window::new() .title("RUIN Async Runtime I/O") .app_id("dev.ruin.async-runtime-io") .size(1280.0, 820.0), ) .mount(view! { RuntimeIoExample(server_addr = demo_server_addr) {} }) .run() .await } #[component] fn RuntimeIoExample(server_addr: SocketAddr) -> impl IntoView { let manifest_path = use_signal(|| "Cargo.lock".to_string()); let manifest_reload = use_signal(|| 0_u64); let manifest_scroll = use_signal(|| 0.0_f32); let endpoint = use_signal(|| DemoEndpoint::Release); let endpoint_reload = use_signal(|| 0_u64); let response_scroll = use_signal(|| 0.0_f32); let save_in_flight = use_signal(|| false); let save_status = use_signal(|| Some(format!("Snapshot target: {SNAPSHOT_PATH}"))); let manifest = use_resource({ let manifest_path = manifest_path.clone(); let manifest_reload = manifest_reload.clone(); move || { let path = manifest_path.get(); let _ = manifest_reload.get(); async move { ruin_runtime::fs::read_to_string(&path) .await .map_err(|error| error.to_string()) } } }); let response = use_resource({ let endpoint = endpoint.clone(); let endpoint_reload = endpoint_reload.clone(); move || { let route = endpoint.get(); let _ = endpoint_reload.get(); async move { fetch_demo_endpoint(server_addr, route).await } } }); use_effect({ let manifest_path = manifest_path.clone(); move || { eprintln!( "example05: manifest path changed -> {}", manifest_path.get() ); } }); use_effect({ let endpoint = endpoint.clone(); move || { eprintln!( "example05: demo endpoint changed -> {}", endpoint.get().path() ); } }); use_effect({ let manifest_path = manifest_path.clone(); let manifest_reload = manifest_reload.clone(); let manifest_scroll = manifest_scroll.clone(); move || { let _ = manifest_path.get(); let _ = manifest_reload.get(); let _ = manifest_scroll.set(0.0); } }); use_effect({ let endpoint = endpoint.clone(); let endpoint_reload = endpoint_reload.clone(); let response_scroll = response_scroll.clone(); move || { let _ = endpoint.get(); let _ = endpoint_reload.get(); let _ = response_scroll.set(0.0); } }); let manifest_state = manifest.read(); let response_state = response.read(); view! { row(gap = 20.0, padding = 20.0) { block( flex = 1.0, padding = 18.0, gap = 14.0, background = surfaces::raised(), border_radius = 16.0, ) { text(role = TextRole::Heading(1), size = 28.0, weight = FontWeight::Semibold) { "Filesystem" } text(color = colors::muted(), wrap = TextWrap::Word) { "Real async file reads use `ruin_runtime::fs`, while the snapshot button writes \ the current contents back through the runtime on a background future." } row(gap = 8.0) { button( on_press = { let manifest_path = manifest_path.clone(); move |_| { let _ = manifest_path.set("Cargo.toml".to_string()); } }, ) { "Cargo.toml" } button( on_press = { let manifest_path = manifest_path.clone(); move |_| { let _ = manifest_path.set("Cargo.lock".to_string()); } }, ) { "Cargo.lock" } button( on_press = { let manifest_path = manifest_path.clone(); move |_| { let _ = manifest_path.set("missing-file.toml".to_string()); } }, ) { "Missing file" } button( on_press = { let manifest_reload = manifest_reload.clone(); move |_| { manifest_reload.update(|value| *value += 1); } }, ) { "Reload" } } text(size = 16.0, weight = FontWeight::Semibold) { "Path: "; manifest_path.clone() } match manifest_state { Pending => view! { block( padding = 14.0, background = surfaces::canvas(), border_radius = 12.0, ) { text(color = colors::muted()) { "Reading file through the runtime..." } } }, Ready(Ok(contents)) => view! { column(gap = 12.0) { row(gap = 8.0) { button( on_press = { let manifest = manifest.clone(); let save_in_flight = save_in_flight.clone(); let save_status = save_status.clone(); move |_| { if save_in_flight.get() { return; } let current_manifest = match manifest.read() { Ready(Ok(contents)) => contents, Pending => { let _ = save_status.set(Some( "Manifest is still loading; try again in a moment." .to_string(), )); return; } Ready(Err(error)) => { let _ = save_status.set(Some(format!( "Cannot snapshot the current manifest: {error}" ))); return; } }; let _ = save_in_flight.set(true); let _ = save_status .set(Some("Saving snapshot through ruin_runtime::fs...".to_string())); std::mem::drop(ruin_runtime::queue_future({ let save_in_flight = save_in_flight.clone(); let save_status = save_status.clone(); async move { let result = ruin_runtime::fs::write( SNAPSHOT_PATH, current_manifest, ) .await; let _ = save_in_flight.set(false); let completed_at = timestamp_label(); let message = match result { Ok(()) => format!( "Saved snapshot to {SNAPSHOT_PATH} at {completed_at}" ), Err(error) => { format!( "Snapshot write failed at {completed_at}: {error}" ) } }; eprintln!("example05: {message}"); let _ = save_status.set(Some(message)); } })); } }, ) { if save_in_flight.get() { "Saving snapshot..." } else { "Save snapshot" } } } text(color = colors::muted()) { "Loaded "; contents.len(); " bytes." } if let Some(status) = save_status.get() { view! { text( color = if status.contains("failed") { colors::danger() } else { colors::muted() }, wrap = TextWrap::Word, ) { status } } } else { View::from_element(Element::column()) } scroll_box( height = 500.0, offset_y = manifest_scroll.clone(), padding = 12.0, background = surfaces::canvas(), border_radius = 12.0, border = (2.0, colors::muted()), ) { text( color = colors::muted(), font_family = TextFontFamily::Monospace, ) { contents } } } }, Ready(Err(error)) => view! { block( padding = 14.0, gap = 8.0, background = surfaces::canvas(), border_radius = 12.0, border = (2.0, colors::danger()), ) { text(size = 18.0, weight = FontWeight::Semibold, color = colors::danger()) { "Failed to read file" } text(color = colors::muted(), wrap = TextWrap::Word) { error } } }, } } block( flex = 1.0, padding = 18.0, gap = 14.0, background = surfaces::raised(), border_radius = 16.0, ) { text(role = TextRole::Heading(1), size = 28.0, weight = FontWeight::Semibold) { "Network" } text(color = colors::muted(), wrap = TextWrap::Word) { "This panel talks to a tiny local HTTP server over `ruin_runtime::net::TcpStream`. \ It keeps the example truthful without pretending TLS, a JSON client, or text \ input already exist." } row(gap = 8.0) { button( on_press = { let endpoint = endpoint.clone(); move |_| { let _ = endpoint.set(DemoEndpoint::Release); } }, ) { "Release" } button( on_press = { let endpoint = endpoint.clone(); move |_| { let _ = endpoint.set(DemoEndpoint::Health); } }, ) { "Health" } button( on_press = { let endpoint = endpoint.clone(); move |_| { let _ = endpoint.set(DemoEndpoint::Notes); } }, ) { "Notes" } button( on_press = { let endpoint = endpoint.clone(); move |_| { let _ = endpoint.set(DemoEndpoint::Missing); } }, ) { "404" } button( on_press = { let endpoint_reload = endpoint_reload.clone(); move |_| { endpoint_reload.update(|value| *value += 1); } }, ) { "Reload" } } text(size = 16.0, weight = FontWeight::Semibold) { "Endpoint: "; endpoint.get().path() } match response_state { Pending => view! { block( padding = 14.0, background = surfaces::canvas(), border_radius = 12.0, ) { text(color = colors::muted()) { "Fetching a local HTTP response through the runtime..." } } }, Ready(Ok(response)) => view! { column(gap = 12.0) { text(size = 18.0, weight = FontWeight::Semibold) { response.status_line.clone() } text(color = colors::muted(), wrap = TextWrap::Word) { "Received "; response.body.len(); " body bytes from "; response.path.clone(); "." } if let Some(content_type) = response.content_type.clone() { view! { text(color = colors::muted()) { "content-type: "; content_type } } } else { View::from_element(Element::column()) } scroll_box( height = 500.0, offset_y = response_scroll.clone(), padding = 12.0, background = surfaces::canvas(), border_radius = 12.0, border = (2.0, colors::muted()), ) { text( color = colors::muted(), font_family = TextFontFamily::Monospace, wrap = TextWrap::Word, ) { response.body } } } }, Ready(Err(error)) => view! { block( padding = 14.0, gap = 8.0, background = surfaces::canvas(), border_radius = 12.0, border = (2.0, colors::danger()), ) { text(size = 18.0, weight = FontWeight::Semibold, color = colors::danger()) { "Request failed" } text(color = colors::muted(), wrap = TextWrap::Word) { error } } }, } } } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum DemoEndpoint { Release, Health, Notes, Missing, } impl DemoEndpoint { fn path(self) -> &'static str { match self { Self::Release => "/release", Self::Health => "/health", Self::Notes => "/notes", Self::Missing => "/missing", } } } #[derive(Clone, Debug)] struct DemoResponse { path: String, status_line: String, content_type: Option, body: String, } fn spawn_demo_server() -> std::io::Result { let listener = StdTcpListener::bind(("127.0.0.1", 0))?; let address = listener.local_addr()?; thread::Builder::new() .name("ruin-example05-demo-server".into()) .spawn(move || { for stream in listener.incoming() { let Ok(mut stream) = stream else { break; }; let mut request = [0_u8; 4096]; let read = match stream.read(&mut request) { Ok(read) => read, Err(_) => continue, }; let request = String::from_utf8_lossy(&request[..read]); let path = request .lines() .next() .and_then(|line| line.split_whitespace().nth(1)) .unwrap_or("/"); let (status, body) = demo_response_for_path(path); let response = format!( "HTTP/1.1 {status}\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", body.len(), body ); let _ = stream.write_all(response.as_bytes()); } }) .map_err(std::io::Error::other)?; Ok(address) } fn demo_response_for_path(path: &str) -> (&'static str, String) { match path { "/release" => ( "200 OK", "release = v0.1.0\npublished_at = 2026-03-22T00:00:00Z\nnotes = honest runtime IO demo backed by ruin_runtime".to_string(), ), "/health" => ( "200 OK", "status = ok\nreactor = draining microtasks\nio = idle".to_string(), ), "/notes" => ( "200 OK", "The network pane is intentionally local and plaintext.\nIt still exercises ruin_runtime::net::TcpStream end-to-end,\nbut it does not pretend that TLS, a JSON client, or text inputs are finished yet.".to_string(), ), _ => ( "404 Not Found", format!("No demo route is registered for {path}. Try /release, /health, or /notes."), ), } } fn timestamp_label() -> String { match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(duration) => { let seconds = duration.as_secs(); let millis = duration.subsec_millis(); format!("{seconds}.{millis:03}s since unix epoch") } Err(_) => "time went backwards".to_string(), } } async fn fetch_demo_endpoint( address: SocketAddr, endpoint: DemoEndpoint, ) -> std::result::Result { let path = endpoint.path().to_string(); let mut stream = ruin_runtime::net::TcpStream::connect(address) .await .map_err(|error| error.to_string())?; let request = format!("GET {path} HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n\r\n"); stream .write_all(request.as_bytes()) .await .map_err(|error| error.to_string())?; let mut bytes = Vec::new(); let mut chunk = [0_u8; 4096]; loop { let read = stream .read(&mut chunk) .await .map_err(|error| error.to_string())?; if read == 0 { break; } bytes.extend_from_slice(&chunk[..read]); } parse_http_response(&path, &bytes) } fn parse_http_response(path: &str, bytes: &[u8]) -> std::result::Result { let text = String::from_utf8(bytes.to_vec()).map_err(|error| error.to_string())?; let Some((head, body)) = text.split_once("\r\n\r\n") else { return Err("demo server returned a malformed HTTP response".to_string()); }; let mut lines = head.lines(); let status_line = lines .next() .ok_or_else(|| "demo server returned an empty status line".to_string())? .to_string(); let content_type = lines.find_map(|line| { line.split_once(':').and_then(|(name, value)| { name.eq_ignore_ascii_case("content-type") .then(|| value.trim().to_string()) }) }); if !status_line.contains("200") { return Err(format!("{status_line}\n\n{body}")); } Ok(DemoResponse { path: path.to_string(), status_line, content_type, body: body.to_string(), }) } #[cfg(test)] mod tests { use super::{DemoEndpoint, parse_http_response}; #[test] fn parse_http_response_extracts_status_body_and_content_type() { let parsed = parse_http_response( DemoEndpoint::Release.path(), b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\nconnection: close\r\n\r\nhello", ) .expect("response should parse"); assert_eq!(parsed.status_line, "HTTP/1.1 200 OK"); assert_eq!(parsed.content_type.as_deref(), Some("text/plain")); assert_eq!(parsed.body, "hello"); } #[test] fn parse_http_response_surfaces_non_success_statuses_as_errors() { let error = parse_http_response( DemoEndpoint::Missing.path(), b"HTTP/1.1 404 Not Found\r\ncontent-type: text/plain\r\n\r\nmissing", ) .expect_err("non-200 responses should surface as errors"); assert!(error.contains("404 Not Found")); assert!(error.contains("missing")); } }