fix(servo): retire sessions on internal navigation

This commit is contained in:
2026-07-09 22:37:17 -04:00
parent b422ac1631
commit 31fc1acf17
9 changed files with 571 additions and 93 deletions
+13 -3
View File
@@ -1,9 +1,9 @@
use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use ely_domain::{BrowserTab, TabId};
use ely_domain::{BrowserTab, ProfileId, TabId};
use crate::services::ProfileDataMode;
use crate::services::{ProfileDataMode, servo_live::ServoLivePermissionGrant};
use super::{
web_surface_cadence::{ACTIVE_POLL_INTERVAL, IDLE_POLL_INTERVAL},
@@ -250,7 +250,7 @@ impl WebSurfaceStore {
.min(IDLE_POLL_INTERVAL)
}
pub(super) fn retain_tabs(&mut self, open_tab_ids: &[TabId]) {
pub(super) fn retain_tabs(&mut self, open_tab_ids: &[TabId]) -> Vec<ServoLivePermissionGrant> {
let stale_tab_ids = self
.surfaces
.keys()
@@ -260,6 +260,16 @@ impl WebSurfaceStore {
for tab_id in stale_tab_ids {
self.close_surface(&tab_id);
}
self.runtime.take_retired_permission_consumptions()
}
pub(super) fn reconcile_tab_scope(
&mut self,
tab_id: &TabId,
profile_id: &ProfileId,
profile_data_mode: ProfileDataMode,
) -> Vec<ServoLivePermissionGrant> {
self.runtime.reconcile_tab_scope(tab_id, profile_id, profile_data_mode)
}
fn take_pending_input(
@@ -1,7 +1,7 @@
use std::{collections::HashMap, sync::Arc};
use ely_browser_core::{BrowserCore, BrowserSnapshot};
use ely_domain::{BrowserTab, ProfileKind, TabId, UrlText};
use ely_domain::{BrowserTab, ProfileId, ProfileKind, TabId, UrlText};
use gpui::{AnyElement, Bounds, Context, Pixels, Point};
use crate::services::{ProfileDataMode, servo_live::ServoLivePermissionGrant};
@@ -49,15 +49,34 @@ impl ElyShell {
}
pub(super) fn tick_external_web_surfaces(&mut self, cx: &mut Context<Self>) -> bool {
let (visible_tab_ids, open_tab_ids, visible_tabs) = match &self.state {
let (visible_tab_ids, external_tab_ids, external_scopes, visible_tabs) = match &self.state {
super::ShellState::Ready(core) => {
let visible_tabs = core.visible_content_tabs().unwrap_or_else(|_| Vec::new());
let visible_tab_ids = visible_tabs.iter().map(|tab| tab.id().clone()).collect();
(visible_tab_ids, core.open_tab_ids(), visible_web_surface_tabs(core, visible_tabs))
let visible_tab_ids = external_web_surface_tab_ids(&visible_tabs);
let external_tab_ids = external_web_surface_tab_ids(core.open_tabs());
let external_scopes = external_web_surface_scopes(core);
(visible_tab_ids, external_tab_ids, external_scopes, visible_tabs)
}
super::ShellState::StartupError(_) => (Vec::new(), Vec::new(), Vec::new()),
super::ShellState::StartupError(_) => (Vec::new(), Vec::new(), Vec::new(), Vec::new()),
};
let mut retired_permissions = self.web_surfaces.retain_tabs(&external_tab_ids);
for (tab_id, profile_id, profile_data_mode) in external_scopes {
retired_permissions.extend(self.web_surfaces.reconcile_tab_scope(
&tab_id,
&profile_id,
profile_data_mode,
));
}
let mut permission_changed = false;
if let super::ShellState::Ready(core) = &mut self.state {
for consumed in retired_permissions {
permission_changed |= apply_permission_consumption(core, &consumed);
}
}
let visible_tabs = match &self.state {
super::ShellState::Ready(core) => visible_web_surface_tabs(core, visible_tabs),
super::ShellState::StartupError(_) => Vec::new(),
};
self.web_surfaces.retain_tabs(&open_tab_ids);
let mut url_changed = self.ensure_visible_web_surfaces(visible_tabs);
let result = self.web_surfaces.tick(&visible_tab_ids);
for url_change in result.url_changes {
@@ -67,7 +86,6 @@ impl ElyShell {
for metadata in result.page_metadata {
metadata_changed |= self.apply_web_surface_page_metadata(metadata);
}
let mut permission_changed = false;
if let super::ShellState::Ready(core) = &mut self.state {
for grant in result.permission_transfers {
permission_changed |= core
@@ -288,6 +306,25 @@ fn visible_web_surface_tabs(
visible
}
fn external_web_surface_tab_ids(tabs: &[BrowserTab]) -> Vec<TabId> {
tabs.iter()
.filter(|tab| super::web_surface::is_external_web_url(tab.url().as_str()))
.map(|tab| tab.id().clone())
.collect()
}
fn external_web_surface_scopes(core: &BrowserCore) -> Vec<(TabId, ProfileId, ProfileDataMode)> {
core.open_tabs()
.iter()
.filter(|tab| super::web_surface::is_external_web_url(tab.url().as_str()))
.filter_map(|tab| {
core.profile_kind_for(tab.profile_id()).ok().map(|kind| {
(tab.id().clone(), tab.profile_id().clone(), profile_data_mode_from_kind(kind))
})
})
.collect()
}
fn profile_data_mode_for(tab: &BrowserTab, snapshot: &BrowserSnapshot) -> Option<ProfileDataMode> {
snapshot
.profiles
@@ -327,34 +364,5 @@ fn apply_permission_consumption(
}
#[cfg(test)]
mod tests {
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{SiteOrigin, SitePermissionDecision, SitePermissionFeature};
use super::{ServoLivePermissionGrant, apply_permission_consumption};
#[test]
fn consumption_receipt_finishes_a_pending_allow_once_grant()
-> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let profile_id = core.snapshot()?.active_profile_id;
let origin = SiteOrigin::parse("https://example.com")?;
core.set_site_permission(
origin.clone(),
SitePermissionFeature::Camera,
SitePermissionDecision::AllowOnce,
)?;
let revision =
core.site_permission_revision(&profile_id, &origin, SitePermissionFeature::Camera);
let consumed = ServoLivePermissionGrant::new(
profile_id,
origin,
SitePermissionFeature::Camera,
revision,
);
assert!(apply_permission_consumption(&mut core, &consumed));
assert!(core.snapshot()?.site_permissions.is_empty());
Ok(())
}
}
#[path = "web_surface_controller_tests.rs"]
mod tests;
@@ -0,0 +1,141 @@
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText};
use crate::services::ProfileDataMode;
use super::{
ServoLivePermissionGrant, apply_permission_consumption, external_web_surface_scopes,
external_web_surface_tab_ids, visible_web_surface_tabs,
};
#[test]
fn consumption_receipt_finishes_a_pending_allow_once_grant()
-> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::private_window()?)?;
let profile_id = core.snapshot()?.active_profile_id;
let origin = SiteOrigin::parse("https://example.com")?;
core.set_site_permission(
origin.clone(),
SitePermissionFeature::Camera,
SitePermissionDecision::AllowOnce,
)?;
let revision =
core.site_permission_revision(&profile_id, &origin, SitePermissionFeature::Camera);
let consumed =
ServoLivePermissionGrant::new(profile_id, origin, SitePermissionFeature::Camera, revision);
assert!(apply_permission_consumption(&mut core, &consumed));
assert!(core.snapshot()?.site_permissions.is_empty());
Ok(())
}
#[test]
fn stale_consumption_keeps_a_newer_allow_once_grant() -> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let profile_id = core.snapshot()?.active_profile_id;
let origin = SiteOrigin::parse("https://example.com")?;
core.set_site_permission(
origin.clone(),
SitePermissionFeature::Camera,
SitePermissionDecision::AllowOnce,
)?;
let stale_revision =
core.site_permission_revision(&profile_id, &origin, SitePermissionFeature::Camera);
let stale = ServoLivePermissionGrant::new(
profile_id.clone(),
origin.clone(),
SitePermissionFeature::Camera,
stale_revision,
);
core.set_site_permission(
origin.clone(),
SitePermissionFeature::Camera,
SitePermissionDecision::DenyAlways,
)?;
core.set_site_permission(
origin,
SitePermissionFeature::Camera,
SitePermissionDecision::AllowOnce,
)?;
assert!(!apply_permission_consumption(&mut core, &stale));
assert_eq!(
core.site_permissions_for_profile(&profile_id)[0].decision(),
SitePermissionDecision::AllowOnce,
);
Ok(())
}
#[test]
fn retired_permissions_are_settled_before_the_next_external_snapshot()
-> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::private_window()?)?;
core.navigate_active_tab(UrlText::parse("https://example.com/a")?)?;
let profile_id = core.snapshot()?.active_profile_id;
let retiring_tab_id = core.snapshot()?.active_tab_id;
let origin = SiteOrigin::parse("https://example.com")?;
let mut retired = Vec::new();
for feature in [SitePermissionFeature::Camera, SitePermissionFeature::Microphone] {
core.set_site_permission(origin.clone(), feature, SitePermissionDecision::AllowOnce)?;
let revision = core.site_permission_revision(&profile_id, &origin, feature);
retired.push(ServoLivePermissionGrant::new(
profile_id.clone(),
origin.clone(),
feature,
revision,
));
}
assert!(core.transfer_site_permission_once(
&profile_id,
&origin,
SitePermissionFeature::Microphone,
retired[1].grant_revision(),
)?);
let visible_tab_id = core.open_tab(UrlText::parse("https://example.com/b")?);
core.navigate_tab_to_loaded_url(&retiring_tab_id, UrlText::parse("ely://settings/general")?)?;
let raw_visible_tabs = core.visible_content_tabs()?;
for consumed in &retired {
assert!(apply_permission_consumption(&mut core, consumed));
}
let visible_tabs = visible_web_surface_tabs(&core, raw_visible_tabs);
assert_eq!(visible_tabs.len(), 1);
assert_eq!(visible_tabs[0].tab.id(), &visible_tab_id);
assert!(visible_tabs[0].permissions.is_empty());
for consumed in &retired {
assert!(!apply_permission_consumption(&mut core, consumed));
}
Ok(())
}
#[test]
fn external_tab_ids_exclude_internal_routes() -> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let tab_id = core.snapshot()?.active_tab_id;
core.navigate_active_tab(UrlText::parse("https://example.com")?)?;
assert_eq!(external_web_surface_tab_ids(core.open_tabs()), vec![tab_id.clone()]);
core.navigate_active_tab(UrlText::parse("ely://settings/general")?)?;
let snapshot = core.snapshot()?;
assert_eq!(snapshot.active_tab_id, tab_id);
assert!(external_web_surface_tab_ids(core.open_tabs()).is_empty());
Ok(())
}
#[test]
fn external_tab_ids_include_inactive_spaces() -> Result<(), Box<dyn std::error::Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let profile_id = core.snapshot()?.active_profile_id;
let external_tab_id = core.snapshot()?.active_tab_id;
core.navigate_active_tab(UrlText::parse("https://example.com")?)?;
core.create_space("Second", "circle", 0x807d72)?;
assert!(external_web_surface_tab_ids(&core.snapshot()?.tabs).is_empty());
assert_eq!(external_web_surface_tab_ids(core.open_tabs()), vec![external_tab_id.clone()]);
assert_eq!(
external_web_surface_scopes(&core),
vec![(external_tab_id, profile_id, ProfileDataMode::Persistent,)]
);
Ok(())
}
@@ -71,15 +71,14 @@ fn successful_worker_ensure_confirms_allow_once_transfer() -> Result<(), String>
store.flush_runtime_for_test();
let result = store.tick(std::slice::from_ref(tab.id()));
assert_eq!(
result.permission_transfers,
vec![crate::services::servo_live::ServoLivePermissionGrant::new(
profile_id,
permission.origin().clone(),
permission.feature(),
permission.revision(),
)],
);
let transferred = vec![crate::services::servo_live::ServoLivePermissionGrant::new(
profile_id,
permission.origin().clone(),
permission.feature(),
permission.revision(),
)];
assert_eq!(result.permission_transfers, transferred);
assert_eq!(store.retain_tabs(&[]), transferred);
Ok(())
}
@@ -108,6 +107,42 @@ fn rejected_worker_ensure_keeps_allow_once_untransferred() -> Result<(), String>
Ok(())
}
#[test]
fn retiring_transient_worker_returns_only_submitted_allow_once_grants() -> Result<(), String> {
let runtime = WebSurfaceRuntime::new_with_client_factory(|_| Ok(Box::new(AcceptingClient)));
let mut store = WebSurfaceStore::new_with_runtime(runtime);
let (tab, permission) = tab_and_permission()?;
let second = BrowserTab::new(
TabId::new(),
SpaceId::new(),
tab.profile_id().clone(),
"Second",
UrlText::parse("https://example.com/second").map_err(|error| error.to_string())?,
);
let submitted = crate::services::servo_live::ServoLivePermissionGrant::new(
tab.profile_id().clone(),
permission.origin().clone(),
permission.feature(),
permission.revision(),
);
record_viewport(&mut store, &tab);
assert!(store.ensure_surface(
&tab,
ProfileDataMode::Transient,
std::slice::from_ref(&permission),
));
record_viewport(&mut store, &second);
assert!(store.ensure_surface(
&second,
ProfileDataMode::Transient,
std::slice::from_ref(&permission),
));
assert!(store.retain_tabs(std::slice::from_ref(second.id())).is_empty());
assert_eq!(store.retain_tabs(&[]), vec![submitted]);
Ok(())
}
fn tab_and_permission() -> Result<(BrowserTab, WebSurfaceSitePermission), String> {
let profile_id = ProfileId::new();
let tab = BrowserTab::new(
+14 -36
View File
@@ -8,7 +8,7 @@ use ely_domain::{BrowserTab, TabId};
use crate::services::{
ProfileDataMode,
servo_live::{ServoLiveEnsureRequest, ServoLiveSitePermission},
servo_live::{ServoLiveEnsureRequest, ServoLivePermissionGrant, ServoLiveSitePermission},
servo_profile_data::cleanup_stale_transient_profile_data_dirs,
};
@@ -39,6 +39,7 @@ pub(super) struct WebSurfaceRuntime {
sessions: BTreeMap<TabId, WebSurfaceSession>,
retry_state: BTreeMap<WebSurfaceRuntimeScope, ScopeRetryState>,
retired_workers: Vec<JoinHandle<Result<(), String>>>,
retired_permission_consumptions: Vec<ServoLivePermissionGrant>,
transient_cleanup_error: Option<String>,
client_factory: LiveRuntimeClientFactory,
last_generation: u64,
@@ -55,6 +56,7 @@ impl WebSurfaceRuntime {
sessions: BTreeMap::new(),
retry_state: BTreeMap::new(),
retired_workers: Vec::new(),
retired_permission_consumptions: Vec::new(),
transient_cleanup_error,
client_factory: new_servo_live_client,
last_generation: 0,
@@ -68,6 +70,7 @@ impl WebSurfaceRuntime {
sessions: BTreeMap::new(),
retry_state: BTreeMap::new(),
retired_workers: Vec::new(),
retired_permission_consumptions: Vec::new(),
transient_cleanup_error: None,
client_factory,
last_generation: 0,
@@ -136,9 +139,10 @@ impl WebSurfaceRuntime {
allow_once_grants: allow_once_grants(tab.profile_id(), permissions),
};
let Some(scoped) = self.workers.get(&scope) else {
let Some(scoped) = self.workers.get_mut(&scope) else {
return Err("Servo sidecar worker was created but is no longer registered".to_string());
};
scoped.track_allow_once_grants(&request.allow_once_grants);
scoped.worker.submit_ensure(generation, request);
if let Some(session) = self.sessions.get_mut(tab.id()) {
session.cadence.note_poll_submitted(submitted_at);
@@ -150,7 +154,7 @@ impl WebSurfaceRuntime {
pub(super) fn tick(&mut self, visible_tab_ids: &[TabId]) -> Vec<WebSurfaceRuntimeFrame> {
self.reap_retired_workers(false);
let mut frames = Vec::new();
let mut frames = self.take_retired_permission_frames();
let now = Instant::now();
let scopes = self.workers.keys().cloned().collect::<Vec<_>>();
let mut unavailable_scopes = Vec::new();
@@ -168,6 +172,7 @@ impl WebSurfaceRuntime {
for scope in unavailable_scopes {
self.invalidate_scope(&scope, now);
}
frames.extend(self.take_retired_permission_frames());
let poll_now = Instant::now();
for tab_id in visible_tab_ids {
@@ -209,19 +214,6 @@ impl WebSurfaceRuntime {
.min()
}
pub(super) fn close_tab(&mut self, tab_id: &TabId) {
let Some(session) = self.sessions.remove(tab_id) else {
return;
};
let has_remaining_session =
self.sessions.values().any(|candidate| candidate.scope == session.scope);
if session.scope.is_transient() && !has_remaining_session {
self.remove_worker(&session.scope);
} else if let Some(scoped) = self.workers.get(&session.scope) {
scoped.worker.submit_close(tab_id.as_str().to_string());
}
}
pub(super) fn has_session(
&self,
tab_id: &TabId,
@@ -277,28 +269,10 @@ impl WebSurfaceRuntime {
return Err(error);
}
};
self.workers.insert(scope, ScopedWorker { worker, transient_profile_data_dir });
self.workers.insert(scope, ScopedWorker::new(worker, transient_profile_data_dir));
Ok(())
}
fn detach_tab_from_previous_scope(&mut self, tab_id: &TabId, scope: &WebSurfaceRuntimeScope) {
let Some(previous_scope) = self.sessions.get(tab_id).map(|session| session.scope.clone())
else {
return;
};
if &previous_scope == scope {
return;
}
self.sessions.remove(tab_id);
let has_remaining_session =
self.sessions.values().any(|candidate| candidate.scope == previous_scope);
if previous_scope.is_transient() && !has_remaining_session {
self.remove_worker(&previous_scope);
} else if let Some(scoped) = self.workers.get(&previous_scope) {
scoped.worker.submit_close(tab_id.as_str().to_string());
}
}
fn collect_responses(
&mut self,
scope: &WebSurfaceRuntimeScope,
@@ -362,6 +336,9 @@ impl WebSurfaceRuntime {
frames.push(WebSurfaceRuntimeFrame::PermissionSnapshotAccepted(grant));
}
WorkerResponse::PermissionConsumed(consumed) => {
if let Some(scoped) = self.workers.get_mut(scope) {
scoped.mark_permission_consumed(&consumed);
}
frames.push(WebSurfaceRuntimeFrame::PermissionConsumed(consumed));
}
}
@@ -398,9 +375,10 @@ impl WebSurfaceRuntime {
}
fn remove_worker(&mut self, scope: &WebSurfaceRuntimeScope) {
let Some(scoped) = self.workers.remove(scope) else {
let Some(mut scoped) = self.workers.remove(scope) else {
return;
};
self.retire_scoped_worker_permissions(&mut scoped);
if scoped.transient_profile_data_dir.is_some() {
match std::thread::Builder::new()
.name("ely-servo-profile-cleanup".to_string())
@@ -1,8 +1,17 @@
use std::path::PathBuf;
use crate::services::{servo_live::ServoLiveClient, servo_profile_data::TransientProfileDataDir};
use ely_domain::{ProfileId, TabId};
use super::{LiveRuntimeClient, LiveRuntimeWorker};
use crate::services::{
ProfileDataMode,
servo_live::{ServoLiveClient, ServoLivePermissionGrant},
servo_profile_data::TransientProfileDataDir,
};
use super::{
LiveRuntimeClient, LiveRuntimeWorker, WebSurfaceRuntime, WebSurfaceRuntimeFrame,
WebSurfaceRuntimeScope,
};
pub(super) type LiveRuntimeClientFactory =
fn(PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String>;
@@ -18,10 +27,36 @@ pub(super) fn new_servo_live_client(
pub(super) struct ScopedWorker {
pub(super) worker: LiveRuntimeWorker,
pub(super) transient_profile_data_dir: Option<TransientProfileDataDir>,
allow_once_grants: Vec<ServoLivePermissionGrant>,
}
impl ScopedWorker {
pub(super) fn new(
worker: LiveRuntimeWorker,
transient_profile_data_dir: Option<TransientProfileDataDir>,
) -> Self {
Self { worker, transient_profile_data_dir, allow_once_grants: Vec::new() }
}
pub(super) fn track_allow_once_grants(&mut self, grants: &[ServoLivePermissionGrant]) {
for grant in grants {
if !self.allow_once_grants.contains(grant) {
self.allow_once_grants.push(grant.clone());
}
}
}
pub(super) fn mark_permission_consumed(&mut self, consumed: &ServoLivePermissionGrant) {
self.allow_once_grants.retain(|grant| grant != consumed);
}
fn take_allow_once_grants(&mut self) -> Vec<ServoLivePermissionGrant> {
std::mem::take(&mut self.allow_once_grants)
}
}
pub(super) fn shutdown_scoped_worker(scoped: ScopedWorker) -> Result<(), String> {
let ScopedWorker { worker, transient_profile_data_dir } = scoped;
let ScopedWorker { worker, transient_profile_data_dir, .. } = scoped;
drop(worker);
let Some(directory) = transient_profile_data_dir else {
return Ok(());
@@ -30,3 +65,72 @@ pub(super) fn shutdown_scoped_worker(scoped: ScopedWorker) -> Result<(), String>
.close()
.map_err(|error| format!("failed to remove transient Servo profile data: {error}"))
}
impl WebSurfaceRuntime {
pub(in crate::shell) fn reconcile_tab_scope(
&mut self,
tab_id: &TabId,
profile_id: &ProfileId,
profile_data_mode: ProfileDataMode,
) -> Vec<ServoLivePermissionGrant> {
let scope = WebSurfaceRuntimeScope::new(profile_id.clone(), profile_data_mode);
self.detach_tab_from_previous_scope(tab_id, &scope);
self.take_retired_permission_consumptions()
}
pub(in crate::shell) fn close_tab(&mut self, tab_id: &TabId) {
let Some(session) = self.sessions.remove(tab_id) else {
return;
};
let has_remaining_session =
self.sessions.values().any(|candidate| candidate.scope == session.scope);
if session.scope.is_transient() && !has_remaining_session {
self.remove_worker(&session.scope);
} else if let Some(scoped) = self.workers.get(&session.scope) {
scoped.worker.submit_close(tab_id.as_str().to_string());
}
}
pub(super) fn retire_scoped_worker_permissions(&mut self, scoped: &mut ScopedWorker) {
for grant in scoped.take_allow_once_grants() {
if !self.retired_permission_consumptions.contains(&grant) {
self.retired_permission_consumptions.push(grant);
}
}
}
pub(in crate::shell) fn take_retired_permission_consumptions(
&mut self,
) -> Vec<ServoLivePermissionGrant> {
std::mem::take(&mut self.retired_permission_consumptions)
}
pub(super) fn take_retired_permission_frames(&mut self) -> Vec<WebSurfaceRuntimeFrame> {
self.take_retired_permission_consumptions()
.into_iter()
.map(WebSurfaceRuntimeFrame::PermissionConsumed)
.collect()
}
pub(super) fn detach_tab_from_previous_scope(
&mut self,
tab_id: &TabId,
scope: &WebSurfaceRuntimeScope,
) {
let Some(previous_scope) = self.sessions.get(tab_id).map(|session| session.scope.clone())
else {
return;
};
if &previous_scope == scope {
return;
}
self.sessions.remove(tab_id);
let has_remaining_session =
self.sessions.values().any(|candidate| candidate.scope == previous_scope);
if previous_scope.is_transient() && !has_remaining_session {
self.remove_worker(&previous_scope);
} else if let Some(scoped) = self.workers.get(&previous_scope) {
scoped.worker.submit_close(tab_id.as_str().to_string());
}
}
}
@@ -4,16 +4,20 @@ use std::{
time::Instant,
};
use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText};
use ely_domain::{
BrowserTab, ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, SpaceId,
TabId, UrlText,
};
use crate::services::{
ProfileDataMode,
servo_live::{ServoLiveEnsureRequest, ServoLiveFrame},
servo_live::{ServoLiveEnsureRequest, ServoLiveFrame, ServoLivePermissionGrant},
};
use super::{
super::{
web_surface_geometry::{WebSurfaceScrollOffset, WebSurfaceSize},
web_surface_permissions::{WebSurfaceSitePermission, WebSurfaceSitePermissionState},
web_surface_state::WebSurfacePendingInput,
web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError, WorkerResponse},
},
@@ -107,6 +111,123 @@ fn late_frame_from_a_is_discarded_after_a_to_b() -> Result<(), String> {
Ok(())
}
#[test]
fn late_frame_is_discarded_after_tab_session_closes() -> Result<(), String> {
let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory);
let tab_id = TabId::new();
let profile_id = ProfileId::new();
let tab = web_tab(tab_id.clone(), profile_id.clone(), "https://example.com/external")?;
runtime.ensure_tab(&tab, surface_size(), ProfileDataMode::Transient, &[], pending_input())?;
let generation = current_generation(&runtime, &tab_id)?;
runtime.close_tab(&tab_id);
let mut frames = Vec::new();
runtime.collect_responses(
&scope(&profile_id),
vec![
WorkerResponse::Frame {
generation,
tab_id: tab_id.as_str().to_string(),
frame: live_frame(),
},
WorkerResponse::Failed {
generation,
tab_id: tab_id.as_str().to_string(),
message: "late failure".to_string(),
},
],
Instant::now(),
&mut frames,
);
assert!(!runtime.sessions.contains_key(&tab_id));
assert!(frames.is_empty());
Ok(())
}
#[test]
fn scope_change_returns_submitted_permission_grants_on_the_next_tick() -> Result<(), String> {
let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory);
let tab_id = TabId::new();
let profile_a = ProfileId::new();
let (permission, grant) = allow_once_permission(&profile_a)?;
let tab_a = web_tab(tab_id.clone(), profile_a, "https://example.com/a")?;
let tab_b = web_tab(tab_id, ProfileId::new(), "https://example.com/b")?;
runtime.ensure_tab(
&tab_a,
surface_size(),
ProfileDataMode::Transient,
std::slice::from_ref(&permission),
pending_input(),
)?;
runtime.ensure_tab(&tab_b, surface_size(), ProfileDataMode::Transient, &[], pending_input())?;
let frames = runtime.tick(&[]);
assert!(matches!(
frames.as_slice(),
[WebSurfaceRuntimeFrame::PermissionConsumed(consumed)] if consumed == &grant
));
Ok(())
}
#[test]
fn reconciliation_retires_same_profile_mode_before_replacement_ensure() -> Result<(), String> {
let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory);
let tab_id = TabId::new();
let profile_id = ProfileId::new();
let (permission, grant) = allow_once_permission(&profile_id)?;
let transient = web_tab(tab_id.clone(), profile_id.clone(), "https://example.com/private")?;
let persistent = web_tab(tab_id.clone(), profile_id.clone(), "https://example.com/standard")?;
runtime.ensure_tab(
&transient,
surface_size(),
ProfileDataMode::Transient,
std::slice::from_ref(&permission),
pending_input(),
)?;
let retired = runtime.reconcile_tab_scope(&tab_id, &profile_id, ProfileDataMode::Persistent);
assert_eq!(retired, vec![grant]);
runtime.ensure_tab(
&persistent,
surface_size(),
ProfileDataMode::Persistent,
&[],
pending_input(),
)?;
assert!(runtime.has_session(&tab_id, &profile_id, ProfileDataMode::Persistent));
Ok(())
}
#[test]
fn consumed_permission_is_removed_from_worker_tracking() -> Result<(), String> {
let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory);
let tab_id = TabId::new();
let profile_id = ProfileId::new();
let scope = scope(&profile_id);
let (permission, grant) = allow_once_permission(&profile_id)?;
let tab = web_tab(tab_id.clone(), profile_id, "https://example.com")?;
runtime.ensure_tab(
&tab,
surface_size(),
ProfileDataMode::Transient,
std::slice::from_ref(&permission),
pending_input(),
)?;
let mut frames = Vec::new();
runtime.collect_responses(
&scope,
vec![WorkerResponse::PermissionConsumed(grant)],
Instant::now(),
&mut frames,
);
runtime.close_tab(&tab_id);
assert!(runtime.take_retired_permission_consumptions().is_empty());
assert!(matches!(frames.as_slice(), [WebSurfaceRuntimeFrame::PermissionConsumed(_)]));
Ok(())
}
#[test]
fn generation_blocks_aba_frames_and_failures() -> Result<(), String> {
let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory);
@@ -321,3 +442,20 @@ fn pending_input() -> WebSurfacePendingInput {
fn live_frame() -> ServoLiveFrame {
ServoLiveFrame::for_test(1, 1, vec![16, 32, 64, 255])
}
fn allow_once_permission(
profile_id: &ProfileId,
) -> Result<(WebSurfaceSitePermission, ServoLivePermissionGrant), String> {
let origin = SiteOrigin::parse("https://example.com").map_err(|error| error.to_string())?;
let feature = SitePermissionFeature::Camera;
let revision = 7;
Ok((
WebSurfaceSitePermission::new(
origin.clone(),
feature,
WebSurfaceSitePermissionState::Decision(SitePermissionDecision::AllowOnce),
revision,
),
ServoLivePermissionGrant::new(profile_id.clone(), origin, feature, revision),
))
}
@@ -20,6 +20,7 @@ use super::super::{
use super::WebSurfaceStore;
static ENSURES: Mutex<Vec<RecordedEnsure>> = Mutex::new(Vec::new());
static CLOSES: Mutex<Vec<String>> = Mutex::new(Vec::new());
#[derive(Debug)]
struct RecordedEnsure {
@@ -28,6 +29,7 @@ struct RecordedEnsure {
}
struct RecordingClient;
struct ClosingClient;
impl LiveRuntimeClient for RecordingClient {
fn ensure(
@@ -58,12 +60,36 @@ impl LiveRuntimeClient for RecordingClient {
}
}
impl LiveRuntimeClient for ClosingClient {
fn ensure(
&mut self,
_request: ServoLiveEnsureRequest,
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Ok(None)
}
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
Ok(None)
}
fn close(&mut self, tab_id: String) -> Result<(), LiveRuntimeClientError> {
CLOSES.lock().map_err(|_| "close recorder lock was poisoned".to_string())?.push(tab_id);
Ok(())
}
}
fn recording_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
Ok(Box::new(RecordingClient))
}
fn closing_client_factory(
_config_dir: std::path::PathBuf,
) -> Result<Box<dyn LiveRuntimeClient>, String> {
Ok(Box::new(ClosingClient))
}
#[test]
fn profile_scope_change_clears_pixels_input_and_focus_before_ensure() -> Result<(), Box<dyn Error>>
{
@@ -121,6 +147,44 @@ fn profile_scope_change_clears_pixels_input_and_focus_before_ensure() -> Result<
Ok(())
}
#[test]
fn retaining_only_external_tabs_closes_an_internal_navigation_surface() -> Result<(), Box<dyn Error>>
{
CLOSES.lock().map_err(|_| "close recorder lock was poisoned")?.clear();
let runtime = WebSurfaceRuntime::new_with_client_factory(closing_client_factory);
let mut store = WebSurfaceStore::new_with_runtime(runtime);
let tab = BrowserTab::new(
TabId::new(),
SpaceId::new(),
ProfileId::new(),
"Web",
UrlText::parse("https://example.com")?,
);
assert_eq!(
store.record_viewport_size(tab.id(), viewport_bounds(), 1.0),
WebSurfaceInputOutcome::Applied,
);
assert!(store.ensure_surface(&tab, ProfileDataMode::Persistent, &[]));
store.flush_runtime_for_test();
assert_eq!(
store.record_click_point(tab.id(), tab.url().as_str(), point(px(20.0), px(20.0)), 1.0,),
WebSurfaceInputOutcome::Applied,
);
store.retain_tabs(&[]);
store.flush_runtime_for_test();
assert!(store.surface_for_test(tab.id()).is_none());
assert!(store.runtime.session_scope_for_test(tab.id()).is_none());
assert!(store.keyboard_focus.is_none());
assert_eq!(
CLOSES.lock().map_err(|_| "close recorder lock was poisoned")?.as_slice(),
[tab.id().as_str()],
);
Ok(())
}
fn viewport_bounds() -> Bounds<gpui::Pixels> {
Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)))
}
@@ -5,8 +5,8 @@ use super::BrowserCore;
use crate::CoreError;
impl BrowserCore {
pub fn open_tab_ids(&self) -> Vec<TabId> {
self.tabs.iter().map(|tab| tab.id().clone()).collect()
pub fn open_tabs(&self) -> &[BrowserTab] {
&self.tabs
}
pub fn visible_content_tab_ids(&self) -> Result<Vec<TabId>, CoreError> {