-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
322 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
[package] | ||
name = "riot-module-stdio-coap" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
authors = ["Christian Amsüss <chrysn@fsfe.org>"] | ||
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" ] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<STDOUT_BUFSIZE>> = 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<Option<&'static mut Stdin<STDIN_BUFSIZE>>> = 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<STDIN_BUFSIZE>, | ||
> { | ||
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<STDIN_BUFSIZE>, | ||
>, | ||
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<HT> = static_cell::StaticCell::new(); | ||
static LISTENER: static_cell::StaticCell<riot_wrappers::gcoap::SingleHandlerListener<HT>> = | ||
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<scroll_ring::Buffer<STDOUT_BUFSIZE>> = | ||
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<Stdin<STDIN_BUFSIZE>> = 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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
use riot_wrappers::mutex::{Mutex, MutexGuard}; | ||
use ringbuf::Rb; | ||
|
||
pub(crate) struct Stdin<const N: usize> { | ||
pub(crate) lock_for_waiter: Option<MutexGuard<'static, ()>>, | ||
pub(crate) buffer: ringbuf::LocalRb<u8, [core::mem::MaybeUninit<u8>; N]>, | ||
} | ||
|
||
pub struct StdinHandler<const N: usize>(pub(crate) &'static Mutex<Option<&'static mut Stdin<N>>>); | ||
|
||
impl<const N: usize> coap_handler::Handler for StdinHandler<N> { | ||
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""); | ||
} | ||
} |