Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/rustgis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ fn run(s: &mut Session, id: &str, p: Value) -> Result<Value, String> {
eprintln!();
}
TOLD_TENTH.store(usize::MAX, Ordering::Relaxed);
// A failure is noted too; it's printed once, by the caller.
for n in s.notices_since(mark).iter().filter(|n| r.as_ref().err() != Some(&n.text)) {
// A failure is noted too, redacted; it's printed once, by the caller.
let told = r.as_ref().err().map(|e| rustgis_engine::data::diagnostics::redact(e));
for n in s.notices_since(mark).iter().filter(|n| r.as_ref().err() != Some(&n.text) && told.as_ref() != Some(&n.text)) {
eprintln!("rustgis-cli: {}{}", if n.error { "error: " } else { "" }, n.text);
}
r.map_err(|e| format!("{id}: {e}"))
Expand Down
80 changes: 69 additions & 11 deletions apps/rustgis-web/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;

use rustgis_ui_egui::rustgis_engine::data::diagnostics::Capture;
use rustgis_ui_egui::{Inbox, RustGisApp, Services, Slot};

use crate::parts::Notes;
Expand Down Expand Up @@ -563,7 +564,19 @@ async fn unless_stalled(

/// Downloads `url`, or the byte range it asks for (a PMTiles archive's part: see
/// `rustgis_tiles::cache::split_range`).
async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
/// A status in `omit` is an expected answer (a tile past a map's edge): its failure makes no
/// record of its own, as on the desktop.
async fn fetch_bytes(url: &str, capture: Capture, omit: &'static [u16]) -> Result<Vec<u8>, String> {
use rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::{cache::RequestDiagnostics, failure::Failure};
let mut diagnostics = RequestDiagnostics::with_capture(url, "GET", capture);
diagnostics.omit_statuses(omit);
fetch_bytes_inner(url, &mut diagnostics).await.inspect_err(|error| diagnostics.failed(&Failure::new(url, error)))
Comment thread
deniial00 marked this conversation as resolved.
Resolved
}

async fn fetch_bytes_inner(
url: &str,
diagnostics: &mut rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::RequestDiagnostics<'_>,
) -> Result<Vec<u8>, String> {
let err = |e: wasm_bindgen::JsValue| format!("{e:?}");
let window = web_sys::window().ok_or("no window")?;
use rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache;
Expand Down Expand Up @@ -621,7 +634,10 @@ async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
// say why, so the likely reasons are named, with the browser's own message but not its stack.
let resp: web_sys::Response = unless_stalled(promise, abort.as_ref())
.await
.ok_or_else(stalled)?
.ok_or_else(|| {
diagnostics.timed_out();
stalled()
})?
.map_err(|e| {
let why = e.dyn_ref::<js_sys::Error>().map(|e| String::from(e.message())).unwrap_or_else(|| err(e));
format!(
Expand All @@ -630,6 +646,7 @@ async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
})?
.dyn_into()
.map_err(err)?;
diagnostics.answered(resp.status());
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
}
Expand Down Expand Up @@ -667,7 +684,10 @@ async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
loop {
// Headers came and then the body stalled: given up as a fetch that doesn't answer.
// (Aborting the fetch has cancelled its body too.)
let Some(chunk) = unless_stalled(reader.read(), abort.as_ref()).await else { return Err(stalled()) };
let Some(chunk) = unless_stalled(reader.read(), abort.as_ref()).await else {
diagnostics.timed_out();
return Err(stalled());
};
let chunk = chunk.map_err(err)?;
if js_sys::Reflect::get(&chunk, &"done".into()).ok().and_then(|d| d.as_bool()).unwrap_or(true) {
break;
Expand Down Expand Up @@ -699,7 +719,17 @@ async fn fetch_bytes(url: &str) -> Result<Vec<u8>, String> {

/// POSTs a JSON document and reads the answer (at most the tile cache's `MAX_BYTES`), without
/// following redirects; an error status is `HTTP <status>`.
async fn post_json(url: &str, body: &str) -> Result<Vec<u8>, String> {
async fn post_json(url: &str, body: &str, capture: Capture) -> Result<Vec<u8>, String> {
use rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::{cache::RequestDiagnostics, failure::Failure};
let mut diagnostics = RequestDiagnostics::with_capture(url, "POST", capture);
post_json_inner(url, body, &mut diagnostics).await.inspect_err(|error| diagnostics.failed(&Failure::new(url, error)))
}

async fn post_json_inner(
url: &str,
body: &str,
diagnostics: &mut rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::RequestDiagnostics<'_>,
) -> Result<Vec<u8>, String> {
use rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache;
let err = |e: wasm_bindgen::JsValue| format!("{e:?}");
let window = web_sys::window().ok_or("no window")?;
Expand All @@ -716,6 +746,7 @@ async fn post_json(url: &str, body: &str) -> Result<Vec<u8>, String> {
.map_err(|_| "the browser couldn't reach it (offline, or the server doesn't accept requests from web pages)".to_string())?
.dyn_into()
.map_err(err)?;
diagnostics.answered(resp.status());
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
}
Expand All @@ -730,7 +761,16 @@ async fn post_json(url: &str, body: &str) -> Result<Vec<u8>, String> {
/// Asks for an address's headers (`HEAD`): a TIFF header's announced length and the type. The
/// browser shows a page `X-TIFF-Header-Length` only when the server exposes it
/// (`Access-Control-Expose-Headers`); without it, the header is read as any other file's.
async fn head(url: &str) -> Result<rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::Head, String> {
async fn head(url: &str, capture: Capture) -> Result<rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::Head, String> {
use rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::{cache::RequestDiagnostics, failure::Failure};
let mut diagnostics = RequestDiagnostics::with_capture(url, "HEAD", capture);
head_inner(url, &mut diagnostics).await.inspect_err(|error| diagnostics.failed(&Failure::new(url, error)))
}

async fn head_inner(
url: &str,
diagnostics: &mut rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::RequestDiagnostics<'_>,
) -> Result<rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::Head, String> {
use rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::{cache, s3};
let err = |e: wasm_bindgen::JsValue| format!("{e:?}");
let window = web_sys::window().ok_or("no window")?;
Expand All @@ -753,6 +793,7 @@ async fn head(url: &str) -> Result<rustgis_ui_egui::rustgis_engine::render::rust
.map_err(|_| "the browser couldn't reach it (offline, or the server doesn't accept requests from web pages)".to_string())?
.dyn_into()
.map_err(err)?;
diagnostics.answered(resp.status());
if !resp.ok() {
return Err(format!("HTTP {}", resp.status()));
}
Expand Down Expand Up @@ -835,8 +876,13 @@ fn clipboard() -> Option<web_sys::Clipboard> {
/// the app must not read what the user saved on the site.
fn services(inbox: Inbox, font_inbox: Inbox, notes: Notes, ctx: egui::Context, embedded: bool) -> Services {
// Map tiles come through the browser's fetch (the app repaints when they arrive).
rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::set_fetcher(|url, done| {
wasm_bindgen_futures::spawn_local(async move { done(fetch_bytes(&url).await) });
rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::set_fetcher_omitting(|url, done, omit| {
let capture = Capture::current();
wasm_bindgen_futures::spawn_local(async move {
let result = fetch_bytes(&url, capture.clone(), omit).await;
let _scope = capture.enter();
done(result);
});
});
// Their pictures are decoded by the browser, off the page's thread.
rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::set_image_decoder(|bytes, done| {
Expand All @@ -853,11 +899,21 @@ fn services(inbox: Inbox, font_inbox: Inbox, notes: Notes, ctx: egui::Context, e
});
// So are HEAD requests (a GeoTIFF made on request saying how long its header is).
rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::set_header(|url, done| {
wasm_bindgen_futures::spawn_local(async move { done(head(&url).await) });
let capture = Capture::current();
wasm_bindgen_futures::spawn_local(async move {
let result = head(&url, capture.clone()).await;
let _scope = capture.enter();
done(result);
});
});
// So are JSON documents POSTed (the NLDI's flowtrace).
rustgis_ui_egui::rustgis_engine::render::rustgis_tiles::cache::set_poster(|url, body, done| {
wasm_bindgen_futures::spawn_local(async move { done(post_json(&url, &body).await) });
let capture = Capture::current();
wasm_bindgen_futures::spawn_local(async move {
let result = post_json(&url, &body, capture.clone()).await;
let _scope = capture.enter();
done(result);
});
});
let open_inbox = inbox.clone();
let font_ctx = ctx.clone();
Expand Down Expand Up @@ -912,8 +968,9 @@ fn services(inbox: Inbox, font_inbox: Inbox, notes: Notes, ctx: egui::Context, e
recovery: None,
fetch_font: Some(Box::new(move |url: &str| {
let (url, inbox, ctx) = (url.to_string(), font_sink.clone(), font_ctx.clone());
let capture = Capture::current();
wasm_bindgen_futures::spawn_local(async move {
match fetch_bytes(&url).await {
match fetch_bytes(&url, capture, &[]).await {
Ok(bytes) => {
inbox.lock().unwrap_or_else(|e| e.into_inner()).push((url, bytes));
ctx.request_repaint();
Expand Down Expand Up @@ -1007,8 +1064,9 @@ fn services(inbox: Inbox, font_inbox: Inbox, notes: Notes, ctx: egui::Context, e
})),
fetch: Some(Box::new(move |url: &str, slot| {
let (url, ctx) = (url.to_string(), fetch_ctx.clone());
let capture = Capture::current();
wasm_bindgen_futures::spawn_local(async move {
let r = fetch_bytes(&url).await.map_err(|e| format!("{url}: {e}"));
let r = fetch_bytes(&url, capture, &[]).await.map_err(|e| format!("{url}: {e}"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
*slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(r);
ctx.request_repaint();
});
Expand Down
14 changes: 14 additions & 0 deletions crates/assistant/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,20 @@ fn tool_results_carry_notices() {
assert!(quiet.get("notices").is_none(), "{quiet}");
}

/// A failed command's error isn't listed again among its notices, even when the notice is the
/// error redacted (an address with a query).
#[test]
fn a_failed_commands_redacted_error_is_not_repeated_as_a_notice() {
let mut s = sample();
let call = ToolCall {
id: "x".into(),
name: "run_command".into(),
args: json!({"command": "layer.addData", "params": {"path": "/no/such/dir/data.geojson?token=hidden"}}),
};
let err = tools::run(&mut s, &call).unwrap_err();
assert_eq!(err.lines().count(), 1, "{err}");
}

/// map.activate / map.remove must not report the newly visible map's layers as layersAdded (#212).
#[test]
fn map_switch_and_remove_do_not_report_layers_added() {
Expand Down
7 changes: 5 additions & 2 deletions crates/assistant/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,9 @@ fn run_script(s: &mut Session, a: &Value, tokens: usize) -> Result<String, Strin
}
Err(e) => {
// Nothing was kept, so no line ran and no layer was added.
let said: Vec<&&str> = notices.iter().filter(|t| **t != e).collect();
// The notice of the failure is the error redacted: compared as such, it isn't repeated.
let told = rustgis_engine::data::diagnostics::redact(&e);
let said: Vec<&&str> = notices.iter().filter(|t| **t != e && **t != told).collect();
let out = json!({"success": false, "result": null, "lines": 0, "layersAdded": [], "notices": said,
"error": clip(&e, 8000), "duration_s": secs});
Err(bounded_json(&out, tokens))
Expand All @@ -494,7 +496,8 @@ fn with_notices(r: Value, s: &Session, mark: u64) -> Value {
/// A failed run's error with what the command said before failing (a GDAL layer that couldn't be
/// converted).
fn error_with_notices(e: String, s: &Session, mark: u64) -> String {
let said: Vec<&str> = s.notices_since(mark).iter().map(|n| n.text.as_str()).filter(|t| *t != e).collect();
let told = rustgis_engine::data::diagnostics::redact(&e);
let said: Vec<&str> = s.notices_since(mark).iter().map(|n| n.text.as_str()).filter(|t| *t != e && *t != told).collect();
// Preserve the original 8,000-character diagnostic assembly cap for MCP errors;
// the agent additionally applies its serialized output budget.
if said.is_empty() { e } else { clip(&format!("{e}\n{}", said.join("\n")), 8000) }
Expand Down
Loading
Loading