moving offline for travel
This commit is contained in:
1 parent
66c9d78ced
commit
a7bfe04e52
52 files changed
+2283
-713
No files matched your search
Generated
+1361
-120
File diff suppressed because it is too large.
Load diff
@@ -19,6 +19,7 @@ members = [
|
||||
"checklings",
|
||||
"checkers-web",
|
||||
"checkers-api",
|
||||
"scoped-log",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[application]
|
||||
|
||||
asset_dir = 'checkers-web/assets'
|
||||
sub_package = 'checkers-web'
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
@@ -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:?}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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() });
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
cargo build -p orchid-std
|
||||
cargo run -p orcx -- lex --file .\examples\hello-world\main.orc
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
@@ -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:?}");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
}
|
||||
@@ -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}"),
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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" }
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user