diff --git a/Cargo.toml b/Cargo.toml index 867b898..19e48eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [package] name = "sovereign_browser" -version = "0.0.13" +version = "0.0.14" edition = "2021" license = "MIT" description = "A Linux x86 web browser built on WebKitGTK with Nostr identity" diff --git a/VERSION b/VERSION index 43b2961..9789c4c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.13 +0.0.14 diff --git a/src/nostr_bridge.rs b/src/nostr_bridge.rs index a2895db..44aba72 100644 --- a/src/nostr_bridge.rs +++ b/src/nostr_bridge.rs @@ -111,8 +111,12 @@ fn handle_sovereign_scheme(request: &URISchemeRequest) { } // ── Processes REST API ───────────────────────────────────────── + // Pass `path` (with its query string): probe-report reads + // tab_index/body and tab_probe/tab_action read index/action from it. + // Passing `route` dropped every parameter, so no probe report was ever + // recorded and the Tabs view stayed blank. if route.starts_with("processes/") { - handle_processes_api(request, &route[10..]); + handle_processes_api(request, &path[10..]); return; } diff --git a/src/nostr_inject.rs b/src/nostr_inject.rs index 26c1e17..5580b04 100644 --- a/src/nostr_inject.rs +++ b/src/nostr_inject.rs @@ -229,15 +229,19 @@ fn add_scrollbar_visibility_fix(manager: &UserContentManager) { manager.add_style_sheet(&sheet); } -/// Perf probe preamble — skips sovereign:// and about: pages, sets tab index. +/// Perf probe preamble — skips sovereign:// pages, sets tab index. /// The tab index is baked in at injection time as a literal. +/// +/// Only `sovereign:` is skipped. The previous check also skipped `about:`, +/// but at document-start the location of a freshly created top-level +/// document can still be `about:blank`, which silently disabled the probe +/// for every real page. fn perf_probe_preamble(tab_index: i32) -> String { format!( r#"(function(){{ 'use strict'; try {{ - var u = location.href || ''; - if (u.indexOf('sovereign://') === 0 || u.indexOf('about:') === 0) {{ + if (location.protocol === 'sovereign:') {{ window.__sbPerfProbeSkip = true; }} else {{ window.__sbPerfProbeSkip = false; @@ -250,6 +254,28 @@ fn perf_probe_preamble(tab_index: i32) -> String { ) } +/// Isolated-world relay for perf probe reports. The probe (main world) +/// dispatches a `__sb_probe_report` DOM event carrying the JSON payload as a +/// string; this relay performs the synchronous XHR to +/// `sovereign://processes/probe-report`. Doing the XHR from an isolated world +/// exempts it from the page's Content-Security-Policy (connect-src), which +/// otherwise blocks the request on many sites. +const PERF_PROBE_RELAY_JS: &str = r#" +(function() { + 'use strict'; + document.addEventListener('__sb_probe_report', function(ev) { + try { + var d = JSON.parse(ev.detail); + var url = 'sovereign://processes/probe-report?tab_index=' + d.tab_index + + '&body=' + encodeURIComponent(ev.detail); + var x = new XMLHttpRequest(); + x.open('GET', url, false); + x.send(); + } catch (e) { /* probe must never break the page */ } + }); +})(); +"#; + /// The perf-probe.js script (embedded from www/js/perf-probe.js). const PERF_PROBE_JS: &str = include_str!("../www/js/perf-probe.js"); @@ -274,6 +300,16 @@ pub fn nostr_inject_create_content_manager_with_probe(tab_index: i32) -> UserCon // Top frame only — mirrors the C version (perf_probe.c:67). Injecting // into all frames would run a separate probe instance (with its own // rAF loop and 1s sync XHR) in every iframe. + let probe_relay = UserScript::for_world( + PERF_PROBE_RELAY_JS, + UserContentInjectedFrames::TopFrame, + UserScriptInjectionTime::Start, + BRIDGE_WORLD, + &[], + &[], + ); + manager.add_script(&probe_relay); + let preamble = UserScript::new( &perf_probe_preamble(tab_index), UserContentInjectedFrames::TopFrame, diff --git a/src/tab_manager.rs b/src/tab_manager.rs index de77672..a7bcc49 100644 --- a/src/tab_manager.rs +++ b/src/tab_manager.rs @@ -841,11 +841,13 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti return true; } - // Update tab URL in state for non-rewritten URLs. - let mut state = G_TAB_STATE.lock().unwrap(); - if let Some(t) = state.tabs.iter_mut().find(|t| t.id == tab_id_clone2) { - t.url = uri_str; - } + // NOTE: the tab's URL in state is intentionally NOT + // updated here. decide-policy also fires for + // sub-frame/iframe navigations (e.g. a PayPal + // button) and for not-yet-committed requests, which + // would clobber the real page URL. The main-frame + // URI is tracked via `notify::uri` below. + let _ = tab_id_clone2; } } } @@ -853,6 +855,31 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti true }); + // Track the main-frame URI: keep the tab state and the URL entry in + // sync with what the webview is actually showing (covers link clicks, + // redirects, history navigation, and Recents/Bookmarks menu loads). + let tab_id_uri = tab_id; + let url_entry_uri = url_entry.clone(); + webview.connect_uri_notify(move |wv| { + let Some(uri) = wv.uri() else { return }; + let uri_str = uri.to_string(); + if uri_str.is_empty() { + return; + } + { + let mut state = G_TAB_STATE.lock().unwrap(); + if let Some(t) = state.tabs.iter_mut().find(|t| t.id == tab_id_uri) { + t.url = uri_str.clone(); + } + } + if uri_str.starts_with("about:blank") { + url_entry_uri.set_text(""); + } else if url_entry_uri.text().as_str() != uri_str { + url_entry_uri.set_text(&uri_str); + url_entry_uri.set_position(-1); + } + }); + // Webview context menu — add "Open Link in New Tab" when right-clicking // a link, and "Open Page in New Tab" when right-clicking the page // background. Mirrors the C version (tab_manager.c:1912). @@ -1669,8 +1696,13 @@ pub fn tab_manager_reload_tab(tab_id: i32) { /// Duplicate a tab by ID (opens the same URL in a new tab in the same window). pub fn tab_manager_duplicate(tab_id: i32) { let state = G_TAB_STATE.lock().unwrap(); - let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone()); + let state_url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone()); drop(state); + // Prefer the webview's live main-frame URI over the stored one. + let url = tab_webview(tab_id) + .and_then(|wv| wv.uri().map(|u| u.to_string())) + .filter(|u| !u.is_empty()) + .or(state_url); // Duplicate into the tab's own notebook (its window). let nb = match tab_notebook(tab_id) { Some(n) => n, @@ -1692,7 +1724,13 @@ pub fn tab_manager_open_in_new_window(tab_id: i32) { // Capture the URL and the tab's own notebook before moving. let (url, nb) = { let state = G_TAB_STATE.lock().unwrap(); - let url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone()); + let state_url = state.tabs.iter().find(|t| t.id == tab_id).map(|t| t.url.clone()); + drop(state); + // Prefer the webview's live main-frame URI over the stored one. + let url = tab_webview(tab_id) + .and_then(|wv| wv.uri().map(|u| u.to_string())) + .filter(|u| !u.is_empty()) + .or(state_url); (url, tab_notebook(tab_id)) }; let nb = match nb { diff --git a/src/version.rs b/src/version.rs index 6913923..1b1daa9 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,7 +1,7 @@ //! Version information for sovereign_browser /// The current version of sovereign_browser (with leading 'v'). -pub const VERSION: &str = "v0.0.13"; +pub const VERSION: &str = "v0.0.14"; /// Major version number. pub const VERSION_MAJOR: u32 = 0; @@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0; pub const VERSION_MINOR: u32 = 0; /// Patch version number. -pub const VERSION_PATCH: u32 = 13; +pub const VERSION_PATCH: u32 = 14; diff --git a/www/js/perf-probe.js b/www/js/perf-probe.js index 2413c22..c6529b5 100644 --- a/www/js/perf-probe.js +++ b/www/js/perf-probe.js @@ -125,6 +125,8 @@ try { var resObs = new PerformanceObserver(function (list) { list.getEntries().forEach(function (e) { + // Ignore the probe's own reports and other browser-internal calls. + if (e.name.indexOf('sovereign://') === 0) return; netRequestsLastSec.push({ name: e.name, type: e.initiatorType }); var key = e.name.split('?')[0]; netEndpoints[key] = (netEndpoints[key] || 0) + 1; @@ -135,13 +137,42 @@ } // ── FPS via requestAnimationFrame ─────────────────────────────── + // The rAF loop is the probe's only continuous main-thread work. It is + // paused whenever the document is hidden (background tab / minimised + // window) so a backgrounded tab can never keep the compositor and font + // machinery busy. It is also guarded so callbacks can't stack. var rafFrames = 0; var fps = 0; - function rafLoop(ts) { + var rafScheduled = false; + var rafActive = false; + + function rafLoop() { + rafScheduled = false; + if (!rafActive) return; rafFrames++; + scheduleRaf(); + } + function scheduleRaf() { + if (rafScheduled || !rafActive) return; + rafScheduled = true; requestAnimationFrame(rafLoop); } - requestAnimationFrame(rafLoop); + function setRafActive(on) { + if (on === rafActive) return; + rafActive = on; + if (on) { + scheduleRaf(); + } else { + rafFrames = 0; + fps = 0; + } + } + rafActive = !document.hidden; + if (rafActive) scheduleRaf(); + + var onVisibility = function () { setRafActive(!document.hidden); }; + onVisibility.__sbInternal = true; // don't count our own listener + document.addEventListener('visibilitychange', onVisibility); // ── Timer tracking (patched setTimeout/setInterval) ───────────── var activeTimers = {}; // id -> {code, interval_ms, count} @@ -344,21 +375,21 @@ netEndpoints = {}; timerTop = {}; - // Send. Use sync XHR to sovereign:// (same convention as the - // window.nostr shim — WebKit's custom scheme handler doesn't - // reliably support fetch() or async XHR for sovereign:// URLs). + // Send. The actual sync XHR to sovereign:// runs in an isolated + // script world (see PERF_PROBE_RELAY_JS in nostr_inject.rs) so that + // pages with a strict CSP can't block it. We hand the payload over as + // a JSON string in a DOM CustomEvent (objects don't cross worlds). try { - var body = encodeURIComponent(JSON.stringify(payload)); - var url = 'sovereign://processes/probe-report?tab_index=' + TAB_INDEX + - '&body=' + body; - var x = new XMLHttpRequest(); - x.open('GET', url, false); // synchronous - x.send(); + document.dispatchEvent(new CustomEvent('__sb_probe_report', { + detail: JSON.stringify(payload) + })); } catch (e) { /* swallow — probe must never break the page */ } } - // FPS counter: count frames in each 1s window. - setInterval(function () { + // FPS counter: count frames in each 1s window. Use the original + // setInterval so the probe doesn't count itself as an active timer. + origSetInterval.call(window, function () { + if (!rafActive) return; fps = rafFrames; rafFrames = 0; }, 1000);