From faa7637da4067bd33de9d74484b8a43a3f468ee0 Mon Sep 17 00:00:00 2001 From: chrysn Date: Mon, 20 Feb 2023 14:07:04 +0100 Subject: [PATCH] sys: Add stdio_coap --- .cargo/config.toml | 4 +- makefiles/cargo-targets.inc.mk | 3 + makefiles/stdio.inc.mk | 18 ++ sys/rust_riotmodules/Cargo.toml | 1 + sys/rust_riotmodules/src/lib.rs | 3 + sys/rust_riotmodules_standalone/Cargo.lock | 4 +- sys/stdio_coap/Cargo.toml | 24 +++ sys/stdio_coap/src/lib.rs | 197 +++++++++++++++++++++ sys/stdio_coap/src/stdin.rs | 72 ++++++++ 9 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 sys/stdio_coap/Cargo.toml create mode 100644 sys/stdio_coap/src/lib.rs create mode 100644 sys/stdio_coap/src/stdin.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 03fd9739485c5..38a5973926b5b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,5 +5,5 @@ # Cargo.toml, copy this file over, or just use the released versions. [patch.crates-io] -riot-sys = { git = "https://github.com/RIOT-OS/rust-riot-sys" } -riot-wrappers = { git = "https://github.com/RIOT-OS/rust-riot-wrappers" } +riot-sys = { git = "https://github.com/RIOT-OS/rust-riot-sys", branch = "marker-for-config-auto-init-enable-debug" } +riot-wrappers = { git = "https://github.com/RIOT-OS/rust-riot-wrappers", branch = "auto-init" } diff --git a/makefiles/cargo-targets.inc.mk b/makefiles/cargo-targets.inc.mk index 118cb83f7aafc..7df8fb76688f4 100644 --- a/makefiles/cargo-targets.inc.mk +++ b/makefiles/cargo-targets.inc.mk @@ -16,6 +16,9 @@ endif ifneq (,$(filter shell_democommands,$(USEMODULE))) CARGO_OPTIONS += --features rust_riotmodules/riot-module-shell-democommands endif +ifneq (,$(filter stdio_coap,$(USEMODULE))) + CARGO_OPTIONS += --features rust_riotmodules/riot-module-stdio-coap +endif # This is duplicating the compile-commands rule because unlike in the use case # when a $(RIOTBASE)/compile_commands.json is built, we *want* this to be diff --git a/makefiles/stdio.inc.mk b/makefiles/stdio.inc.mk index 9749c3417788c..bf7b4316103ba 100644 --- a/makefiles/stdio.inc.mk +++ b/makefiles/stdio.inc.mk @@ -1,6 +1,7 @@ STDIO_MODULES = \ slipdev_stdio \ stdio_cdc_acm \ + stdio_coap \ stdio_ethos \ stdio_native \ stdio_nimble \ @@ -25,6 +26,23 @@ ifneq (,$(filter stdio_cdc_acm,$(USEMODULE))) USEMODULE += stdio_available endif +ifneq (,$(filter stdio_coap,$(USEMODULE))) + # FIXME: Does this obsolete the stdio_coap Makefile.{dep,include}? + USEMODULE += rust_riotmodules + PSEUDOMODULES += stdio_coap + + # FIXME: Whose responsibility is it to include these? + + # either + USEMODULE += netdev_default + #USEMODULE += usbus_cdc_ecm + + USEMODULE += gnrc_sock_udp + USEMODULE += gcoap + USEMODULE += gnrc_ipv6_default + USEMODULE += auto_init_gnrc_netif +endif + ifneq (,$(filter stdio_tinyusb_cdc_acm,$(USEMODULE))) USEPKG += tinyusb endif diff --git a/sys/rust_riotmodules/Cargo.toml b/sys/rust_riotmodules/Cargo.toml index 45fe52acbee14..75007b44539cc 100644 --- a/sys/rust_riotmodules/Cargo.toml +++ b/sys/rust_riotmodules/Cargo.toml @@ -12,3 +12,4 @@ publish = false riot-module-lsm303agr = { path = "../../drivers/lsm303agr", optional = true } riot-module-shell-democommands = { path = "../../sys/shell/democommands", optional = true } +riot-module-stdio-coap = { path = "../../sys/stdio_coap", optional = true } diff --git a/sys/rust_riotmodules/src/lib.rs b/sys/rust_riotmodules/src/lib.rs index 819c4a5391e2a..4d88b7afa7989 100644 --- a/sys/rust_riotmodules/src/lib.rs +++ b/sys/rust_riotmodules/src/lib.rs @@ -12,3 +12,6 @@ pub use riot_module_lsm303agr as lsm303agr; #[cfg(feature = "riot-module-shell-democommands")] pub use riot_module_shell_democommands as democommands; + +#[cfg(feature = "riot-module-stdio-coap")] +pub use riot_module_stdio_coap as stdio_coap; diff --git a/sys/rust_riotmodules_standalone/Cargo.lock b/sys/rust_riotmodules_standalone/Cargo.lock index 30be49f05edf9..ee9e3f7102452 100644 --- a/sys/rust_riotmodules_standalone/Cargo.lock +++ b/sys/rust_riotmodules_standalone/Cargo.lock @@ -446,7 +446,7 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "riot-sys" version = "0.7.9" -source = "git+https://github.com/RIOT-OS/rust-riot-sys#81d791789e07a2e1019385ef2d1665348dea1df4" +source = "git+https://github.com/RIOT-OS/rust-riot-sys?branch=marker-for-config-auto-init-enable-debug#9bd040471e947bbbc562b4a2c1d4ee08bee034d3" dependencies = [ "bindgen", "c2rust-asm-casts", @@ -461,7 +461,7 @@ dependencies = [ [[package]] name = "riot-wrappers" version = "0.8.1" -source = "git+https://github.com/RIOT-OS/rust-riot-wrappers#9c29faf55d4c14d2d7f55f8df5059c52af4e5317" +source = "git+https://github.com/RIOT-OS/rust-riot-wrappers?branch=auto-init#d6bb92533adc640c14f92fac0f13e777ba2fb0b4" dependencies = [ "bare-metal", "cstr", diff --git a/sys/stdio_coap/Cargo.toml b/sys/stdio_coap/Cargo.toml new file mode 100644 index 0000000000000..6379cd2967768 --- /dev/null +++ b/sys/stdio_coap/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "riot-module-stdio-coap" +version = "0.1.0" +edition = "2021" + +authors = ["Christian Amsüss "] +license = "LGPL-2.1-only" + +# Shipped with RIOT-OS; this has no external API that would make +# sense to consume in any context than from within RIOT +publish = false + +[dependencies] +coap-scroll-ring-server = "0.1" +riot-wrappers = { version = "^0.8", features = [ "with_coap_handler" ] } # esp. the git version: fixes mutex bug +riot-sys = "0.7" +scroll-ring = "0.1.1" +static_cell = "1" +# for stdin +coap-handler = "0.1" +coap-message = "0.2" +coap-numbers = "0.2" +ringbuf = { version = "0.3.2", default-features = false } +coap-handler-implementations = { git = "https://gitlab.com/chrysn/coap-handler-implementations", features = [ "leaky_names" ] } diff --git a/sys/stdio_coap/src/lib.rs b/sys/stdio_coap/src/lib.rs new file mode 100644 index 0000000000000..5514295771b89 --- /dev/null +++ b/sys/stdio_coap/src/lib.rs @@ -0,0 +1,197 @@ +//! An implementation of stdio based on CoAP +//! +//! The stdout side is trivial in that it builds on stdout being a [scroll_ring::Buffer] and using +//! the implementation provided in [coap_scroll_ring_server] to expose it through CoAP. +//! +//! The stdin side is a manual implementation of a non-overwriting MPMC queue (which rejects +//! excessive input with a 503 error) +#![no_std] + +use coap_handler_implementations::{HandlerBuilder, ReportingHandlerBuilder}; +use ringbuf::{ring_buffer::RbBase, Rb}; +use riot_wrappers::mutex::Mutex; + +mod stdin; +use stdin::{Stdin, StdinHandler}; + +const STDOUT_BUFSIZE: usize = 1024; +const STDIN_BUFSIZE: usize = 128; + +static mut STDOUT: Option<&'static scroll_ring::Buffer> = None; + +// FIXME: The whole construction of Stdin, STDIN and STDIN_WAITER could very well be described by a +// blocking SPMC thread-safe ring buffer (which we don't have in RIOT right now) + +/// This is never held for long, and only awaited blockingly while also holding STDIN_WAITER. +/// +/// It is only None during initialization. +static STDIN: Mutex>> = Mutex::new(None); +/// A lock of this is stored in STDIN while any reader is waiting. +static STDIN_WAITER: Mutex<()> = Mutex::new(()); + +/// The CoAP handler for the stdout resource +/// +/// If you do not use the auto-initialized CoAP tree, this handler needs to be placed discoverably +/// somewhere in the CoAP resource tree to make stdout accessible. +pub fn stdout_handler() -> coap_scroll_ring_server::BufferHandler<'static, STDOUT_BUFSIZE> { + coap_scroll_ring_server::BufferHandler::new(unsafe { STDOUT }.expect("stdio was configured")) +} +/// +/// The CoAP handler for the stdin resource +/// +/// If you do not use the auto-initialized CoAP tree, this handler needs to be placed discoverably +/// somewhere in the CoAP resource tree to make stdin accessible. +pub fn stdin_handler() -> coap_handler_implementations::wkc::ConstantSingleRecordReport< + 'static, + StdinHandler, +> { + coap_handler_implementations::wkc::ConstantSingleRecordReport::new( + StdinHandler(&STDIN), + &[coap_handler::Attribute::Interface( + "tag:riot-os.org,2021:ser-in", + )], + ) +} + +riot_wrappers::auto_init!(inittest, 65535); +fn inittest() { + riot_wrappers::println!("Coming up"); +} + +// FIXME: Declaring this creates a hard dependency on the auto_init module -- but we don't have an +// easy way to spot whether it's active right now. This practically makes the stdio_coap module +// depend on auto_init. + +riot_wrappers::auto_init!(auto_init_stdio_coap_late, 42); + +fn auto_init_stdio_coap_late() { + let handler = coap_handler_implementations::new_dispatcher() + .at(&["so"], stdout_handler()) + .at(&["si"], stdin_handler()) + .with_wkc(); + + // If we had feature(type_alias_impl_trait), we could use this, and drop the leaky_names + // feature: + // type HT = impl riot_wrappers::gcoap::Handler + 'static; + // (By the way, I didn't come up with that type name ... I boxed things and had type_name + // printed :-) ) + type HT = riot_wrappers::coap_handler::GcoapHandler< + coap_handler_implementations::WellKnownCore< + coap_handler_implementations::ForkingHandler< + 'static, + coap_handler_implementations::wkc::ConstantSingleRecordReport< + 'static, + StdinHandler, + >, + coap_handler_implementations::ForkingHandler< + 'static, + coap_scroll_ring_server::BufferHandler<'static, STDOUT_BUFSIZE>, + coap_handler_implementations::wkc::NotReporting< + coap_handler_implementations::NeverFound, + >, + >, + >, + >, + >; + static HANDLER: static_cell::StaticCell = static_cell::StaticCell::new(); + static LISTENER: static_cell::StaticCell> = + static_cell::StaticCell::new(); + + let handler = HANDLER.init(riot_wrappers::coap_handler::GcoapHandler(handler)); + let listener = LISTENER.init(riot_wrappers::gcoap::SingleHandlerListener::new_catch_all( + handler, + )); + + riot_wrappers::gcoap::register(listener); +} + +#[no_mangle] +pub extern "C" fn stdio_init() { + static SO: static_cell::StaticCell> = + static_cell::StaticCell::new(); + // Calling this twice would panic + let so = SO.init_with(Default::default); + + // unsafe: There's only one place in the code that writes there, and this is guarded in the + // previous line by a run-once condition. + // + // (And it may be safely read all the time, because there is no parallelism yet when this is + // initialized) + unsafe { STDOUT = Some(so) }; + + static SI: static_cell::StaticCell> = static_cell::StaticCell::new(); + let si = SI.init_with(|| Stdin { + lock_for_waiter: None, + buffer: Default::default(), + }); + *STDIN.try_lock().unwrap() = Some(si); +} + +#[no_mangle] +extern "C" fn stdio_write( + buffer: *const riot_sys::libc::c_void, + len: riot_sys::size_t, +) -> riot_sys::size_t { + // unsafe: See its one writer. + let stdout = unsafe { STDOUT }; + + // unsafe: data is initialized per API + let data = unsafe { core::slice::from_raw_parts(buffer as *const u8, len as _) }; + + if let Some(stdout) = stdout { + stdout.write(data); + data.len() as _ + } else { + // stdout not initialized + 0 + } +} + +#[no_mangle] +extern "C" fn stdio_read( + buffer: *mut riot_sys::libc::c_void, + max_len: riot_sys::size_t, +) -> riot_sys::ssize_t { + // API adjustments for type readability + let mut buffer = buffer as *mut u8; + let mut max_len = max_len as usize; + + if max_len == 0 { + // The later loop requires that it can count down max_len after each popped item (which + // requires popping first and checking after decrement). + return 0; + } + + loop { + let waiter_lock = STDIN_WAITER.lock(); + + // There's no guarantee that every reader from stdio is in a single thread, although that + // appears to be a common assumption. + let mut stdin = STDIN.lock(); + let mut stdin = stdin.as_mut().expect("STDIN was initialized"); + + if stdin.buffer.is_empty() { + // Move away waiter_lock on STDIN_WAITER, so when we're next entering the loop we're + // blocked until there has been progress on stdin + stdin.lock_for_waiter = Some(waiter_lock); + drop(stdin); + continue; + } + + let mut assigned = 0; + // This might be doable more efficiently, but then again, stdio_read is usually called with + // a 1-long buffer anyway. + for b in stdin.buffer.pop_iter() { + // unsafe: Per C API + unsafe { *buffer = b }; + // unsafe: Per C API + buffer = unsafe { buffer.offset(1) }; + max_len -= 1; + assigned += 1; + if max_len == 0 { + break; + } + } + return assigned; + } +} diff --git a/sys/stdio_coap/src/stdin.rs b/sys/stdio_coap/src/stdin.rs new file mode 100644 index 0000000000000..df3e8f25a6ea3 --- /dev/null +++ b/sys/stdio_coap/src/stdin.rs @@ -0,0 +1,72 @@ +use riot_wrappers::mutex::{Mutex, MutexGuard}; +use ringbuf::Rb; + +pub(crate) struct Stdin { + pub(crate) lock_for_waiter: Option>, + pub(crate) buffer: ringbuf::LocalRb; N]>, +} + +pub struct StdinHandler(pub(crate) &'static Mutex>>); + +impl coap_handler::Handler for StdinHandler { + type RequestData = u8; + fn extract_request_data(&mut self, req: &impl coap_message::ReadableMessage) -> u8 { + use coap_handler_implementations::option_processing::OptionsExt; + + let opts = req.options().ignore_elective_others(); + + if opts.is_err() { + // FIXME: We could also use its Render implementation if that were public + return coap_numbers::code::BAD_OPTION; + } + + if req.code().into() != coap_numbers::code::POST { + return coap_numbers::code::METHOD_NOT_ALLOWED; + } + + let Some(mut stdin) = self.0.try_lock() else { + // This only happens when someone is just in the process of locking stdin -- we'll need + // to return in ordrr to let them place their mutex there. FIXME: Max-Age:0 + return coap_numbers::code::SERVICE_UNAVAILABLE; + }; + let Some(stdin) = stdin.as_mut() else { + // Stdin not initialized + // + // Returning as before here not only because it's a sensible error, but also because it + // should allow the compiler to collapse the branches. + return coap_numbers::code::SERVICE_UNAVAILABLE; + }; + + let maybe_the_waiter = stdin.lock_for_waiter.take(); + + if stdin.buffer.free_len() < req.payload().len() { + if req.payload().len() > N { + // Buffer full. FIXME: Max-Age? Too-Many-Requests? Request-Entity-Too-Large? + return coap_numbers::code::SERVICE_UNAVAILABLE; + } else { + // Not a chance + return coap_numbers::code::REQUEST_ENTITY_TOO_LARGE; + } + } + stdin.buffer.push_slice(req.payload()); + + drop(maybe_the_waiter); + + coap_numbers::code::CHANGED + } + fn estimate_length(&mut self, _req: &u8) -> usize { + 1 + } + fn build_response(&mut self, res: &mut impl coap_message::MutableWritableMessage, req: u8) { + res.set_code(req.try_into().ok().unwrap()); + if req == coap_numbers::code::REQUEST_ENTITY_TOO_LARGE { + if let (Ok(opt), Ok(size)) = ( + coap_numbers::option::SIZE1.try_into(), + u16::try_from(N), + ) { + res.add_option_uint(opt, size); + } + } + res.set_payload(b""); + } +}