moving offline for travel

This commit is contained in:
lbfalvy committed 2026-07-28 23:35:33 +00:00
1 parent 66c9d78ced
commit a7bfe04e52
52 files changed
+2283 -713

No files matched your search

Generated
+1361 -120
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -19,6 +19,7 @@ members = [
"checklings",
"checkers-web",
"checkers-api",
"scoped-log",
]
[workspace.package]
+4
View File
@@ -0,0 +1,4 @@
[application]
asset_dir = 'checkers-web/assets'
sub_package = 'checkers-web'
+3
View File
@@ -6,7 +6,10 @@ authors.workspace = true
repository.workspace = true
[dependencies]
actix-cors = "0.7.1"
actix-web = "4.14.0"
actix-ws = "0.4.0"
async-event = "0.2.1"
env_logger = "0.11.11"
futures = "0.3.32"
log = "0.4.33"
+158 -44
View File
@@ -1,7 +1,11 @@
use std::collections::HashMap;
use std::num::NonZero;
use std::pin::pin;
use std::range::Range;
use std::sync::Arc;
use actix_cors::Cors;
use actix_web::middleware::Logger;
use actix_web::{App, Error, HttpRequest, HttpResponse, HttpServer, Responder, get, post, rt, web};
use actix_ws::{AggregatedMessage, ProtocolError};
use async_event::Event;
@@ -9,32 +13,36 @@ use futures::lock::Mutex;
use futures::stream::StreamExt as _;
use futures::stream_select;
#[derive(Clone)]
struct Message {
data: web::Bytes,
}
#[derive(Default)]
struct MessageStream {
backlog: Mutex<Vec<Message>>,
backlog: std::sync::Mutex<Vec<Message>>,
pushed: Event,
}
#[derive(Default)]
async fn publish_on_topic(topic: &MessageStream, message: Message) {
topic.backlog.lock().unwrap().push(message);
topic.pushed.notify_all();
}
#[derive(Default, Clone)]
struct AppState {
streams: Mutex<HashMap<String, Arc<MessageStream>>>,
streams: Arc<Mutex<HashMap<String, Arc<MessageStream>>>>,
}
#[get("/read/{topic}")]
async fn read(
topic: String,
req: HttpRequest,
stream: web::Payload,
state: web::Data<AppState>,
) -> Result<HttpResponse, Error> {
let topic = req.match_info().get("topic").unwrap().to_string();
eprintln!("Listening on {topic:?}");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
let stream = stream
.aggregate_continuations()
// aggregate continuation frames up to 1MiB
@@ -42,51 +50,73 @@ async fn read(
// start task but don't wait for it
rt::spawn(async move {
let backlog_g = topic.backlog.lock().await;
let mut cursor = backlog_g.len();
for message in backlog_g.iter() {
session.binary(message.data.clone()).await.unwrap();
}
std::mem::drop(backlog_g);
enum Change {
enum SubscrEvent {
Message(Result<AggregatedMessage, ProtocolError>),
Ingress,
Ingress(Range<usize>),
Reset,
}
let stream = pin!(stream.map(Change::Message));
let stream = pin!(stream.map(SubscrEvent::Message));
let data_changes = pin!(futures::stream::unfold(topic.clone(), async |topic| {
topic.pushed.wait_until(|| Some(())).await;
Some((Change::Ingress, topic))
}));
let data_changes =
pin!(futures::stream::unfold((topic.clone(), 0usize), async |(topic, cursor)| {
eprintln!("Listening for new data");
let len_chg = (topic.pushed)
.wait_until(|| {
eprintln!("Checking for new data");
NonZero::new(topic.backlog.lock().unwrap().len() as i64 - cursor as i64)
})
.await;
if let Ok(added) = usize::try_from(len_chg.get()) {
eprintln!("Found new data");
let next_cursor = cursor + added;
Some((
SubscrEvent::Ingress(Range { start: cursor, end: next_cursor }),
(topic, next_cursor),
))
} else {
eprintln!("Topic reset");
Some((SubscrEvent::Reset, (topic, 0)))
}
}));
let mut events = stream_select!(stream, data_changes);
while let Some(ev) = events.next().await {
eprintln!("activity!");
match ev {
Change::Message(Err(e)) => {
SubscrEvent::Message(Err(e)) => {
eprintln!("{e}");
session.close(None).await.unwrap();
break;
},
Change::Message(Ok(AggregatedMessage::Ping(msg))) => session.pong(&msg).await.unwrap(),
Change::Message(Ok(_)) => {
SubscrEvent::Message(Ok(AggregatedMessage::Ping(msg))) => session.pong(&msg).await.unwrap(),
SubscrEvent::Message(Ok(AggregatedMessage::Pong(_))) => (),
SubscrEvent::Message(Ok(AggregatedMessage::Text(msg))) if msg.trim() == "exit" => {
session
.close(Some(actix_ws::CloseReason {
code: actix_ws::CloseCode::Normal,
description: Some("User requested shutdown by sending message".to_string()),
description: Some("User requested shutdown by sending \"exit\"".to_string()),
}))
.await
.unwrap();
break;
},
Change::Ingress => {
let backlog_g = topic.backlog.lock().await;
for msg in &backlog_g[cursor..] {
session.binary(msg.data.clone()).await.unwrap();
SubscrEvent::Message(Ok(AggregatedMessage::Close(_))) => (),
SubscrEvent::Message(Ok(AggregatedMessage::Text(tx))) => {
publish_on_topic(&topic, Message { data: tx.into_bytes() }).await;
},
SubscrEvent::Message(Ok(AggregatedMessage::Binary(data))) => {
publish_on_topic(&topic, Message { data }).await;
},
SubscrEvent::Reset => {
session.text("reset").await.unwrap();
},
SubscrEvent::Ingress(range) => {
let payload = topic.backlog.lock().unwrap()[range].to_vec();
for msg in payload {
session.binary(msg.data).await.unwrap();
}
cursor = backlog_g.len();
},
}
}
@@ -96,29 +126,113 @@ async fn read(
Ok(res)
}
#[post("/push/{topic}")]
async fn push(topic: String, mut body: web::Payload, state: web::Data<AppState>) -> impl Responder {
async fn write(
req: HttpRequest,
stream: web::Payload,
state: web::Data<AppState>,
) -> Result<HttpResponse, Error> {
let topic = req.match_info().get("topic").unwrap().to_string();
eprintln!("Listening on {topic:?}");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
let mut stream = stream
.aggregate_continuations()
// aggregate continuation frames up to 1MiB
.max_continuation_size(2_usize.pow(20));
// start task but don't wait for it
rt::spawn(async move {
while let Some(msg) = stream.next().await {
match msg {
Err(e) => eprintln!("Error on stream: {e}"),
Ok(AggregatedMessage::Ping(data)) => session.pong(&data).await.unwrap(),
Ok(AggregatedMessage::Pong(_)) => (),
Ok(AggregatedMessage::Close(_)) => (),
Ok(AggregatedMessage::Text(msg)) if msg.trim() == "exit" => {
session
.close(Some(actix_ws::CloseReason {
code: actix_ws::CloseCode::Normal,
description: Some("User requested shutdown by sending \"exit\"".to_string()),
}))
.await
.unwrap();
break;
},
Ok(AggregatedMessage::Binary(data)) => publish_on_topic(&topic, Message { data }).await,
Ok(AggregatedMessage::Text(msg)) =>
publish_on_topic(&topic, Message { data: msg.into_bytes() }).await,
}
}
});
// respond immediately with response connected to WS session
Ok(res)
}
#[get("/hello")]
async fn hello() -> impl Responder {
println!("said hello");
"Hello world!"
}
#[post("/topic/{topic}/push")]
async fn push(
path: web::Path<(String,)>,
mut body: web::Payload,
state: web::Data<AppState>,
) -> impl Responder {
eprintln!("publishing on {:?}", path.0);
let mut data = Vec::new();
while let Some(chunk) = body.next().await {
data.extend_from_slice(&chunk?);
}
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
topic.backlog.lock().await.push(Message { data: data.into() });
topic.pushed.notify_all();
let topic = state.streams.lock().await.entry(path.0.clone()).or_insert_with(Arc::default).clone();
publish_on_topic(&topic, Message { data: data.into() }).await;
Ok::<_, actix_web::error::PayloadError>(())
}
#[post("/topic/{topic}/clear")]
async fn clear(path: web::Path<(String,)>, state: web::Data<AppState>) -> impl Responder {
eprintln!("Clearing {:?}", path.0);
let state_lock = state.streams.lock().await;
let Some(topic) = state_lock.get(&path.0) else { return "already empty" };
*topic.backlog.lock().unwrap() = Vec::new();
topic.pushed.notify_all();
""
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let custom_path_prefix = std::env::var("CHECKERS_API_PATH");
let path_prefix = custom_path_prefix.map(|s| s.to_owned()).unwrap_or("".to_string());
HttpServer::new(move || App::new().service(web::scope(&path_prefix).service(push).service(read)))
.bind((
std::env::var("CHECKERS_API_HOST").as_deref().unwrap_or("127.0.0.1"),
std::env::var("CHECKERS_API_PORT")
.map(|s| s.parse::<u16>().expect("Could not parse custom port"))
.unwrap_or(8170),
))?
.run()
.await
let state = AppState::default();
HttpServer::new(move || {
App::new()
.wrap(Logger::default().log_level(log::Level::Warn))
.wrap(
Cors::default()
.allow_any_method()
.allow_any_header()
.allow_any_origin()
.expose_any_header(),
)
.service(
web::scope(&path_prefix)
.app_data(web::Data::new(state.clone()))
.service(push)
.service(clear)
.service(hello)
.route("/topic/{topic}/read", web::get().to(read))
.route("/topic/{topic}/write", web::get().to(write)),
)
})
.bind((
std::env::var("CHECKERS_API_HOST").as_deref().unwrap_or("127.0.0.1"),
std::env::var("CHECKERS_API_PORT")
.map(|s| s.parse::<u16>().expect("Could not parse custom port"))
.unwrap_or(8170),
))?
.run()
.await
}
+10 -1
View File
@@ -4,8 +4,17 @@ version = "0.1.0"
edition = "2021"
[dependencies]
checklings = { version = "0.1.0", path = "../checklings" }
dioxus = { version = "0.7.9", features = [] }
facet-xdr = "0.46.1"
futures = "0.3.32"
gloo-net = "0.7.0"
itertools = "0.15.0"
js-sys = "0.3.103"
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
reqwest = { version = "0.13.4", features = ["json"] }
wasm-bindgen-futures = "0.4.76"
web-sys = { version = "0.3.103", features = ["Storage", "Window"] }
[features]
default = ["web"]
web = ["dioxus/web"]
+96 -2
View File
@@ -1,16 +1,110 @@
use std::time::Duration;
use checklings::{TraceInfo, TracedEvent};
use dioxus::prelude::*;
use futures::stream::StreamExt;
use gloo_net::websocket::Message;
use gloo_net::websocket::futures::WebSocket;
use orchid_api_traits::Decode;
const MAIN_CSS: Asset = asset!("/assets/main.css");
fn main() { dioxus::launch(App); }
pub async fn sleep(duration: Duration) {
let p = js_sys::Promise::new(&mut |resolve: js_sys::Function, _reject: js_sys::Function| {
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
&resolve,
duration.as_millis().try_into().unwrap(),
)
.unwrap();
});
p.await.unwrap();
}
fn get_local_storage(key: &str) -> Option<String> {
web_sys::window().unwrap().local_storage().unwrap().unwrap().get_item(key).unwrap()
}
fn set_local_storage(key: &str, value: &str) {
web_sys::window().unwrap().local_storage().unwrap().unwrap().set_item(key, value).unwrap()
}
#[component]
fn App() -> Element {
// Build cool things ✌️
let mut topic = use_signal(|| get_local_storage("topic").unwrap_or_else(|| "main".to_string()));
use_effect(move || set_local_storage("topic", &topic()));
let mut log = use_signal(Vec::<Result<TracedEvent, String>>::new);
use_resource(move || async move {
'connect: loop {
let mut socket =
WebSocket::open(&format!("wss://8170.code.lbfalvy.com/topic/{}/read", topic())).unwrap();
log.set(Vec::new());
while let Some(msg) = socket.next().await {
match msg {
Err(e) => {
log.write().push(Err(format!("connection error: {e}")));
sleep(Duration::from_secs(3)).await;
continue 'connect;
},
Ok(Message::Text(txt)) if txt == "reset" => log.set(Vec::new()),
Ok(Message::Text(txt)) => log.write().push(Err(format!("text: {txt}"))),
Ok(Message::Bytes(bytes)) =>
log.write().push(Ok(TracedEvent::decode_slice(&mut &bytes[..]))),
}
}
}
});
rsx! {
// Global app resources
document::Link { rel: "stylesheet", href: MAIN_CSS }
p { "Hello, World!" }
input {
placeholder: "topic",
value: topic(),
oninput: move |event| {
topic.set(event.value());
}
}
div {
{log.iter().map(|log_line| match &*log_line {
Err(msg) => rsx!{ TextLine { msg: msg } },
Ok(event) => rsx!{ EventLine { event: event.clone() }}
})}
}
}
}
#[component]
fn TextLine(msg: String) -> Element {
rsx! {
div {
pre { "{msg}" }
}
}
}
fn mk_path(info: &TraceInfo) -> String {
match &info.parent {
None => info.label.clone(),
Some(parent) => format!("{}/{}", mk_path(parent), info.label),
}
}
#[component]
fn EventLine(event: TracedEvent) -> Element {
let TracedEvent { info, kind } = event;
let path = info.as_deref().map(mk_path);
rsx! {
div {
{ if let Some(path) = path {
rsx!{ span { class: "path", "{path}" } }
} else {
rsx!{}
} }
"{kind:?}"
}
}
}
+15
View File
@@ -4,4 +4,19 @@ version = "0.1.0"
edition = "2024"
[dependencies]
facet = "0.46.5"
facet-xdr = { version = "0.46.1", optional = true }
orchid-api-derive = { version = "0.1.0", path = "../orchid-api-derive" }
orchid-api-traits = { version = "0.1.0", path = "../orchid-api-traits" }
scoped-value = { path = "../scoped-value", version = "1.0.0" }
ureq = { version = "3.3.0", optional = true }
websocket = { version = "0.27.1", optional = true, default-features = false, features = [
"bytes",
"sync",
"native-tls",
"sync-ssl",
] }
[features]
default = []
telemetry = ["dep:facet-xdr", "dep:ureq", "dep:websocket"]
+115 -1
View File
@@ -1,11 +1,30 @@
#[cfg(feature = "telemetry")]
use std::borrow::Cow;
use std::cell::RefCell;
use std::pin::Pin;
#[cfg(feature = "telemetry")]
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Wake, Waker};
#[cfg(feature = "telemetry")]
use std::time::Duration;
use facet::Facet;
use orchid_api_derive::Coding;
#[cfg(feature = "telemetry")]
use orchid_api_traits::Encode;
use scoped_value::{ScopeExt, WithRef};
#[cfg(feature = "telemetry")]
use ureq::Agent;
#[cfg(feature = "telemetry")]
use websocket::stream::sync::NetworkStream;
#[cfg(feature = "telemetry")]
use websocket::sync::Client;
#[cfg(feature = "telemetry")]
use websocket::{ClientBuilder, OwnedMessage};
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
pub struct TracedEvent {
/// Where the event was triggered
pub info: Option<Arc<TraceInfo>>,
@@ -13,16 +32,24 @@ pub struct TracedEvent {
pub kind: TracedEventKind,
}
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum TracedEventKind {
Future(FutureEvent),
/// Events triggered by a cloned-out waker
Waker(WakerEvent),
/// Events triggered by user to be pushed on log
Log(String),
}
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
pub struct FutureEvent {
pub id: u64,
pub kind: FutureEventKind,
}
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum FutureEventKind {
New,
Drop,
@@ -34,6 +61,7 @@ pub enum FutureEventKind {
Trampoline,
}
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
pub struct WakerEvent {
/// A sequential unique ID assigned to this waker object for tracing
/// purposes
@@ -41,6 +69,8 @@ pub struct WakerEvent {
pub kind: WakerEventKind,
}
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum WakerEventKind {
New { future_id: u64 },
Wake { by_ref: WakeIsByRef },
@@ -48,6 +78,8 @@ pub enum WakerEventKind {
}
/// Distinguish between [Wake::wake_by_ref] and [Wake::wake] calls
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
#[repr(u8)]
pub enum WakeIsByRef {
/// Call by reference
ByRef,
@@ -57,6 +89,7 @@ pub enum WakeIsByRef {
ByValue,
}
#[derive(Facet, Debug, Coding, Clone, Hash, PartialEq, Eq)]
pub struct TraceInfo {
pub parent: Option<Arc<TraceInfo>>,
pub label: String,
@@ -64,6 +97,85 @@ pub struct TraceInfo {
pub static ON_TRACE: OnceLock<Box<dyn Fn(TracedEvent) + Send + Sync>> = OnceLock::new();
#[cfg(feature = "telemetry")]
static UREQ_AGENT: OnceLock<Agent> = OnceLock::new();
#[cfg(feature = "telemetry")]
struct WebsocketState {
builder: ClientBuilder<'static>,
client: Client<Box<dyn NetworkStream + Send>>,
}
#[cfg(feature = "telemetry")]
static WEBSOCKET_CONN: OnceLock<Mutex<WebsocketState>> = OnceLock::new();
pub fn init_trace<F: Fn(TracedEvent) + Send + Sync + 'static>(if_not_ready: bool, on_trace: F) {
let new = ON_TRACE.set(Box::new(on_trace)).is_ok();
assert!(new || if_not_ready, "Trace handler already configured");
}
#[cfg(feature = "telemetry")]
pub fn get_ureq() -> &'static Agent {
UREQ_AGENT.get_or_init(|| {
Agent::config_builder().timeout_global(Some(Duration::from_secs(40))).build().new_agent()
})
}
#[cfg(feature = "telemetry")]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum Role {
/// Clear the topic on the server, this should be true on exactly one instance
/// of this function for a given topic, and one that is statically known to
/// come online first.
Leader,
/// Force-initialize, fail if a conflicting tracing callback is already set,
/// join the topic without resetting
Instance,
/// Do nothing if already initialized
Library,
}
/// Initialize telemetry by posting to a checkers-api installation.
#[cfg(feature = "telemetry")]
pub fn init_telemetry(url: impl AsRef<str>, topic: impl AsRef<str>, role: Role) {
let url = url.as_ref();
let topic = topic.as_ref();
let url = url.strip_suffix('/').unwrap_or(url);
if role == Role::Leader {
let url = url.strip_prefix("ws").map_or(Cow::Borrowed(url), |s| Cow::Owned(format!("http{s}")));
get_ureq().post(format!("{url}/topic/{topic}/clear")).send_empty().unwrap();
}
fn encode_event(ev: &TracedEvent) -> Vec<u8> {
let mut vec = Vec::new();
ev.encode_vec(&mut vec);
vec
}
if url.starts_with("http") {
let post_url = format!("{url}/topic/{topic}/push");
let ureq = get_ureq();
init_trace(role == Role::Library, move |ev| {
ureq.post(&post_url).send(&encode_event(&ev)).unwrap();
});
return;
};
let ws_write_url = format!("{url}/topic/{topic}/write");
let mut builder = ClientBuilder::new(&ws_write_url).unwrap();
let client = builder.connect(None).unwrap();
let _ = WEBSOCKET_CONN.set(Mutex::new(WebsocketState { builder, client }));
init_trace(role == Role::Library, move |ev| {
let data = OwnedMessage::Binary(encode_event(&ev));
let WebsocketState { builder, client } = &mut *WEBSOCKET_CONN.get().unwrap().lock().unwrap();
for _attempt in 0..3 {
match client.send_message(&data) {
Ok(()) => return,
Err(_) => {
*client = builder.connect(None).unwrap();
std::thread::sleep(Duration::from_secs_f32(0.5));
},
}
}
panic!("Could not send telemetry to {ws_write_url} in 3 attempts");
})
}
thread_local! {
static TRACE_INFO: RefCell<Option<Arc<TraceInfo>>> = const { RefCell::new(None) }
}
@@ -76,8 +188,10 @@ fn emit(kind: TracedEventKind) {
}
/// An empty section, a "moment" of execution time
pub async fn event(label: &str) { section(label, async {}).await }
pub fn event(message: String) { emit(TracedEventKind::Log(message)) }
/// A phase of execution represented by an instance of a future, and by a stack
/// frame whenever it is polled
pub async fn section<F: Future>(label: impl AsRef<str>, fut: F) -> F::Output {
let parent = TRACE_INFO.get();
let info = Arc::new(TraceInfo { parent, label: label.as_ref().to_string() });
-2
View File
@@ -1,2 +0,0 @@
cargo build -p orchid-std
cargo run -p orcx -- lex --file .\examples\hello-world\main.orc
+1
View File
@@ -6,6 +6,7 @@
//! the channel with the same protocol outlined in [crate::proto]
use unsync_pipe::{Reader, Writer};
/// !Send !Sync owned waker
///
/// This object is [Clone] for convenience but it has `drop` and no `clone` so
+2 -2
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::rc::Rc;
use orchid_api_derive::{Coding, Hierarchy};
@@ -15,6 +14,8 @@ pub enum LogStrategy {
File { path: String, append: bool },
/// Discard any log output
Discard,
/// Forward to Checkers
Checkers,
}
#[derive(Clone, Debug, Coding)]
@@ -27,6 +28,5 @@ pub struct Logger {
#[extends(ExtHostNotif)]
pub struct Log {
pub category: TStr,
pub path: Rc<String>,
pub message: String,
}
+11 -3
View File
@@ -35,17 +35,22 @@ static HOST_INTRO: &[u8] = b"Orchid host, binary API v0\n";
#[derive(Clone, Debug)]
pub struct HostHeader {
pub logger: logging::Logger,
pub checkers_url: Option<String>,
}
impl Decode for HostHeader {
async fn decode<R: AsyncRead + ?Sized>(mut read: Pin<&mut R>) -> io::Result<Self> {
read_exact(read.as_mut(), HOST_INTRO).await?;
Ok(Self { logger: logging::Logger::decode(read).await? })
Ok(Self {
logger: logging::Logger::decode(read.as_mut()).await?,
checkers_url: Option::<String>::decode(read).await?,
})
}
}
impl Encode for HostHeader {
async fn encode<W: AsyncWrite + ?Sized>(&self, mut write: Pin<&mut W>) -> io::Result<()> {
write.write_all(HOST_INTRO).await?;
self.logger.encode(write.as_mut()).await
self.logger.encode(write.as_mut()).await?;
self.checkers_url.encode(write).await
}
}
@@ -167,7 +172,10 @@ mod tests {
#[test]
fn host_header_enc() {
spin_on(async {
let hh = HostHeader { logger: Logger { routing: HashMap::new(), default: None } };
let hh = HostHeader {
logger: Logger { routing: HashMap::new(), default: None },
checkers_url: None,
};
let mut enc = &enc_vec(&hh)[..];
eprintln!("Encoded to {enc:?}");
HostHeader::decode(Pin::new(&mut enc)).await.unwrap();
+1 -62
View File
@@ -5,17 +5,13 @@
//! control
use std::cell::RefCell;
use std::fmt::Display;
use std::pin::pin;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Context, Poll, Wake, Waker};
use std::thread::panicking;
use futures::Stream;
use itertools::Itertools;
use scoped_value::{ScopeExt, WithRef};
struct OnPollWaker<F: Fn() + 'static>(Waker, F);
impl<F: Fn() + 'static> Wake for OnPollWaker<F> {
@@ -90,63 +86,6 @@ pub fn on_stream_wake<'a, S: Stream + 'a>(
})
}
thread_local! {
static LABEL_STATE: RefCell<Vec<Rc<String>>> = const { RefCell::new(Vec::new()) };
}
pub fn scoped_logs() -> bool { LABEL_STATE.with_ref(|v| !v.is_empty()) }
/// Add a label to the "label stack" for the duration of a future that helps you
/// efficiently visualize important aspects of the call stack during logging
pub async fn with_label<Fut: Future>(label: &str, f: Fut) -> Fut::Output {
let mut new_lbl = LABEL_STATE.get();
new_lbl.push(Rc::new(label.to_string()));
LABEL_STATE.async_scope(&mut new_lbl, f).await
}
pub fn any_labels() -> bool { LABEL_STATE.try_with(|_| true).unwrap_or(false) }
/// Allows to print the label stack
pub fn label() -> impl Display + Clone + Send + Sync + 'static {
LABEL_STATE.with_ref(|lbl| lbl.iter().join("/"))
}
/// Displays the label stack when printed
pub struct Label;
impl Display for Label {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", label()) }
}
/// Attaches generic eprintln handlers to a future
pub async fn eprint_events<Fut: Future>(note: &str, f: Fut) -> Fut::Output {
let label = label();
let note1 = note.to_string();
on_wake(
wrap_poll(f, |cb| {
eprintln!("{Label} polling {note}");
eprintln!("{Label} polled {note} (ready? {})", cb())
}),
move || eprintln!("{label} woke {note1}"),
)
.await
}
/// Attaches generic eprintln handlers to a stream
pub fn eprint_stream_events<'a, S: Stream + 'a>(
note: &'a str,
s: S,
) -> impl Stream<Item = S::Item> + 'a {
let label = label();
let note1 = note.to_string();
on_stream_wake(
wrap_poll_next(s, move |cb| {
eprintln!("{Label} polling {note}");
eprintln!("{Label} polled {note} (ready? {})", cb())
}),
move || eprintln!("{label} woke {note1}"),
)
}
thread_local! {
static WAKE_LODUD: RefCell<bool> = const { RefCell::new(false) };
}
@@ -166,7 +105,7 @@ impl Wake for SpinWaker {
fn wake(self: Arc<Self>) {
self.repeat.store(true, Ordering::SeqCst);
if WAKE_LODUD.with_borrow(|k| *k) {
eprintln!("{Label} Triggered repeat for spin_on")
eprintln!("Triggered repeat for spin_on")
}
}
}
+2
View File
@@ -33,6 +33,8 @@ substack = "1.1.1"
trait-set = "0.3.0"
tokio = { version = "1.52.3", optional = true }
scoped-value = { version = "1.0.0", path = "../scoped-value" }
checklings = { version = "0.1.0", path = "../checklings" }
scoped-log = { version = "0.1.0", path = "../scoped-log" }
[features]
tokio = ["dep:tokio", "tokio/time", "tokio/rt"]
+16 -14
View File
@@ -18,8 +18,9 @@ use hashbrown::HashMap;
use orchid_api_traits::{Decode, Encode, Request, UnderRoot};
use orchid_async_utils::debug::{PanicOnDrop, assert_no_drop};
use orchid_async_utils::{Handle, cancel_cleanup, local_set, to_task};
use scoped_log::{LogWriter, NullLogger, log};
use crate::{LogWriter, NullLogWriter, clone, finish_or_stash, log, stash, with_stash};
use crate::{clone, finish_or_stash, stash, with_stash};
// TODO: revise error handling; error recovery is never partial, it always
// requires dropping the server, client, and all requests
@@ -103,7 +104,7 @@ pub trait MsgWriter {
/// For initiating outbound requests and notifications
pub trait Client {
fn log_strat(&self) -> Option<&str>;
fn log_category(&self) -> Option<&str>;
fn start_request(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn ReqWriter>>>;
fn start_notif(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn MsgWriter>>>;
}
@@ -114,7 +115,7 @@ impl<T: Client + ?Sized> ClientExt for T {}
#[allow(async_fn_in_trait)]
pub trait ClientExt: Client {
fn log(&self) -> Rc<dyn LogWriter> {
if let Some(strat) = self.log_strat() { log(strat) } else { Rc::new(NullLogWriter) }
if let Some(strat) = self.log_category() { log(strat) } else { Rc::new(NullLogger) }
}
#[allow(
clippy::await_holding_refcell_ref,
@@ -123,7 +124,7 @@ pub trait ClientExt: Client {
async fn request<T: Request + UnderRoot<Root: Encode>>(&self, t: T) -> io::Result<T::Response> {
let log = self.log();
let req_txt = format!("{t:?}");
writeln!(log, "Sending request {req_txt}").await;
writeln!(log, "Sending request {req_txt}");
let start_req = self.start_request();
// This section must finish if it has started, and the returned writer's `send`
// must be called as well.
@@ -165,8 +166,8 @@ pub trait ClientExt: Client {
let result = T::Response::decode(reader).await;
rep.finish().await;
match &result {
Err(e) => writeln!(log, "Error on request {req_txt}: {e}").await,
Ok(rep) => writeln!(log, "Request {req_txt} got response {rep:?}").await,
Err(e) => writeln!(log, "Error on request {req_txt}: {e}"),
Ok(rep) => writeln!(log, "Request {req_txt} got response {rep:?}"),
}
result
})),
@@ -384,7 +385,7 @@ impl IoClient {
}
}
impl Client for IoClient {
fn log_strat(&self) -> Option<&str> { self.log.as_deref() }
fn log_category(&self) -> Option<&str> { self.log.as_deref() }
fn start_notif(&self) -> LocalBoxFuture<'static, io::Result<Box<dyn MsgWriter>>> {
let output = self.output.clone();
Box::pin(async {
@@ -678,12 +679,13 @@ impl IoCommServer {
pub mod test {
use std::cell::RefCell;
use checklings::section;
use futures::channel::mpsc;
use futures::future::{join3, select};
use futures::{SinkExt, StreamExt};
use orchid_api_derive::{Coding, Hierarchy};
use orchid_api_traits::Request;
use orchid_async_utils::debug::{spin_on, with_label};
use orchid_async_utils::debug::spin_on;
use unsync_pipe::pipe;
use super::*;
@@ -787,12 +789,12 @@ pub mod test {
async |_| panic!("Only the other server expected requests"),
);
spin_on(join3(
with_label("reply", async move {
section("reply", async move {
right_srv_fut.await.unwrap();
exit.send(()).unwrap();
let _client = right.client;
}),
with_label("client", async move {
section("client", async move {
left_srv_fut.await.unwrap();
let _ctx = left.cx;
}),
@@ -820,9 +822,9 @@ pub mod test {
},
);
spin_on(join3(
with_label("server", async { right_srv_fut.await.unwrap() }),
with_label("client", listen_no_ingress(left.srv)),
with_label(
section("server", async { right_srv_fut.await.unwrap() }),
section("client", listen_no_ingress(left.srv)),
section(
"outer_stash",
with_stash(async {
with_stash(async {
@@ -863,7 +865,7 @@ pub mod test {
);
spin_on(join3(
async { right_srv_fut.await.unwrap() },
with_label("client", listen_no_ingress(left.srv)),
section("client", listen_no_ingress(left.srv)),
async {
with_stash(Box::pin(async {
select(
+1 -1
View File
@@ -46,7 +46,7 @@ pub fn init_spinlock_probe(spawner: Rc<dyn SpawnSrv>, name: &str) {
// mutex critical section
let Some(last_beat) = hb2.lock().ok() else { break };
let now = Instant::now();
if *last_beat + Duration::from_secs(3) < now {
if *last_beat + *DEADLOCK_TIMEOUT.lock().unwrap() < now {
panic!("{thread_name} thread spinlocked!");
}
}
+6 -1
View File
@@ -9,7 +9,7 @@ use std::{fmt, hash};
use futures::future::LocalBoxFuture;
use scoped_value::{ScopeExt, WithRef};
use crate::api;
use crate::{ContextLayer, api};
/// Implementation-specific backing object for an interned string.
pub trait IStrHandle: AsRef<str> {
@@ -117,6 +117,11 @@ thread_local! {
static INTERNER: RefCell<Option<Rc<dyn InternerSrv>>> = const { RefCell::new(None) };
}
/// Get a context layer that sets a global interner according to [with_interner]
pub fn mk_interner_cx(val: Rc<dyn InternerSrv>) -> ContextLayer {
ContextLayer::from_thread_local(Some(val), &INTERNER)
}
/// Install a global interner. Within this future, the global [is], [iv], [es]
/// and [ev] functions call the provided [InternerSrv]
pub async fn with_interner<F: Future>(val: Rc<dyn InternerSrv>, fut: F) -> F::Output {
-2
View File
@@ -27,8 +27,6 @@ mod join;
pub use join::*;
mod location;
pub use location::*;
mod logging;
pub use logging::*;
mod match_mapping;
mod name;
pub use name::*;
-92
View File
@@ -1,92 +0,0 @@
use std::any::Any;
use std::cell::RefCell;
use std::fmt;
use std::fmt::{Arguments, Display};
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use orchid_async_utils::debug::label;
use scoped_value::{ScopeExt, WithRef};
use crate::{api, clone};
/// A first argument to [write!] and [writeln!] that causes them to return a
/// future.
pub trait LogWriter {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()>;
}
pub struct NullLogWriter;
impl LogWriter for NullLogWriter {
fn write_fmt<'a>(&'a self, _: Arguments<'a>) -> LocalBoxFuture<'a, ()> { Box::pin(async {}) }
}
/// Injectable logging service passed to [with_logger]
pub trait Logger: Any {
/// Obtain a writer that processes the message according to the given category
fn writer_at(&self, category: &str, path: Rc<dyn fmt::Display>) -> Rc<dyn LogWriter>;
fn writer(&self, category: &str) -> Rc<dyn LogWriter> {
self.writer_at(category, Rc::new(label()))
}
/// Obtain a serializable limited description of what will eventually happen
/// to the message
fn strat(&self, category: &str) -> api::LogStrategy;
/// Determine whether a certain category would get processed at all. This is a
/// shortcut
fn is_active(&self, category: &str) -> bool {
!matches!(self.strat(category), api::LogStrategy::Discard)
}
}
thread_local! {
static LOGGER: RefCell<Option<Rc<dyn Logger>>> = const { RefCell::new(None)};
}
/// Within the future passed to this function the freestanding [log] and
/// [get_logger] functions can be used
pub async fn with_logger<F: Future>(logger: impl Logger + 'static, fut: F) -> F::Output {
LOGGER.async_scope(&mut Some(Rc::new(logger)), fut).await
}
/// Obtain an async log writer
///
/// ```
/// use orchid_base::log;
/// async {
/// let user = "lorentz";
/// writeln!(log("info"), "Hello from {user}").await
/// };
/// ```
pub fn log(category: &str) -> Rc<dyn LogWriter> { get_logger().writer(category) }
/// Obtain an async log writer with an explicit label
pub fn log_at(category: &str, label: impl Display + 'static) -> Rc<dyn LogWriter> {
get_logger().writer_at(category, Rc::new(label))
}
/// Obtain a reference to the current [Logger]. This is mostly useful for
/// [Logger::is_active]-based optimizations
pub fn get_logger() -> Rc<dyn Logger> { LOGGER.get().expect("Logger not set!") }
#[derive(Clone)]
pub struct TestLogger(Rc<dyn Fn(String) -> LocalBoxFuture<'static, ()>>);
impl LogWriter for TestLogger {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()> {
(self.0)(fmt.to_string())
}
}
impl Logger for TestLogger {
fn strat(&self, _category: &str) -> orchid_api::LogStrategy { orchid_api::LogStrategy::Default }
fn writer_at(&self, _category: &str, _label: Rc<dyn Display>) -> std::rc::Rc<dyn LogWriter> {
Rc::new(self.clone())
}
}
impl TestLogger {
pub fn new(f: impl AsyncFn(String) + 'static) -> Self {
let f = Rc::new(f);
Self(Rc::new(move |s| clone!(f; Box::pin(async move { f(s).await }))))
}
}
impl Default for TestLogger {
fn default() -> Self { TestLogger::new(async |s| eprint!("{s}")) }
}
+81 -15
View File
@@ -10,32 +10,87 @@ use std::time::Duration;
/// Can be extended from the same HOFs
use std::{any::Any, cell::OnceCell};
#[cfg(feature = "tokio")]
use async_event::Event;
use checklings::section;
use futures::future::LocalBoxFuture;
use orchid_async_utils::debug::{Label, with_label};
use scoped_value::{ScopeExt, StackExt, WithRef};
use itertools::Itertools;
use scoped_value::{ScopeExt, StackExt, WithMut, WithRef};
use crate::on_drop;
trait DynContextLayer {
fn apply<'a>(&'a self, inner: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()>;
/// Create a future that initializes the context on poll and resets it on
/// poll-return
fn wrap<'a>(&'a self, inner: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()>;
}
impl<F: for<'a> Fn(LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()>> DynContextLayer for F {
fn apply<'a>(&'a self, inner: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> { self(inner) }
fn wrap<'a>(&'a self, inner: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> { self(inner) }
}
struct ThreadLocalContext<T, S: 'static> {
value: RefCell<T>,
thread_local: &'static S,
}
impl<T, S: WithMut<'static, T>> DynContextLayer for ThreadLocalContext<T, S> {
fn wrap<'a>(&'a self, mut inner: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(futures::future::poll_fn(move |cx| {
self.thread_local.sync_scope(&mut *self.value.borrow_mut(), || inner.as_mut().poll(cx))
}))
}
}
struct SetResetContext<A, FSet, FReset> {
set: RefCell<FSet>,
reset: RefCell<FReset>,
init: RefCell<Option<A>>,
}
impl<A, B, FSet: FnMut(A) -> B, FReset: FnMut(B) -> A> DynContextLayer
for SetResetContext<A, FSet, FReset>
{
fn wrap<'a>(&'a self, mut inner: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(futures::future::poll_fn(move |cx| {
let b = (self.set.borrow_mut())(self.init.take().unwrap());
let _guard = on_drop(|| {
*self.init.borrow_mut() = Some((self.reset.borrow_mut())(b));
});
inner.as_mut().poll(cx)
}))
}
}
#[derive(Clone)]
pub struct ContextLayer(Rc<dyn DynContextLayer>);
impl ContextLayer {
pub fn new<F: for<'a> Fn(LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> + 'static>(
/// Directly forward the [Self::apply] function
pub fn from_apply_fn<
F: for<'a> Fn(LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> + 'static,
>(
layer: F,
) -> Self {
Self(Rc::new(layer))
}
/// Assign a value to a [thread_local!] when the context is live
pub fn from_thread_local<T: 'static>(
value: T,
thread_local: &'static impl WithMut<'static, T>,
) -> Self {
Self(Rc::new(ThreadLocalContext { value: RefCell::new(value), thread_local }))
}
/// Call `set` when the context becomes live, and `reset` when it goes to the
/// background
pub fn from_set_reset<T: 'static, U>(
init: T,
set: impl FnMut(T) -> U + 'static,
reset: impl FnMut(U) -> T + 'static,
) -> Self {
Self(Rc::new(SetResetContext {
init: RefCell::new(Some(init)),
set: RefCell::new(set),
reset: RefCell::new(reset),
}))
}
pub async fn apply<F: Future>(&self, fut: F) -> F::Output {
let mut c = OnceCell::default();
self.0.apply(Box::pin(async { c.set(fut.await).unwrap_or_else(|_| unreachable!()) })).await;
self.0.wrap(Box::pin(async { c.set(fut.await).unwrap_or_else(|_| unreachable!()) })).await;
c.take().expect("context layer appears not to have executed its argument")
}
}
@@ -44,8 +99,17 @@ thread_local! {
static CONTEXT: RefCell<Vec<ContextLayer>> = const { RefCell::new(Vec::new()) };
}
pub async fn with_context<F: Future>(cl: ContextLayer, fut: F) -> F::Output {
CONTEXT.async_frame(cl.clone(), cl.apply(fut)).await.0
/// Apply a context layer and also add it to the context so that it will also be
/// applied and added to context in any tasks spawned within the enclosed future
pub async fn with_context<F: Future>(
cl: impl IntoIterator<Item = ContextLayer>,
fut: F,
) -> F::Output {
let cxv_apply = cl.into_iter().collect_vec();
let mut cxv_push = cxv_apply.iter().map(|cx| Some(cx.clone())).collect_vec();
let fut = (cxv_apply.iter().rev())
.fold::<LocalBoxFuture<_>, _>(Box::pin(fut), |fut, cx| Box::pin(cx.apply(fut)));
CONTEXT.async_frame(&mut cxv_push, fut).await
}
/// Handle for a task that gets polled independently from the codepath that
@@ -101,20 +165,21 @@ fn spawn_inner<F: Future + 'static>(
fut: F,
) -> TaskHandle<F::Output> {
let mut cx = CONTEXT.get();
let spawner_label = Label.to_string();
let th_obj = srv.spawn_obj(
keepalive,
Box::pin(async move {
// Apply
// Reestablish the context stack
CONTEXT
.async_scope(&mut cx, async {
// apply each individual context
let val = CONTEXT.with_ref(|v| {
(v.iter().cloned().rev())
.fold::<LocalBoxFuture<'_, _>, _>(Box::pin(fut), |fut, cx_lvl| {
Box::pin(async move { cx_lvl.apply(fut).await })
})
});
Box::new(with_label(&format!("{spawn_lbl}[{spawner_label}]"), val).await) as _
// restart the label stack
Box::new(section(spawn_lbl, val).await) as _
})
.await
}),
@@ -141,9 +206,10 @@ thread_local! {
static SPAWN: RefCell<Option<Rc<dyn SpawnSrv>>> = const { RefCell::new(None) };
}
pub async fn with_spawn<F: Future>(spawn: Rc<dyn SpawnSrv>, fut: F) -> F::Output {
SPAWN.async_scope(&mut Some(spawn), fut).await
pub fn mk_spawn_ctx(spawn: Rc<dyn SpawnSrv>) -> ContextLayer {
ContextLayer::from_thread_local(Some(spawn), &SPAWN)
}
pub fn get_spawn() -> Rc<dyn SpawnSrv> { SPAWN.get().expect("Spawner not set!") }
pub async fn sleep(delay: Duration) { get_spawn().sleep(delay).await }
+2 -2
View File
@@ -11,11 +11,11 @@ use std::cell::RefCell;
use std::pin::Pin;
use std::task::{Context, Poll};
use checklings::section;
use futures::StreamExt;
use futures::future::LocalBoxFuture;
use futures::stream::FuturesUnordered;
use orchid_async_utils::cancel_cleanup;
use orchid_async_utils::debug::with_label;
thread_local! {
/// # Invariant
@@ -53,7 +53,7 @@ pub fn stash<F: Future<Output = ()> + 'static>(fut: F) {
}
return;
};
stash.push(Box::pin(with_label("stashed", fut)))
stash.push(Box::pin(section("stashed", fut)))
})
}
+4
View File
@@ -9,6 +9,9 @@ edition = "2024"
async-event = "0.2.1"
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4"
checklings = { version = "0.1.0", path = "../checklings", features = [
"telemetry",
] }
chrono = "0.4.45"
derive_destructure = "1.0.0"
dyn-clone = "1.0.20"
@@ -32,6 +35,7 @@ orchid-async-utils = { version = "0.1.0", path = "../orchid-async-utils" }
orchid-base = { version = "0.1.0", path = "../orchid-base", default-features = false }
ordered-float = "5.3.0"
pastey = "0.2.3"
scoped-log = { version = "0.1.0", path = "../scoped-log" }
scoped-value = { version = "1.0.0", path = "../scoped-value" }
substack = "1.1.1"
tokio = { version = "1.52.3", optional = true, features = ["io-std", "time"] }
+8 -8
View File
@@ -17,8 +17,9 @@ use itertools::Itertools;
use memo_map::MemoMap;
use never::Never;
use orchid_api_traits::{Decode, Encode, enc_vec};
use orchid_base::{FmtCx, FmtCxImpl, FmtUnit, Format, Sym, log, take_first};
use scoped_value::{ScopeExt, WithRef};
use orchid_base::{ContextLayer, FmtCx, FmtCxImpl, FmtUnit, Format, Sym, take_first};
use scoped_log::log;
use scoped_value::WithRef;
use crate::gen_expr::{GExpr, bot};
use crate::{
@@ -95,7 +96,7 @@ impl<A: OwnedAtom> AtomOps for OwnedAtomOps<A> {
eprintln!("after arg1 {arg:?}");
let arg_s = take_first(&arg.print(&FmtCxImpl::default()).await, true);
eprintln!("after arg2");
writeln!(log("msg"), "owned call {} {}", fun_s, arg_s,).await;
writeln!(log("msg"), "owned call {} {}", fun_s, arg_s,);
eprintln!("after debug msg");
take_atom(id.unwrap()).await.dyn_call(arg).await
})
@@ -107,8 +108,7 @@ impl<A: OwnedAtom> AtomOps for OwnedAtomOps<A> {
"owned call_ref {} {}",
take_first(&AtomReadGuard::new(id.unwrap()).await.dyn_print().await, false),
take_first(&arg.print(&FmtCxImpl::default()).await, true),
)
.await;
);
AtomReadGuard::new(id.unwrap()).await.dyn_call_ref(arg).await
})
}
@@ -352,8 +352,8 @@ thread_local! {
static OBJ_STORE: RefCell<Option<Rc<ObjStore>>> = const { RefCell::new(None) };
}
pub(crate) fn with_obj_store<'a>(fut: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move { OBJ_STORE.async_scope(&mut Some(Rc::new(ObjStore::default())), fut).await })
pub(crate) fn mk_owned_atom_cx() -> ContextLayer {
ContextLayer::from_thread_local(Some(Rc::new(ObjStore::default())), &OBJ_STORE)
}
pub(crate) fn get_obj_store() -> Rc<ObjStore> {
@@ -380,5 +380,5 @@ pub async fn debug_print_obj_store(show_atoms: bool) {
message += &format!("\n{k:?} -> {}", take_first(&atom.dyn_print().await, true));
}
}
writeln!(log("debug"), "{message}").await
writeln!(log("debug"), "{message}")
}
+3 -2
View File
@@ -6,7 +6,8 @@ use async_once_cell::OnceCell;
use futures::AsyncWrite;
use futures::future::LocalBoxFuture;
use orchid_api_traits::{Coding, enc_vec};
use orchid_base::{FmtUnit, Sym, log};
use orchid_base::{FmtUnit, Sym};
use scoped_log::log;
use crate::gen_expr::{GExpr, bot};
use crate::{
@@ -81,7 +82,7 @@ impl<T: ThinAtom> AtomOps for ThinAtomOps<T> {
fn drop<'a>(&'a self, AtomCtx(buf, _): AtomCtx<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move {
let string_self = T::decode_slice(&mut &buf[..]).print().await;
writeln!(log("warn"), "Received drop signal for non-drop atom {string_self:?}").await;
writeln!(log("warn"), "Received drop signal for non-drop atom {string_self:?}");
})
}
}
+2 -6
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use std::time::Duration;
use checklings::section;
use futures::future::LocalBoxFuture;
use orchid_async_utils::debug::{on_wake, with_label};
use orchid_base::{SpawnExt, SpawnSrv, future_to_outbox, future_to_vt, vt_to_future};
use crate::{ExtPort, ExtensionBuilder, api};
@@ -23,11 +23,7 @@ impl SpawnSrv for Spawner {
fut: LocalBoxFuture<'static, Box<dyn std::any::Any>>,
) -> Box<dyn orchid_base::DynTaskHandle> {
let (future, outbox) = future_to_outbox(fut);
let hand_bin = (self.0.spawn)(
self.0.data,
keepalive,
future_to_vt(on_wake(with_label("ext", future), || eprintln!("Extension future woken"))),
);
let hand_bin = (self.0.spawn)(self.0.data, keepalive, future_to_vt(section("ext", future)));
Box::new(outbox.into_task_handle(hand_bin))
}
}
+78 -88
View File
@@ -6,6 +6,7 @@ use std::pin::{Pin, pin};
use std::rc::Rc;
use std::time::Duration;
use checklings::{Role, init_telemetry};
use futures::future::{LocalBoxFuture, join_all, join3};
use futures::{AsyncReadExt, AsyncWriteExt, FutureExt, StreamExt, select, stream};
use hashbrown::HashMap;
@@ -13,32 +14,33 @@ use itertools::Itertools;
use orchid_api_traits::{Decode, Encode, Request, UnderRoot, enc_vec};
use orchid_base::deadlock_probe::ThreadlockProbe;
use orchid_base::{
Client, ClientExt, CommCx, Comment, IoComm, MsgReader, MsgReaderExt, ReqHandleExt, ReqReaderExt,
ReqWait, Snippet, SpawnExt, Sym, TokFromApiCx, TokToApiCx, TokenVariant, Witness,
char_filter_match, char_filter_union, es, io_comm, is, log, mk_char_filter, spawn, spawn_weak,
try_with_reporter, ttv_from_api, ttv_into_api, with_interner, with_logger, with_reporter,
with_spawn, with_stash,
Client, ClientExt, CommCx, Comment, ContextLayer, IoComm, MsgReader, MsgReaderExt, ReqHandleExt,
ReqReaderExt, ReqWait, Snippet, SpawnExt, Sym, TokFromApiCx, TokToApiCx, TokenVariant, Witness,
char_filter_match, char_filter_union, es, io_comm, is, mk_char_filter, mk_interner_cx,
mk_spawn_ctx, spawn, spawn_weak, try_with_reporter, ttv_from_api, ttv_into_api, with_context,
with_reporter, with_stash,
};
use scoped_log::{LOGGER, Logger, NullLogger, log, with_logger};
use scoped_value::{ScopeExt, WithMut, WithRef};
use substack::Substack;
use unsync_pipe::pipe;
use crate::gen_expr::serialize;
use crate::gen_tok::{GenTokTree, drop_flex_tree, read_flex_tree, with_flex_tree_context};
use crate::gen_tok::{GenTokTree, drop_flex_tree, mk_flex_tree_cx, read_flex_tree};
use crate::interner::new_interner;
use crate::logger::LoggerImpl;
use crate::tree::{TreeIntoApiCtxImpl, get_lazy, with_lazy_member_store};
use crate::tree::{TreeIntoApiCtxImpl, get_lazy, mk_lazy_tree_cx};
use crate::trivial_req::TrivialReqCycle;
use crate::{
AtomCtx, AtomTypeId, BorrowedExprStore, CtedObj, DynSystemCardExt, DynSystemCtor, Expr,
ExprHandle, ExtPort, LexContext, ParsCtx, SysCtx, SystemCtor, api, dyn_cted, ekey_not_applicable,
linev_into_api, resolve_atom_type, take_atom, with_funs_ctx, with_obj_store, with_refl_roots,
linev_into_api, mk_funs_cx, mk_owned_atom_cx, mk_reflection_cx, resolve_atom_type, take_atom,
with_sys,
};
thread_local! {
static CLIENT: RefCell<Option<Rc<dyn Client>>> = const { RefCell::new(None) };
static CTX: RefCell<Option<CommCx>> = const { RefCell::new(None) };
static COMM_CTX: RefCell<Option<CommCx>> = const { RefCell::new(None) };
}
fn get_client() -> Rc<dyn Client> { CLIENT.get().expect("No client!") }
@@ -46,14 +48,14 @@ fn get_client() -> Rc<dyn Client> { CLIENT.get().expect("No client!") }
/// Do not expect any more requests or notifications, exit once all pending
/// requests settle
pub async fn exit() {
let cx = CTX.with_mut(|cx| cx.take());
let cx = COMM_CTX.with_mut(|cx| cx.take());
cx.unwrap().exit().await.unwrap()
}
/// Set the client used for global [request] and [notify] functions within the
/// runtime of this future
pub async fn with_comm<F: Future>(c: Rc<dyn Client>, ctx: CommCx, fut: F) -> F::Output {
CLIENT.async_scope(&mut Some(c), CTX.async_scope(&mut Some(ctx), fut)).await
CLIENT.async_scope(&mut Some(c), COMM_CTX.async_scope(&mut Some(ctx), fut)).await
}
thread_local! {
@@ -70,7 +72,7 @@ pub async fn request<T: Request + UnderRoot<Root = api::ExtHostReq>>(t: T) -> T:
let response = get_client().request(t).await.unwrap();
if !MUTE_REPLY.get() {
let ext = dyn_cted().inst().card().name();
writeln!(log("msg"), "{ext} {req_str} got response {response:?}").await;
writeln!(log("msg"), "{ext} {req_str} got response {response:?}");
}
response
}
@@ -115,47 +117,36 @@ impl<F: AsyncFnOnce(LocalBoxFuture<'_, ()>) + 'static> ContextModifier for F {
pub struct ExtensionBuilder {
pub name: &'static str,
pub systems: Vec<Box<dyn DynSystemCtor>>,
pub context: Vec<Box<dyn ContextModifier>>,
}
impl ExtensionBuilder {
/// Create a new extension
pub fn new(name: &'static str) -> Self { Self { name, systems: Vec::new(), context: Vec::new() } }
pub fn new(name: &'static str) -> Self { Self { name, systems: Vec::new() } }
/// Add a system to the extension
pub fn system(mut self, ctor: impl SystemCtor) -> Self {
self.systems.push(Box::new(ctor) as Box<_>);
self
}
/// Add some [scoped_value] state to the extension. Bear in mind that distinct
/// [crate::System] instances should not visibly affect each other
pub fn add_context(&mut self, fun: impl ContextModifier) {
self.context.push(Box::new(fun) as Box<_>);
}
/// Builder form of [Self::add_context]
pub fn context(mut self, fun: impl ContextModifier) -> Self {
self.add_context(fun);
self
}
/// Start the extension on a message channel, blocking the task until the peer
/// on the other side drops the extension. Extension authors would typically
/// pass the prepared builder into some function that is responsible for
/// managing the [ExtPort]
pub async fn run(mut self, mut ctx: ExtPort) {
self.add_context(with_funs_ctx);
self.add_context(with_flex_tree_context);
self.add_context(with_obj_store);
self.add_context(with_lazy_member_store);
self.add_context(with_refl_roots);
pub async fn run(self, mut ctx: ExtPort) {
let spawner = ctx.spawn.clone();
let host_header = api::HostHeader::decode(ctx.input.as_mut()).await.unwrap();
let host_header =
with_logger(NullLogger, api::HostHeader::decode(ctx.input.as_mut())).await.unwrap();
if let Some(url) = host_header.checkers_url {
init_telemetry(&url, "orchid", Role::Library);
}
let decls = (self.systems.iter().enumerate())
.map(|(id, sys)| (u16::try_from(id).expect("more than u16max system ctors"), sys))
.map(|(id, sys)| sys.decl(api::SysDeclId(NonZero::new(id + 1).unwrap())))
.collect_vec();
api::ExtensionHeader { name: self.name.to_string(), systems: decls.clone() }
.encode(ctx.output.as_mut())
.await
.unwrap();
ctx.output.as_mut().flush().await.unwrap();
let ext_header = api::ExtensionHeader { name: self.name.to_string(), systems: decls.clone() };
with_logger(NullLogger, async {
ext_header.encode(ctx.output.as_mut()).await.unwrap();
ctx.output.as_mut().flush().await.unwrap();
})
.await;
let logger1 = LoggerImpl::from_api(&host_header.logger);
let logger2 = logger1.clone();
let IoComm { client, cx: comm_ctx, srv } = io_comm(ctx.output, ctx.input, None);
@@ -178,7 +169,7 @@ impl ExtensionBuilder {
// Atom printing is never reported because it generates too much
// noise
if !matches!(req, api::HostExtReq::AtomReq(api::AtomReq::AtomPrint(_))) {
writeln!(log("msg"), "{} extension received request {req:?}", self.name).await;
writeln!(log("msg"), "{} extension received request {req:?}", self.name);
}
let deadlock = writeln!(ThreadlockProbe, "{} blocked on {req:?}", self.name);
let receipt = match req {
@@ -291,7 +282,7 @@ impl ExtensionBuilder {
},
}
}
writeln!(log("warn"), "Got notified about n/a character '{trigger_char}'").await;
writeln!(log("warn"), "Got notified about n/a character '{trigger_char}'");
expr_store.dispose().await;
handle.reply(&lex, None).await
})
@@ -441,57 +432,56 @@ impl ExtensionBuilder {
.await
},
);
// add essential services to the very tail, then fold all context into the run
// future
SYSTEM_TABLE
.async_scope(
&mut Some(RefCell::default()),
with_interner(
new_interner(),
with_logger(
logger2,
with_comm(
client,
comm_ctx,
with_spawn(
spawner,
(self.context.into_iter()).fold(
Box::pin(async {
let ping_proc = spawn_weak(async move {
loop {
let client3 = client2.clone();
let ping_req = spawn(async move {
let mut req = client3.start_request().await.unwrap();
api::ExtHostReq::Ping(api::Ping).encode(req.writer()).await.unwrap();
let ReqWait { canceller: _, future } = req.send();
let mut rep = future.await.unwrap();
let reader = rep
.reader()
.expect("We don't use the canceller; the ping loop exits on shutdown");
<()>::decode(reader).await.unwrap();
rep.finish().await;
});
let mut sleep_future = pin!(spawner2.sleep(Duration::from_secs(10)).fuse());
select! {
_ = ping_req.join().fuse() => (),
_ = sleep_future => {
writeln!(log("futures"), "Channel blocked").await;
break;
},
};
sleep_future.await
}
});
extension_fut.await.unwrap();
ping_proc.abort();
}) as LocalBoxFuture<()>,
|fut, cx| cx.apply(fut),
),
),
),
),
),
)
.await;
with_context(
[
ContextLayer::from_thread_local(Some(RefCell::default()), &SYSTEM_TABLE),
mk_interner_cx(new_interner()),
ContextLayer::from_thread_local(Some(Rc::new(logger2) as Rc<dyn Logger>), &LOGGER),
ContextLayer::from_thread_local(Some(client as Rc<dyn Client>), &CLIENT),
ContextLayer::from_thread_local(Some(comm_ctx), &COMM_CTX),
mk_spawn_ctx(spawner),
mk_funs_cx(),
mk_lazy_tree_cx(),
mk_owned_atom_cx(),
mk_flex_tree_cx(),
mk_reflection_cx(),
],
async move {
let ping_proc = spawn_weak(async move {
loop {
let client3 = client2.clone();
let ping_req = spawn(async move {
let mut req = client3.start_request().await.unwrap();
api::ExtHostReq::Ping(api::Ping).encode(req.writer()).await.unwrap();
let ReqWait { canceller: _, future } = req.send();
let mut rep = future.await.unwrap();
let reader =
rep.reader().expect("We don't use the canceller; the ping loop exits on shutdown");
<()>::decode(reader).await.unwrap();
rep.finish().await;
});
let mut sleep_future = pin!(spawner2.sleep(Duration::from_secs(10)).fuse());
select! {
_ = ping_req.join().fuse() => (),
_ = sleep_future => {
writeln!(log("futures"), "Channel blocked");
break;
},
};
sleep_future.await
}
});
extension_fut.await.unwrap();
ping_proc.abort();
},
)
.await;
// TODO: wrap destructor calls in context and make sure scoped-value does
// the same. Revisit whether we can design scoped-value to be usable in
// context so this concern is only handled once.
}
}
+4 -4
View File
@@ -12,8 +12,8 @@ use futures::{AsyncWrite, FutureExt};
use itertools::Itertools;
use never::Never;
use orchid_api_traits::Encode;
use orchid_base::{FmtCx, FmtUnit, OrcRes, Pos, Sym, clone};
use scoped_value::{ScopeExt, WithMut, WithRef};
use orchid_base::{ContextLayer, FmtCx, FmtUnit, OrcRes, Pos, Sym, clone};
use scoped_value::{WithMut, WithRef};
use trait_set::trait_set;
use crate::gen_expr::{GExpr, new_atom};
@@ -69,8 +69,8 @@ thread_local! {
static FUNS_CTX: RefCell<Option<HashMap<(api::SysId, Sym), FunRecord>>> = const { RefCell::new(None) };
}
pub(crate) fn with_funs_ctx<'a>(fut: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move { FUNS_CTX.async_scope(&mut Some(HashMap::default()), fut).await })
pub(crate) fn mk_funs_cx() -> ContextLayer {
ContextLayer::from_thread_local(Some(HashMap::default()), &FUNS_CTX)
}
#[derive(Clone)]
+5 -7
View File
@@ -6,10 +6,10 @@ use std::rc::Rc;
use futures::future::LocalBoxFuture;
use orchid_base::{
FmtCx, FmtUnit, Format, IdStore, ManagedStrat, OrcErrv, OrcRes, Snippet, SrcRange, Sym, TokTree,
Token, TokenVariant,
ContextLayer, FmtCx, FmtUnit, Format, IdStore, ManagedStrat, OrcErrv, OrcRes, Snippet, SrcRange,
Sym, TokTree, Token, TokenVariant,
};
use scoped_value::{ScopeExt, WithRef};
use scoped_value::WithRef;
use crate::gen_expr::{GExpr, serialize};
use crate::{BorrowedExprStore, Expr, ExprHandle, ToExpr, api, request, sys_id};
@@ -116,10 +116,8 @@ fn get_flex_tree_store() -> FlexTreeStore {
REFLEXIVE_TREES.get().expect("Flex tree store not initialized")
}
pub fn with_flex_tree_context<'a>(fut: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(
async move { REFLEXIVE_TREES.async_scope(&mut Some(FlexTreeStore::default()), fut).await },
)
pub fn mk_flex_tree_cx() -> ContextLayer {
ContextLayer::from_thread_local(Some(FlexTreeStore::default()), &REFLEXIVE_TREES)
}
pub async fn read_flex_tree(id: api::ReflexiveTreeId, in_const: Sym) -> GenTokTree {
+20 -29
View File
@@ -1,39 +1,29 @@
use std::fmt::{Arguments, Display};
use std::fmt::Arguments;
use std::fs::File;
use std::io::Write as _;
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use hashbrown::HashMap;
use orchid_base::{LogWriter, Logger, is};
use scoped_log::{LogWriter, Logger};
use crate::{api, notify};
use crate::api;
pub(crate) struct LogWriterImpl {
category: String,
label: Rc<dyn Display>,
strat: api::LogStrategy,
}
impl LogWriter for LogWriterImpl {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move {
match &self.strat {
api::LogStrategy::Discard => (),
api::LogStrategy::Default =>
notify(api::Log {
category: is(&self.category).await.to_api(),
path: Rc::new(self.label.to_string()),
message: fmt.to_string(),
})
.await,
api::LogStrategy::File { path, .. } => {
let mut file = (File::options().write(true).create(true).truncate(false).open(path))
.unwrap_or_else(|e| panic!("Could not open {path}: {e}"));
write!(file, "{} {fmt}", self.label)
.unwrap_or_else(|e| panic!("Could not write to {path}: {e}"));
},
}
})
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) {
match &self.strat {
api::LogStrategy::Discard => (),
api::LogStrategy::Checkers => checklings::event(fmt.to_string()),
api::LogStrategy::Default => eprint!("{} {fmt}", self.category),
api::LogStrategy::File { path, .. } => {
let mut file = (File::options().write(true).create(true).truncate(false).open(path))
.unwrap_or_else(|e| panic!("Could not open {path}: {e}"));
write!(file, "{fmt}").unwrap_or_else(|e| panic!("Could not write to {path}: {e}"));
},
}
}
}
@@ -49,13 +39,14 @@ impl LoggerImpl {
routing: api.routing.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
}
}
}
impl Logger for LoggerImpl {
fn writer_at(&self, category: &str, label: Rc<dyn Display>) -> Rc<dyn LogWriter> {
Rc::new(LogWriterImpl { category: category.to_string(), label, strat: self.strat(category) })
}
fn strat(&self, category: &str) -> orchid_api::LogStrategy {
(self.routing.get(category).cloned().or(self.default.clone()))
.expect("Unrecognized log category with no default strategy")
}
}
impl Logger for LoggerImpl {
fn writer(&self, category: &str) -> Rc<dyn LogWriter> {
Rc::new(LogWriterImpl { category: category.to_string(), strat: self.strat(category) })
}
fn is_active(&self, category: &str) -> bool { self.strat(category) != api::LogStrategy::Discard }
}
+4 -5
View File
@@ -2,12 +2,11 @@ use std::cell::{OnceCell, RefCell};
use std::rc::Rc;
use futures::FutureExt;
use futures::future::LocalBoxFuture;
use futures::lock::Mutex;
use hashbrown::HashMap;
use memo_map::MemoMap;
use orchid_base::{IStr, NameLike, VPath, es, iv};
use scoped_value::{ScopeExt, WithMut};
use orchid_base::{ContextLayer, IStr, NameLike, VPath, es, iv};
use scoped_value::WithMut;
use crate::{api, request, sys_id};
@@ -180,6 +179,6 @@ pub fn refl() -> ReflMod {
})
}
pub(crate) fn with_refl_roots<'a>(fut: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move { REFL_ROOTS.async_scope(&mut Some(HashMap::new()), fut).await })
pub(crate) fn mk_reflection_cx() -> ContextLayer {
ContextLayer::from_thread_local(Some(HashMap::new()), &REFL_ROOTS)
}
+4 -6
View File
@@ -8,8 +8,8 @@ use futures::future::{LocalBoxFuture, join_all};
use futures::{FutureExt, StreamExt};
use hashbrown::HashMap;
use itertools::Itertools;
use orchid_base::{IStr, Sym, is};
use scoped_value::{ScopeExt, WithRef};
use orchid_base::{ContextLayer, IStr, Sym, is};
use scoped_value::WithRef;
use substack::Substack;
use trait_set::trait_set;
@@ -184,10 +184,8 @@ thread_local! {
static LAZY_MEMBERS: RefCell<Option<LazyMemberStore>> = const { RefCell::new(None) };
}
pub(crate) fn with_lazy_member_store<'a>(fut: LocalBoxFuture<'a, ()>) -> LocalBoxFuture<'a, ()> {
Box::pin(
async move { LAZY_MEMBERS.async_scope(&mut Some(LazyMemberStore::default()), fut).await },
)
pub fn mk_lazy_tree_cx() -> ContextLayer {
ContextLayer::from_thread_local(Some(LazyMemberStore::default()), &LAZY_MEMBERS)
}
fn add_lazy(cx: &impl TreeIntoApiCtx, fac: LazyMemberFactory) -> api::TreeId {
+2
View File
@@ -10,6 +10,7 @@ async-event = "0.2.1"
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
async-once-cell = "0.5.4"
bound = "0.6.0"
checklings = { version = "0.1.0", path = "../checklings" }
chrono = "0.4.45"
derive_destructure = "1.0.0"
futures = { version = "0.3.32", features = ["std"], default-features = false }
@@ -28,6 +29,7 @@ orchid-base = { version = "0.1.0", path = "../orchid-base", default-features = f
orchid-extension = { version = "0.1.0", path = "../orchid-extension", optional = true }
ordered-float = "5.3.0"
pastey = "0.2.3"
scoped-log = { version = "0.1.0", path = "../scoped-log" }
substack = "1.1.1"
tokio = { version = "1.52.3", features = ["process"], optional = true }
tokio-util = { version = "0.7.18", features = ["compat"], optional = true }
+3 -2
View File
@@ -13,8 +13,9 @@ use futures::stream::select;
use futures::{FutureExt, SinkExt, StreamExt};
use never::Never;
use orchid_async_utils::{LocalSet, LocalSetController, local_set};
use orchid_base::{OrcErrv, Receipt, ReqHandle, Sym, fmt, is, log, mk_errv, sym};
use orchid_base::{OrcErrv, Receipt, ReqHandle, Sym, fmt, is, mk_errv, sym};
use orchid_extension::{self as ox, AtomicFeatures as _, get_arg};
use scoped_log::log;
use crate::cx::Cx;
use crate::execute::{ExecCx, ExecResult};
@@ -168,7 +169,7 @@ impl CmdRunner {
let delta = until - Utc::now();
match delta.to_std() {
Err(_) =>
writeln!(log("debug"), "Negative sleep found ({delta}), requeuing as instant").await,
writeln!(log("debug"), "Negative sleep found ({delta}), requeuing as instant"),
Ok(delay) => cx.sleep(delay).await,
};
queue.push(Task::RunCommand(expr));
+3 -1
View File
@@ -16,6 +16,7 @@ use crate::tree::WeakRoot;
pub struct CxData {
spawner: Rc<dyn SpawnSrv>,
pub checkers_url: Option<String>,
pub systems: RwLock<HashMap<api::SysId, WeakSystem>>,
pub system_id: RefCell<NonZeroU16>,
pub exprs: ExprStore,
@@ -38,9 +39,10 @@ impl WeakCx {
}
impl Cx {
#[must_use]
pub fn new(spawner: Rc<dyn SpawnSrv>, logger: LoggerImpl) -> Self {
pub fn new(spawner: Rc<dyn SpawnSrv>, logger: LoggerImpl, checkers_url: Option<String>) -> Self {
Self(Rc::new(CxData {
spawner,
checkers_url,
systems: RwLock::default(),
system_id: RefCell::new(NonZero::new(1).unwrap()),
exprs: ExprStore::default(),
+3 -2
View File
@@ -9,7 +9,8 @@ use futures::io::BufReader;
use futures::{AsyncBufReadExt, StreamExt};
use hashbrown::HashMap;
use libloading::{Library, Symbol};
use orchid_base::{SpawnExt, future_to_vt, log, on_drop, vt_to_future};
use orchid_base::{SpawnExt, future_to_vt, on_drop, vt_to_future};
use scoped_log::log;
use unsync_pipe::pipe;
use crate::api;
@@ -39,7 +40,7 @@ pub async fn ext_dylib(path: &Path, cx: Cx) -> Result<ExtPort, libloading::Error
let mut lines = BufReader::new(read_log).lines();
while let Some(line) = lines.next().await {
match line {
Ok(line) => writeln!(log("stderr"), "dylib {log_path} err> {line}").await,
Ok(line) => writeln!(log("stderr"), "dylib {log_path} err> {line}"),
Err(e) => match e.kind() {
io::ErrorKind::BrokenPipe | io::ErrorKind::UnexpectedEof => break,
_ => panic!("Error while reading stderr {e}"),
+3 -2
View File
@@ -3,7 +3,8 @@ use std::mem;
use bound::Bound;
use futures::FutureExt;
use futures_locks::{RwLockWriteGuard, TryLockError};
use orchid_base::{OrcErrv, Pos, fmt, log};
use orchid_base::{OrcErrv, Pos, fmt};
use scoped_log::log;
use crate::expr::{Expr, ExprKind, PathSet, Step};
use crate::tree::Root;
@@ -72,7 +73,7 @@ impl ExecCx {
while self.use_gas(1) {
let mut kind_swap = ExprKind::Missing;
mem::swap(&mut kind_swap, &mut self.cur);
writeln!(log("debug"), "Exxecute lvl{} {}", self.stack.len(), fmt(&kind_swap).await).await;
writeln!(log("debug"), "Exxecute lvl{} {}", self.stack.len(), fmt(&kind_swap).await);
let (kind, op) = match kind_swap {
ExprKind::Identity(target) => {
let inner = self.unpack_ident(&target).await;
+10 -11
View File
@@ -19,8 +19,9 @@ use orchid_base::deadlock_probe::ThreadlockProbe;
use orchid_base::{
AtomRepr, Client, ClientExt, CommCx, FmtCxImpl, FmtUnit, Format, IStr, IStrv, IoComm,
MsgReaderExt, Pos, ReqHandleExt, ReqReaderExt, ReqWait, SpawnExt, Sym, TaskHandle, Witness, es,
ev, io_comm, is, iv, log, log_at, stash, take_first, with_stash,
ev, io_comm, is, iv, stash, take_first, with_stash,
};
use scoped_log::log;
use crate::api;
use crate::atom::AtomHand;
@@ -81,7 +82,9 @@ impl Drop for ExtensionData {
pub struct Extension(Rc<ExtensionData>);
impl Extension {
pub async fn new(mut init: ExtPort, cx: Cx) -> io::Result<Self> {
api::HostHeader { logger: cx.logger.to_api() }.encode(init.input.as_mut()).await.unwrap();
let host_header =
api::HostHeader { logger: cx.logger.to_api(), checkers_url: cx.checkers_url.clone() };
host_header.encode(init.input.as_mut()).await.unwrap();
init.input.flush().await.unwrap();
let header = api::ExtensionHeader::decode(init.output.as_mut()).await.unwrap();
@@ -114,7 +117,7 @@ impl Extension {
select! {
_ = ping_req.join().fuse() => (),
_ = sleep_future => {
writeln!(log("futures"), "Channel blocked").await;
writeln!(log("futures"), "Channel blocked");
break;
},
};
@@ -129,7 +132,7 @@ impl Extension {
let notif = reader.read::<api::ExtHostNotif>().await.unwrap();
// logging is never logged because its value will be logged anyway
if !matches!(notif, api::ExtHostNotif::Log(_)) {
writeln!(log("msg"), "Host received notif {notif:?}").await;
writeln!(log("msg"), "Host received notif {notif:?}");
}
match notif {
api::ExtHostNotif::ExprNotif(api::ExprNotif::Acquire(acq)) => {
@@ -139,12 +142,8 @@ impl Extension {
api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => {
this.0.cx.exprs.take_expr(rel.1);
},
api::ExtHostNotif::Log(api::Log { category, message, path }) =>
write!(
log_at(&es(category).await.expect("Log category stale"), path),
"{message}"
)
.await,
api::ExtHostNotif::Log(api::Log { category, message }) =>
write!(log(&es(category).await.expect("Log category stale")), "{message}"),
api::ExtHostNotif::Sweeped(data) => {
for i in
join_all(data.strings.into_iter().map(async |s| {
@@ -177,7 +176,7 @@ impl Extension {
if !matches!(req, api::ExtHostReq::ExtAtomPrint(_))
&& !matches!(req, api::ExtHostReq::IntReq(_))
{
writeln!(log("msg"), "Host received request {req:?}").await;
writeln!(log("msg"), "Host received request {req:?}");
}
let deadlock = writeln!(ThreadlockProbe, "Host blocked on {req:?}");
let this = Self(weak.upgrade().unwrap());
+3 -2
View File
@@ -2,8 +2,9 @@ use std::io;
use futures::io::BufReader;
use futures::{AsyncBufReadExt, StreamExt};
use orchid_base::{SpawnExt, log, on_drop};
use orchid_base::{SpawnExt, on_drop};
use orchid_extension as ox;
use scoped_log::log;
use unsync_pipe::pipe;
use crate::cx::Cx;
@@ -21,7 +22,7 @@ pub async fn ext_inline(builder: ox::ExtensionBuilder, cx: Cx) -> ExtPort {
let mut lines = BufReader::new(out_stderr).lines();
while let Some(line) = lines.next().await {
match line {
Ok(line) => writeln!(log("stderr"), "inline {name} err> {line}").await,
Ok(line) => writeln!(log("stderr"), "inline {name} err> {line}"),
Err(e) => match e.kind() {
io::ErrorKind::BrokenPipe | io::ErrorKind::UnexpectedEof => break,
_ => panic!("Error while reading stderr {e}"),
+25 -29
View File
@@ -1,37 +1,32 @@
use std::fmt::{Arguments, Display};
use std::fmt::Arguments;
use std::fs::File;
use std::io::{Write, stderr};
use std::rc::Rc;
use futures::future::LocalBoxFuture;
use hashbrown::HashMap;
use itertools::Itertools;
use orchid_async_utils::debug::label;
use orchid_base::{LogWriter, Logger};
use scoped_log::{LogWriter, Logger};
use crate::api;
pub struct LogWriterImpl {
logger: api::LogStrategy,
label: Rc<dyn Display>,
}
impl LogWriter for LogWriterImpl {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) -> LocalBoxFuture<'a, ()> {
Box::pin(async move {
match &self.logger {
api::LogStrategy::Discard => (),
api::LogStrategy::Default => {
write!(stderr(), "{} {fmt}", self.label).expect("Could not write to stderr");
stderr().flush().expect("Could not flush stderr")
},
api::LogStrategy::File { path, .. } => {
let mut file = (File::options().write(true).create(true).truncate(false).open(path))
.unwrap_or_else(|e| panic!("Could not open {path}: {e}"));
write!(file, "{} {fmt}", self.label)
.unwrap_or_else(|e| panic!("Could not write to {path}: {e}"));
},
}
})
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) {
match &self.logger {
api::LogStrategy::Discard => (),
orchid_api::LogStrategy::Checkers => checklings::event(fmt.to_string()),
api::LogStrategy::Default => {
write!(stderr(), "{fmt}").expect("Could not write to stderr");
stderr().flush().expect("Could not flush stderr")
},
api::LogStrategy::File { path, .. } => {
let mut file = (File::options().write(true).create(true).truncate(false).open(path))
.unwrap_or_else(|e| panic!("Could not open {path}: {e}"));
write!(file, "{fmt}").unwrap_or_else(|e| panic!("Could not write to {path}: {e}"));
},
}
}
}
@@ -68,22 +63,23 @@ impl LoggerImpl {
self
}
pub async fn log(&self, category: &str, msg: impl AsRef<str>) {
writeln!(self.writer_at(category, Rc::new(label())), "{}", msg.as_ref()).await
writeln!(self.writer(category), "{}", msg.as_ref())
}
pub fn has_category(&self, category: &str) -> bool { self.routing.contains_key(category) }
pub async fn log_buf(&self, category: &str, event: impl AsRef<str>, buf: &[u8]) {
if std::env::var("ORCHID_LOG_BUFFERS").is_ok_and(|v| !v.is_empty()) {
let data = buf.iter().map(|b| format!("{b:02x}")).join(" ");
writeln!(self.writer_at(category, Rc::new(label())), "{}: [{data}]", event.as_ref()).await
writeln!(self.writer(category), "{}: [{data}]", event.as_ref())
}
}
}
impl Logger for LoggerImpl {
fn writer_at(&self, category: &str, label: Rc<dyn Display>) -> Rc<dyn LogWriter> {
Rc::new(LogWriterImpl { logger: self.strat(category).clone(), label })
}
fn strat(&self, category: &str) -> api::LogStrategy {
pub fn strat(&self, category: &str) -> api::LogStrategy {
(self.routing.get(category).cloned().or(self.default.clone()))
.expect("Invalid category and catchall logger not set")
}
}
impl Logger for LoggerImpl {
fn writer(&self, category: &str) -> Rc<dyn LogWriter> {
Rc::new(LogWriterImpl { logger: self.strat(category).clone() })
}
fn is_active(&self, category: &str) -> bool { self.strat(category) != api::LogStrategy::Discard }
}
+3 -2
View File
@@ -1,6 +1,7 @@
use futures::io::BufReader;
use futures::{self, AsyncBufReadExt, StreamExt};
use orchid_base::{SpawnExt, log};
use orchid_base::SpawnExt;
use scoped_log::log;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::cx::Cx;
@@ -22,7 +23,7 @@ pub async fn ext_command(cmd: std::process::Command, cx: Cx) -> std::io::Result<
let mut lines = BufReader::new(child_stderr.compat()).lines();
while let Some(line) = lines.next().await {
match line {
Ok(line) => writeln!(log("stderr"), "subproc {name} err> {line}").await,
Ok(line) => writeln!(log("stderr"), "subproc {name} err> {line}"),
Err(e) => match e.kind() {
std::io::ErrorKind::BrokenPipe | std::io::ErrorKind::UnexpectedEof => break,
_ => panic!("Error while reading stderr {e}"),
+1
View File
@@ -32,6 +32,7 @@ orchid-extension = { version = "0.1.0", path = "../orchid-extension", features =
ordered-float = "5.3.0"
pastey = "0.2.3"
rust_decimal = "1.42.1"
scoped-log = { version = "0.1.0", path = "../scoped-log" }
subslice-offset = "0.1.1"
substack = "1.1.1"
tokio = { version = "1.52.3", features = ["full"] }
+4 -4
View File
@@ -3,9 +3,10 @@ use std::ops::{Add, Range};
use futures::FutureExt;
use hashbrown::{HashMap, HashSet};
use itertools::Itertools;
use orchid_base::{NameLike, Paren, Pos, VPath, fmt, is, log, mk_errv, report};
use orchid_base::{NameLike, Paren, Pos, VPath, fmt, is, mk_errv, report};
use orchid_extension::gen_expr::{call_v, new_atom};
use orchid_extension::{ExecHandle, ReflMemKind, TAtom, refl};
use scoped_log::log;
use subslice_offset::SubsliceOffset;
use crate::macros::macro_value::{Macro, Rule};
@@ -14,7 +15,7 @@ use crate::macros::rule::state::{MatchState, StateEntry};
use crate::{MacTok, MacTree};
pub async fn resolve(h: &mut ExecHandle<'_>, val: MacTree) -> MacTree {
writeln!(log("debug"), "Macro-resolving {}", fmt(&val).await).await;
writeln!(log("debug"), "Macro-resolving {}", fmt(&val).await);
let root = refl();
let mut macros = HashMap::new();
for n in val.glossary() {
@@ -52,8 +53,7 @@ pub async fn resolve(h: &mut ExecHandle<'_>, val: MacTree) -> MacTree {
}
let mut rctx = ResolveCtx { exclusive, priod, h: &mut *h };
let gex = resolve_one(&mut rctx, &val).await.unwrap_or(val.clone());
writeln!(log("debug"), "Macro-resolution over {} yielded {}", fmt(&val).await, fmt(&gex).await)
.await;
writeln!(log("debug"), "Macro-resolution over {} yielded {}", fmt(&val).await, fmt(&gex).await);
gex
}
-27
View File
@@ -51,32 +51,6 @@
"initCommands": [
"settings set target.disable-aslr false"
]
},
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"requireExactSource": true,
"preLaunchTask": "Build All",
"environment": [
{
"name": "ORCHID_EXTENSIONS",
"value": "target/debug/orchid_std"
},
{
"name": "ORCHID_DEFAULT_SYSTEMS",
"value": "orchid::std;orchid::macros"
},
],
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/target/debug/orcx.exe",
"args": [
"--logs=stderr",
"--logs=debug>stderr",
"--logs=msg>stderr",
"exec",
"1 + 1"
]
}
],
"compounds": []
@@ -150,7 +124,6 @@
"recommendations": [
"fill-labs.dependi",
"gruntfuggly.todo-tree",
"maptz.regionfolder",
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"vadimcn.vscode-lldb",
+2
View File
@@ -9,6 +9,7 @@ authors = ["Lawrence Bethlenfalvy <lbfalvy@protonmail.com>"]
[dependencies]
async-fn-stream = { version = "0.1.0", path = "../async-fn-stream" }
camino = "1.2.4"
checklings = { version = "0.1.0", path = "../checklings", features = ["telemetry"] }
clap = { version = "4.6.1", features = ["derive", "env", "cargo"] }
ctrlc = "3.5.2"
futures = "0.3.32"
@@ -22,6 +23,7 @@ orchid-host = { version = "0.1.0", path = "../orchid-host", features = [
"tokio",
"orchid-extension",
] }
scoped-log = { version = "0.1.0", path = "../scoped-log" }
stacker = "0.1.24"
substack = "1.1.1"
tokio = { version = "1.52.3", features = ["full"] }
+23 -13
View File
@@ -1,13 +1,14 @@
use checklings::{Role, init_telemetry, section};
use never::Never;
use orchid_async_utils::debug::with_label;
use orchid_base::deadlock_probe::init_spinlock_probe;
use orchid_base::tokio::TokioSpawn;
use orchid_base::{IStr, Logger, Receipt, ReqHandle};
use orchid_base::{IStr, Receipt, ReqHandle};
use orchid_extension::{self as ox, OrcReader, OrcWriter};
use orchid_host::cmd_system::{CmdEvent, CmdRunner};
use orchid_host::dylib::ext_dylib;
use orchid_host::inline::ext_inline;
use orchid_host::tree::Root;
use scoped_log::{log, with_logger};
use tokio::time::Instant;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
pub mod parse_folder;
@@ -30,8 +31,8 @@ use futures::{Stream, TryStreamExt, io};
use itertools::Itertools;
use orchid_base::local_interner::local_interner;
use orchid_base::{
FmtCxImpl, Snippet, SrcRange, Token, VPath, fmt, fmt_v, is, log, sym, take_first,
try_with_reporter, ttv_fmt, with_interner, with_logger, with_reporter, with_stash,
FmtCxImpl, Snippet, SrcRange, Token, VPath, fmt, fmt_v, is, sym, take_first, try_with_reporter,
ttv_fmt, with_interner, with_reporter, with_stash,
};
use orchid_host::cx::Cx;
use orchid_host::execute::{ExecCx, ExecResult};
@@ -105,6 +106,9 @@ pub struct Args {
verbatim_doc_comment
)]
log: Vec<String>,
/// API URL for checkers config
#[arg(long, env = "CHECKERS_API_URL")]
checkers: Option<String>,
/// Measure and report the timings of various events
#[arg(long, action)]
time: bool,
@@ -165,7 +169,7 @@ pub enum Commands {
/// Execute effectful Orchid code
Exec {
/// Entrypoint or startup command
#[arg(long)]
#[arg(long, default_value = "src::main::main")]
main: String,
},
}
@@ -231,6 +235,8 @@ fn parse_log_dest(dest: &str) -> orchid_api::LogStrategy {
orchid_api::LogStrategy::Discard
} else if dest == "stderr" {
orchid_api::LogStrategy::Default
} else if dest == "checkers" {
orchid_api::LogStrategy::Checkers
} else if let Some(path) = dest.strip_prefix('>') {
orchid_api::LogStrategy::File { path: path.to_string(), append: true }
} else {
@@ -331,7 +337,7 @@ async fn add_const_at(
) -> Result<(), String> {
let prefix_sr = SrcRange::zw(pcx.src(), 0);
let mut lexemes = lex(value.clone(), pcx).await.map_err(|e| e.to_string())?;
writeln!(log("debug"), "lexed: {}", fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" ")).await;
writeln!(log("debug"), "lexed: {}", fmt_v::<ParsTokTree>(lexemes.iter()).await.join(" "));
let prefix = [is!("export"), is!("let"), name, is!("=")];
lexemes.splice(0..0, prefix.map(|n| Token::Name(n).at(prefix_sr.clone())));
let snippet = Snippet::new(&lexemes[0], &lexemes);
@@ -346,9 +352,13 @@ async fn add_const_at(
fn main() -> io::Result<ExitCode> {
// Use a 10MB stack for single-threaded, unoptimized operation
stacker::grow(10 * 1024 * 1024, || {
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(
with_label("host", async {
let args = Args::parse();
let args = Args::parse();
if let Some(checkers_url) = &args.checkers {
init_telemetry(checkers_url, "orchid", Role::Leader);
}
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(section(
"host",
async {
eprintln!("Orcx v{} is free software provided without warranty.", clap::crate_version!());
let exit_code = Rc::new(RefCell::new(ExitCode::SUCCESS));
let local_set = LocalSet::new();
@@ -357,12 +367,12 @@ fn main() -> io::Result<ExitCode> {
let logger2 = logger.clone();
unsafe { STARTUP = Some(Instant::now()) };
let spawner = Rc::new(TokioSpawn::new());
let cx = Cx::new(spawner.clone(), logger2);
let cx = Cx::new(spawner.clone(), logger2, args.checkers.clone());
let (signal_end_main, on_end_main) = futures::channel::oneshot::channel();
let cx1 = cx.clone();
local_set.spawn_local(async move {
let cx = &cx1;
init_spinlock_probe(cx.spawn(), "Orcx main");
// init_spinlock_probe(cx.spawn(), "Orcx main");
let res = with_stash(async move {
let mut extensions =
get_all_extensions(&args, cx).try_collect::<Vec<Extension>>().await.unwrap();
@@ -519,7 +529,7 @@ fn main() -> io::Result<ExitCode> {
.await;
let x = *exit_code.borrow();
Ok(x)
}),
)
},
))
})
}
+3 -2
View File
@@ -2,7 +2,7 @@ use std::io::Write;
use itertools::Itertools;
use orchid_base::{
FmtCxImpl, FmtTTV, Format, Import, NameLike, Snippet, SrcRange, Token, VPath, fmt, is, log, sym,
FmtCxImpl, FmtTTV, Format, Import, NameLike, Snippet, SrcRange, Token, VPath, fmt, is, sym,
take_first, try_with_reporter, with_reporter,
};
use orchid_host::cx::Cx;
@@ -13,6 +13,7 @@ use orchid_host::lex::lex;
use orchid_host::parse::{mk_parse_cx, parse_item};
use orchid_host::parsed::{Item, ItemKind, ParsTokTree, ParsedMember, ParsedModule};
use orchid_host::system::init_systems;
use scoped_log::log;
use substack::Substack;
use tokio::io::{AsyncBufReadExt, BufReader, stdin};
@@ -60,7 +61,7 @@ pub async fn repl(args: &Args, extensions: &[Extension], cx: Cx) -> Result<(), S
let pcx = mk_parse_cx(&cx, path.clone(), systems.iter().cloned());
let mut lexemes = lex(is(prompt.trim()).await, &pcx).await.unwrap();
let Some(discr) = lexemes.first() else { continue };
writeln!(log("debug"), "lexed: {}", fmt(&FmtTTV(&lexemes)).await).await;
writeln!(log("debug"), "lexed: {}", fmt(&FmtTTV(&lexemes)).await);
let prefix_sr = SrcRange::zw(path.clone(), 0);
let process_lexemes = async |lexemes: &[ParsTokTree]| {
let snippet = Snippet::new(&lexemes[0], lexemes);
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "scoped-log"
version = "0.1.0"
edition = "2024"
authors.workspace = true
repository.workspace = true
[dependencies]
scoped-value = { version = "1.0.0", path = "../scoped-value" }
+71
View File
@@ -0,0 +1,71 @@
use std::any::Any;
use std::cell::RefCell;
use std::fmt::Arguments;
use std::rc::Rc;
use scoped_value::{ScopeExt, WithRef};
/// A first argument to [write!] and [writeln!] that causes them to return a
/// future.
pub trait LogWriter {
fn write_fmt(&self, fmt: Arguments);
}
pub struct NullLogger;
impl LogWriter for NullLogger {
fn write_fmt(&self, _: Arguments) {}
}
impl Logger for NullLogger {
fn is_active(&self, _category: &str) -> bool { false }
fn writer(&self, _category: &str) -> Rc<dyn LogWriter> { Rc::new(Self) }
}
/// Injectable logging service passed to [with_logger]
pub trait Logger: Any {
/// Obtain a writer that processes the message according to the given category
fn writer(&self, category: &str) -> Rc<dyn LogWriter>;
/// Determine whether a certain category would get processed at all. This is a
/// shortcut
fn is_active(&self, category: &str) -> bool;
}
thread_local! {
pub static LOGGER: RefCell<Option<Rc<dyn Logger>>> = const { RefCell::new(None)};
}
/// Within the future passed to this function the freestanding [log] and
/// [get_logger] functions can be used
pub async fn with_logger<F: Future>(logger: impl Logger + 'static, fut: F) -> F::Output {
LOGGER.async_scope(&mut Some(Rc::new(logger)), fut).await
}
/// Obtain an async log writer
///
/// ```
/// use orchid_base::log;
/// async {
/// let user = "lorentz";
/// writeln!(log("info"), "Hello from {user}").await
/// };
/// ```
pub fn log(category: &str) -> Rc<dyn LogWriter> { get_logger().writer(category) }
/// Obtain a reference to the current [Logger]. This is mostly useful for
/// [Logger::is_active]-based optimizations
pub fn get_logger() -> Rc<dyn Logger> { LOGGER.get().expect("Logger not set!") }
#[derive(Clone)]
pub struct TestLogger(Rc<dyn Fn(String)>);
impl LogWriter for TestLogger {
fn write_fmt<'a>(&'a self, fmt: Arguments<'a>) { (self.0)(fmt.to_string()) }
}
impl Logger for TestLogger {
fn is_active(&self, _category: &str) -> bool { true }
fn writer(&self, _category: &str) -> std::rc::Rc<dyn LogWriter> { Rc::new(self.clone()) }
}
impl TestLogger {
pub fn new(f: impl Fn(String) + 'static) -> Self { Self(Rc::new(f)) }
}
impl Default for TestLogger {
fn default() -> Self { TestLogger::new(|s| eprint!("{s}")) }
}
+85 -74
View File
@@ -33,11 +33,38 @@
//! [std::thread::LocalKey], but you can implement it for your own types via the
//! traits [WithRef] and [WithMut]
use std::cell::RefCell;
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::thread::LocalKey;
/// A modifier applied whenever a future has control, whether in poll or in
/// drop.
pub trait Decorator {
/// Decorate a code block
fn run<R>(&mut self, inner: impl FnOnce() -> R) -> R;
/// Apply the decorator to a future
fn wrap<F: Future>(self, fut: F) -> impl Future<Output = F::Output>
where Self: Sized {
DecoratedFuture { decorator: self, inner: Some(fut) }
}
}
struct DecoratedFuture<F, D: Decorator> {
inner: Option<F>,
decorator: D,
}
impl<F: Future, D: Decorator> Future for DecoratedFuture<F, D> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let Self { inner, decorator } = unsafe { Pin::get_unchecked_mut(self) };
let inner = inner.as_mut().expect("This is only unset on drop");
decorator.run(|| unsafe { Pin::new_unchecked(inner) }.poll(cx))
}
}
impl<F, D: Decorator> Drop for DecoratedFuture<F, D> {
fn drop(&mut self) { self.decorator.run(|| std::mem::drop(self.inner.take())) }
}
/// Borrow a type from another with a stackframe in between to hold any guards.
/// The lifetime is lifted out specifically to support an implementation for
/// [LocalKey]
@@ -74,6 +101,9 @@ impl<T> WithMut<'static, T> for LocalKey<RefCell<T>> {
self.with_borrow_mut(|t| f(t))
}
}
pub trait WithBorrowMut<'a, T> {
fn with_borrow_mut<R>(&'a mut self, f: impl for<'b> FnOnce(&'b mut T) -> R) -> R;
}
/// Extension trait for swapping a value into a store just for the duration of a
/// call
@@ -82,6 +112,13 @@ pub trait ScopeExt<'a, T>: WithMut<'a, T> {
/// panic
fn sync_scope<R, F: FnOnce() -> R>(&'a self, val: &mut T, f: F) -> R {
self.with_mut(|v| std::mem::swap(v, val));
struct ScopeGuard<'a, 'b, W: WithMut<'a, T> + ?Sized, T> {
local: &'a W,
alt: &'b mut T,
}
impl<'a, W: WithMut<'a, T> + ?Sized, T> Drop for ScopeGuard<'a, '_, W, T> {
fn drop(&mut self) { self.local.with_mut(|v| std::mem::swap(v, &mut *self.alt)) }
}
let drop_trigger = ScopeGuard { local: self, alt: val };
let ret = f();
std::mem::drop(drop_trigger);
@@ -94,97 +131,71 @@ pub trait ScopeExt<'a, T>: WithMut<'a, T> {
value: &mut T,
inner: Fut,
) -> impl Future<Output = Fut::Output> {
ScopeFuture { key: self, value, inner }
struct ScopeDecorator<'a, 'b, W: WithMut<'a, T> + ?Sized, T> {
key: &'a W,
value: &'b mut T,
}
impl<'a, W: WithMut<'a, T> + ?Sized, T> Decorator for ScopeDecorator<'a, '_, W, T> {
fn run<R>(&mut self, inner: impl FnOnce() -> R) -> R {
self.key.sync_scope(self.value, inner)
}
}
DecoratedFuture { inner: Some(inner), decorator: ScopeDecorator { key: self, value } }
}
}
impl<'a, W: WithMut<'a, T> + ?Sized, T> ScopeExt<'a, T> for W {}
struct ScopeGuard<'a, 'b, W: WithMut<'a, T> + ?Sized, T> {
local: &'a W,
alt: &'b mut T,
}
impl<'a, W: WithMut<'a, T> + ?Sized, T> Drop for ScopeGuard<'a, '_, W, T> {
fn drop(&mut self) { self.local.with_mut(|v| std::mem::swap(v, &mut *self.alt)) }
}
struct ScopeFuture<'a, 'b, W: ?Sized, T, F> {
/// The key this future controls. Multiple nested futures can control the same
/// key.
key: &'a W,
/// - When the future isn't running, this is the value this future promotes.
/// - When the future is running, this is the value the enclosing future
/// promoted
value: &'b mut T,
/// The nested future
inner: F,
}
impl<'a, W: WithMut<'a, T> + ?Sized, T, F: Future> Future for ScopeFuture<'a, '_, W, T, F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
this.key.sync_scope(this.value, || unsafe { Pin::new_unchecked(&mut this.inner) }.poll(cx))
}
}
/// Extension trait for pushing a value onto a shared vec just for the duration
/// of a call
pub trait StackExt<'a, T: 'a>: WithMut<'a, Vec<T>> {
/// Push the given value onto the stack, then pop it back off on return or
/// Push the given values onto the stack, then pop them back off on return or
/// panic
fn sync_frame<R, F: FnOnce() -> R>(&'a self, val: T, f: F) -> (R, T) {
self.with_mut(|v| v.push(val));
let drop_trigger = StackGuard { local: Some(self), _phantom: PhantomData };
let ret = f();
let val = drop_trigger.pop();
(ret, val)
fn sync_frame<R, F: FnOnce() -> R>(&'a self, values: &mut [Option<T>], f: F) -> R {
self.with_mut(|v| {
for slot in values.iter_mut() {
v.push(slot.take().expect("No stack frame provided"))
}
});
struct StackGuard<'a, 'b, W: WithMut<'a, Vec<T>> + ?Sized, T> {
local: Option<&'a W>,
values: &'b mut [Option<T>],
}
impl<'a, W: WithMut<'a, Vec<T>> + ?Sized, T> Drop for StackGuard<'a, '_, W, T> {
fn drop(&mut self) {
self.local.take().unwrap().with_mut(|v| {
for slot in self.values.iter_mut().rev() {
assert!(
slot.replace(v.pop().unwrap()).is_none(),
"This array should have been emptied on call"
);
}
})
}
}
let _drop_trigger = StackGuard { local: Some(self), values };
f()
}
/// Swap a value into this store each time the returned future is polled, and
/// swap it back out when the poll returns
fn async_frame<Fut: Future>(
&'a self,
value: T,
values: &mut [Option<T>],
inner: Fut,
) -> impl Future<Output = (Fut::Output, T)> {
StackFuture { key: self, value: Some(value), inner }
) -> impl Future<Output = Fut::Output> {
struct StackDecorator<'a, 'b, W: StackExt<'a, T> + ?Sized, T: 'a> {
key: &'a W,
values: &'b mut [Option<T>],
}
impl<'a, W: StackExt<'a, T> + ?Sized, T: 'a> Decorator for StackDecorator<'a, '_, W, T> {
fn run<R>(&mut self, inner: impl FnOnce() -> R) -> R {
self.key.sync_frame(self.values, inner)
}
}
DecoratedFuture { inner: Some(inner), decorator: StackDecorator { key: self, values } }
}
}
impl<'a, W: WithMut<'a, Vec<T>> + ?Sized, T: 'a> StackExt<'a, T> for W {}
struct StackGuard<'a, W: WithMut<'a, Vec<T>> + ?Sized, T> {
local: Option<&'a W>,
_phantom: PhantomData<&'a T>,
}
impl<'a, W: WithMut<'a, Vec<T>> + ?Sized, T> StackGuard<'a, W, T> {
fn pop(mut self) -> T { self.local.take().unwrap().with_mut(|v| v.pop().unwrap()) }
}
impl<'a, W: WithMut<'a, Vec<T>> + ?Sized, T> Drop for StackGuard<'a, W, T> {
fn drop(&mut self) {
if let Some(w) = self.local {
w.with_mut(|v| v.pop());
}
}
}
struct StackFuture<'a, W: StackExt<'a, T> + ?Sized, T: 'a, F> {
key: &'a W,
value: Option<T>,
inner: F,
}
impl<'a, W: StackExt<'a, T> + ?Sized, T: 'a, F: Future> Future for StackFuture<'a, W, T, F> {
type Output = (F::Output, T);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let Self { inner, key, value } = unsafe { Pin::get_unchecked_mut(self) };
let inner = unsafe { Pin::new_unchecked(inner) };
let init_val = value.take().expect("polled after Ready");
let (poll, new_val) = key.sync_frame(init_val, || inner.poll(cx));
if let Poll::Ready(out) = poll {
return Poll::Ready((out, new_val));
}
*value = Some(new_val);
Poll::Pending
}
}
#[cfg(test)]
mod test {
use std::panic::{AssertUnwindSafe, catch_unwind, panic_any};
+1
View File
@@ -18,3 +18,4 @@ test_executors = "0.4.1"
orchid-async-utils = { version = "0.1.0", path = "../orchid-async-utils" }
futures-io = "0.3.32"
itertools = "0.15.0"
scoped-log = { version = "0.1.0", path = "../scoped-log" }
+11 -23
View File
@@ -13,7 +13,7 @@ use std::{io, mem};
use futures_io::{AsyncRead, AsyncWrite};
use itertools::Itertools;
use orchid_async_utils::debug::{Label, scoped_logs};
use scoped_log::log;
fn pipe_layout(bs: usize) -> Layout { Layout::from_size_align(bs, 1).expect("1-align is trivial") }
@@ -115,21 +115,19 @@ struct AsyncRingbuffer {
}
impl AsyncRingbuffer {
fn wake_reader(&mut self) {
if cfg!(debug_assertions) && scoped_logs() && !self.read_waker.is_empty() {
eprintln!("{Label} Waking reader");
if !self.read_waker.is_empty() {
writeln!(log("unsync-pipe"), "Waking reader");
}
self.read_waker.invoke();
}
fn wake_writer(&mut self) {
if cfg!(debug_assertions) && scoped_logs() && !self.write_waker.is_empty() {
eprintln!("{Label} Waking writer");
if !self.write_waker.is_empty() {
writeln!(log("unsync-pipe"), "Waking writer");
}
self.write_waker.invoke()
}
fn drop_writer(&mut self) {
if cfg!(debug_assertions) && scoped_logs() {
eprintln!("{Label} dropping writer");
}
writeln!(log("unsync-pipe"), "dropping writer");
self.wake_reader();
self.writer_dropped = true;
if self.reader_dropped {
@@ -137,9 +135,7 @@ impl AsyncRingbuffer {
}
}
fn drop_reader(&mut self) {
if cfg!(debug_assertions) && scoped_logs() {
eprintln!("{Label} dropping reader")
}
writeln!(log("unsync-pipe"), "dropping reader");
self.wake_writer();
self.reader_dropped = true;
if self.writer_dropped {
@@ -158,17 +154,13 @@ impl AsyncRingbuffer {
),
));
}
if cfg!(debug_assertions) && scoped_logs() {
eprintln!("{Label} writer blocked")
}
writeln!(log("unsync-pipe"), "writer blocked");
self.write_waker.drop();
self.write_waker = Trigger::new(waker.clone());
Ok(())
}
fn reader_wait(&mut self, waker: &Waker) {
if cfg!(debug_assertions) && scoped_logs() {
eprintln!("{Label} reader blocked")
}
writeln!(log("unsync-pipe"), "reader blocked");
self.read_waker.drop();
self.read_waker = Trigger::new(waker.clone());
}
@@ -223,9 +215,7 @@ impl AsyncRingbuffer {
end.len() + start_count
}
};
if cfg!(debug_assertions) && scoped_logs() {
eprintln!("{Label} written {count} bytes, {} total", self.size - self.buf_free())
}
writeln!(log("unsync-pipe"), "written {count} bytes, {} total", self.size - self.buf_free());
count
}
/// Read as many bytes as possible into the buffer, and return how many bytes
@@ -249,9 +239,7 @@ impl AsyncRingbuffer {
unsafe { self.non_wrapping_read_unchecked(&mut start[0..start_count]) };
end.len() + start_count
};
if cfg!(debug_assertions) && scoped_logs() {
eprintln!("{Label} read {count} bytes, {} remaining", self.size - self.buf_free())
}
writeln!(log("unsync-pipe"), "read {count} bytes, {} remaining", self.size - self.buf_free());
count
}
}