fix(permissions): make profile snapshots authoritative
This commit is contained in:
@@ -13,11 +13,14 @@ use std::collections::BTreeSet;
|
||||
mod iosurface_importer;
|
||||
#[path = "servo_live_ipc.rs"]
|
||||
mod ipc;
|
||||
#[path = "servo_live_permission_grant.rs"]
|
||||
mod permission_grant;
|
||||
#[path = "servo_live_types.rs"]
|
||||
mod types;
|
||||
#[path = "servo_live_wire.rs"]
|
||||
mod wire;
|
||||
|
||||
pub(crate) use permission_grant::ServoLivePermissionGrant;
|
||||
pub(crate) use types::{
|
||||
ServoLiveEnsureRequest, ServoLiveError, ServoLiveFrame, ServoLiveSitePermission,
|
||||
};
|
||||
@@ -53,6 +56,7 @@ pub(crate) struct ServoLiveClient {
|
||||
ipc: ServoLiveIpc,
|
||||
timeouts: SidecarTimeouts,
|
||||
active: bool,
|
||||
pending_permission_consumptions: Vec<ServoLivePermissionGrant>,
|
||||
#[cfg(target_os = "macos")]
|
||||
iosurface_cache: IOSurfaceCache,
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -115,6 +119,7 @@ impl ServoLiveClient {
|
||||
ipc: ServoLiveIpc::spawn(stdin, stdout),
|
||||
timeouts,
|
||||
active: true,
|
||||
pending_permission_consumptions: Vec::new(),
|
||||
#[cfg(target_os = "macos")]
|
||||
iosurface_cache: IOSurfaceCache::new(),
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -155,6 +160,7 @@ impl ServoLiveClient {
|
||||
hover_x: request.hover_x,
|
||||
hover_y: request.hover_y,
|
||||
typed_text: request.typed_text,
|
||||
site_permission_generation: request.site_permission_generation,
|
||||
site_permissions: request.site_permissions,
|
||||
ready_surface_ids,
|
||||
pending_surface_ids,
|
||||
@@ -184,6 +190,7 @@ impl ServoLiveClient {
|
||||
if reply.frame.is_some()
|
||||
|| reply.surface_handle.is_some()
|
||||
|| reply.current_surface_id.is_some()
|
||||
|| !reply.permission_consumptions.is_empty()
|
||||
{
|
||||
return Err(ServoLiveError::InvalidResponse {
|
||||
message: "handshake response contains a frame",
|
||||
@@ -197,6 +204,7 @@ impl ServoLiveClient {
|
||||
|
||||
fn request(&mut self, request: LiveRequest) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
||||
let reply = self.exchange(request, self.timeouts.request, "request")?;
|
||||
self.pending_permission_consumptions.extend(reply.permission_consumptions);
|
||||
if let Some(message) = reply.error {
|
||||
return Err(ServoLiveError::SidecarFailed { message });
|
||||
}
|
||||
@@ -224,6 +232,10 @@ impl ServoLiveClient {
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
pub(crate) fn take_permission_consumptions(&mut self) -> Vec<ServoLivePermissionGrant> {
|
||||
std::mem::take(&mut self.pending_permission_consumptions)
|
||||
}
|
||||
|
||||
fn exchange(
|
||||
&mut self,
|
||||
request: LiveRequest,
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
ServoLiveError, ServoLiveFrame,
|
||||
ServoLiveError, ServoLiveFrame, ServoLivePermissionGrant,
|
||||
wire::{
|
||||
LiveRequest, LiveResponse, LiveSurfaceHandle, MAX_FRAME_BYTE_COUNT, MAX_FRAME_DIMENSION,
|
||||
},
|
||||
@@ -26,6 +26,7 @@ pub(super) struct IpcReply {
|
||||
pub(super) frame: Option<ServoLiveFrame>,
|
||||
pub(super) surface_handle: Option<LiveSurfaceHandle>,
|
||||
pub(super) current_surface_id: Option<u64>,
|
||||
pub(super) permission_consumptions: Vec<ServoLivePermissionGrant>,
|
||||
}
|
||||
|
||||
struct IpcRequest {
|
||||
@@ -194,6 +195,11 @@ fn read_reply(stdout: &mut impl BufRead) -> Result<IpcReply, ServoLiveError> {
|
||||
frame: frame.transpose()?,
|
||||
surface_handle: response.surface_handle,
|
||||
current_surface_id: response.current_surface_id,
|
||||
permission_consumptions: response
|
||||
.permission_consumptions
|
||||
.into_iter()
|
||||
.map(ServoLivePermissionGrant::try_from)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -240,7 +246,7 @@ mod tests {
|
||||
#[test]
|
||||
fn reply_rejects_oversized_frame_before_readback_allocation() {
|
||||
let header = format!(
|
||||
"{{\"protocol_version\":2,\"error\":null,\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":{0},\"height\":{0},\"device_pixel_ratio\":1.0,\"css_viewport_width\":{0},\"css_viewport_height\":{0},\"rgba_byte_count\":1073741824,\"pixels_changed\":true}}}}\n",
|
||||
"{{\"protocol_version\":3,\"error\":null,\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":{0},\"height\":{0},\"device_pixel_ratio\":1.0,\"css_viewport_width\":{0},\"css_viewport_height\":{0},\"rgba_byte_count\":1073741824,\"pixels_changed\":true}}}}\n",
|
||||
MAX_FRAME_DIMENSION
|
||||
);
|
||||
let mut input = Cursor::new(header.into_bytes());
|
||||
@@ -251,11 +257,34 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_parses_permission_consumption_without_a_frame() -> Result<(), ServoLiveError> {
|
||||
let profile_id = ely_domain::ProfileId::new();
|
||||
let header = format!(
|
||||
"{{\"protocol_version\":3,\"error\":null,\"frame\":null,\"permission_consumptions\":[{{\"profile_id\":\"{}\",\"origin\":\"https://example.com\",\"feature\":\"camera\",\"grant_revision\":7}}]}}\n",
|
||||
profile_id.as_str(),
|
||||
);
|
||||
let mut input = Cursor::new(header.into_bytes());
|
||||
|
||||
let reply = read_reply(&mut input)?;
|
||||
|
||||
let [consumed] = reply.permission_consumptions.as_slice() else {
|
||||
return Err(ServoLiveError::InvalidResponse {
|
||||
message: "permission consumption was missing",
|
||||
});
|
||||
};
|
||||
assert_eq!(consumed.profile_id(), &profile_id);
|
||||
assert_eq!(consumed.origin().as_str(), "https://example.com");
|
||||
assert_eq!(consumed.feature(), ely_domain::SitePermissionFeature::Camera);
|
||||
assert_eq!(consumed.grant_revision(), 7);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn hardware_reply_uses_surface_without_rgba_allocation() -> Result<(), ServoLiveError> {
|
||||
let header = concat!(
|
||||
"{\"protocol_version\":2,\"error\":null,",
|
||||
"{\"protocol_version\":3,\"error\":null,",
|
||||
"\"surface_handle\":{\"mach_port_name\":91,\"surface_id\":7,\"width\":64,\"height\":48},",
|
||||
"\"current_surface_id\":7,",
|
||||
"\"frame\":{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",",
|
||||
@@ -307,7 +336,7 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn hardware_header(current_surface_id: u64, handle_width: u32, handle_height: u32) -> String {
|
||||
format!(
|
||||
"{{\"protocol_version\":2,\"error\":null,\"surface_handle\":{{\"mach_port_name\":91,\"surface_id\":7,\"width\":{handle_width},\"height\":{handle_height}}},\"current_surface_id\":{current_surface_id},\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":64,\"height\":48,\"device_pixel_ratio\":1.0,\"css_viewport_width\":64,\"css_viewport_height\":48,\"rgba_byte_count\":0,\"pixels_changed\":true}}}}\n"
|
||||
"{{\"protocol_version\":3,\"error\":null,\"surface_handle\":{{\"mach_port_name\":91,\"surface_id\":7,\"width\":{handle_width},\"height\":{handle_height}}},\"current_surface_id\":{current_surface_id},\"frame\":{{\"loaded_url\":null,\"title\":null,\"state\":\"complete\",\"width\":64,\"height\":48,\"device_pixel_ratio\":1.0,\"css_viewport_width\":64,\"css_viewport_height\":48,\"rgba_byte_count\":0,\"pixels_changed\":true}}}}\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
|
||||
|
||||
use super::wire::LivePermissionConsumption;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ServoLivePermissionGrant {
|
||||
profile_id: ProfileId,
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
grant_revision: u64,
|
||||
}
|
||||
|
||||
impl ServoLivePermissionGrant {
|
||||
pub(crate) fn new(
|
||||
profile_id: ProfileId,
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
grant_revision: u64,
|
||||
) -> Self {
|
||||
Self { profile_id, origin, feature, grant_revision }
|
||||
}
|
||||
|
||||
pub(crate) fn profile_id(&self) -> &ProfileId {
|
||||
&self.profile_id
|
||||
}
|
||||
|
||||
pub(crate) fn origin(&self) -> &SiteOrigin {
|
||||
&self.origin
|
||||
}
|
||||
|
||||
pub(crate) fn feature(&self) -> SitePermissionFeature {
|
||||
self.feature
|
||||
}
|
||||
|
||||
pub(crate) fn grant_revision(&self) -> u64 {
|
||||
self.grant_revision
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<LivePermissionConsumption> for ServoLivePermissionGrant {
|
||||
type Error = ely_domain::DomainError;
|
||||
|
||||
fn try_from(consumed: LivePermissionConsumption) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
profile_id: ProfileId::parse(consumed.profile_id)?,
|
||||
origin: SiteOrigin::parse(consumed.origin)?,
|
||||
feature: SitePermissionFeature::parse(&consumed.feature)?,
|
||||
grant_revision: consumed.grant_revision,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
use std::sync::Arc;
|
||||
use std::{collections::TryReserveError, io, path::PathBuf};
|
||||
|
||||
use ely_domain::SitePermissionDecision;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::wire::LiveFrameReport;
|
||||
use super::{ServoLivePermissionGrant, wire::LiveFrameReport};
|
||||
use crate::services::servo_sidecar_command::SidecarCommandError;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -34,23 +33,27 @@ pub(crate) struct ServoLiveEnsureRequest {
|
||||
pub(crate) hover_x: Option<u32>,
|
||||
pub(crate) hover_y: Option<u32>,
|
||||
pub(crate) typed_text: Option<String>,
|
||||
pub(crate) site_permission_generation: u64,
|
||||
pub(crate) site_permissions: Vec<ServoLiveSitePermission>,
|
||||
pub(crate) allow_once_grants: Vec<ServoLivePermissionGrant>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub(crate) struct ServoLiveSitePermission {
|
||||
pub(crate) origin: String,
|
||||
pub(crate) feature: String,
|
||||
pub(crate) decision: String,
|
||||
pub(crate) state: String,
|
||||
pub(crate) revision: u64,
|
||||
}
|
||||
|
||||
impl ServoLiveSitePermission {
|
||||
pub fn new(
|
||||
origin: impl Into<String>,
|
||||
feature: impl Into<String>,
|
||||
decision: SitePermissionDecision,
|
||||
state: impl Into<String>,
|
||||
revision: u64,
|
||||
) -> Self {
|
||||
Self { origin: origin.into(), feature: feature.into(), decision: decision.as_str().into() }
|
||||
Self { origin: origin.into(), feature: feature.into(), state: state.into(), revision }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +367,9 @@ pub(crate) enum ServoLiveError {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Domain(#[from] ely_domain::DomainError),
|
||||
|
||||
#[error(transparent)]
|
||||
SidecarCommand(#[from] SidecarCommandError),
|
||||
}
|
||||
@@ -380,7 +386,8 @@ impl ServoLiveError {
|
||||
| Self::InvalidFrameByteCount { .. }
|
||||
| Self::FrameAllocation { .. }
|
||||
| Self::InvalidResponse { .. }
|
||||
| Self::Json(_) => true,
|
||||
| Self::Json(_)
|
||||
| Self::Domain(_) => true,
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::IOSurfaceImportFailed { .. }
|
||||
| Self::IOSurfaceBackingFailed { .. }
|
||||
|
||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::ServoLiveSitePermission;
|
||||
|
||||
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 2;
|
||||
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3;
|
||||
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
|
||||
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
|
||||
|
||||
@@ -29,6 +29,7 @@ pub(super) enum LiveRequest {
|
||||
hover_x: Option<u32>,
|
||||
hover_y: Option<u32>,
|
||||
typed_text: Option<String>,
|
||||
site_permission_generation: u64,
|
||||
site_permissions: Vec<ServoLiveSitePermission>,
|
||||
ready_surface_ids: Vec<u64>,
|
||||
pending_surface_ids: Vec<u64>,
|
||||
@@ -54,6 +55,16 @@ pub(super) struct LiveResponse {
|
||||
pub(super) surface_handle: Option<LiveSurfaceHandle>,
|
||||
#[serde(default)]
|
||||
pub(super) current_surface_id: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub(super) permission_consumptions: Vec<LivePermissionConsumption>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct LivePermissionConsumption {
|
||||
pub(super) profile_id: String,
|
||||
pub(super) origin: String,
|
||||
pub(super) feature: String,
|
||||
pub(super) grant_revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
@@ -98,7 +109,7 @@ fn default_device_pixel_ratio() -> f32 {
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{LIVE_PROTOCOL_VERSION, LiveRequest};
|
||||
use super::{LIVE_PROTOCOL_VERSION, LiveRequest, ServoLiveSitePermission};
|
||||
|
||||
#[test]
|
||||
fn handshake_request_serializes_protocol_version() -> Result<(), serde_json::Error> {
|
||||
@@ -138,4 +149,21 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_permission_serializes_revision() -> Result<(), serde_json::Error> {
|
||||
let permission =
|
||||
ServoLiveSitePermission::new("https://example.com", "camera", "allow-once", 7);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(permission)?,
|
||||
json!({
|
||||
"origin": "https://example.com",
|
||||
"feature": "camera",
|
||||
"state": "allow-once",
|
||||
"revision": 7,
|
||||
}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,14 +97,11 @@ impl WebSurfaceStore {
|
||||
// which destroys and reallocates the framebuffer — the source of
|
||||
// the per-frame blank flash. The first ensure (when no prior
|
||||
// `last_ensure_key` is set) always fires so the page can load.
|
||||
let already_ensured =
|
||||
self.surfaces.get(tab.id()).is_some_and(|surface| surface.last_ensure_key.is_some());
|
||||
if already_ensured
|
||||
&& self
|
||||
.surfaces
|
||||
.get(tab.id())
|
||||
.is_some_and(|surface| surface.viewport_size_is_settling(Instant::now()))
|
||||
{
|
||||
let defer_resize = self
|
||||
.surfaces
|
||||
.get(tab.id())
|
||||
.is_some_and(|surface| surface.should_defer_resize(&ensure_key, Instant::now()));
|
||||
if defer_resize {
|
||||
return false;
|
||||
}
|
||||
let previous_frame =
|
||||
@@ -205,6 +202,7 @@ impl WebSurfaceStore {
|
||||
}
|
||||
}
|
||||
WebSurfaceRuntimeFrame::Failed { tab_id, message } => {
|
||||
self.surface_mut(&tab_id).last_ensure_key = None;
|
||||
let had_ready = matches!(
|
||||
self.surfaces.get(&tab_id).and_then(|surface| surface.state.as_ref()),
|
||||
Some(WebSurfaceState::Ready(_))
|
||||
@@ -221,6 +219,12 @@ impl WebSurfaceStore {
|
||||
self.surface_mut(&tab_id).state = Some(WebSurfaceState::Failed { message });
|
||||
result.changed = true;
|
||||
}
|
||||
WebSurfaceRuntimeFrame::PermissionSnapshotAccepted(grant) => {
|
||||
result.permission_transfers.push(grant);
|
||||
}
|
||||
WebSurfaceRuntimeFrame::PermissionConsumed(consumed) => {
|
||||
result.permission_consumptions.push(consumed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,3 +385,7 @@ mod web_surface_hardware_import_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "web_surface_scope_tests.rs"]
|
||||
mod web_surface_scope_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "web_surface_permission_lifecycle_tests.rs"]
|
||||
mod web_surface_permission_lifecycle_tests;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use ely_browser_core::{BrowserCore, BrowserSnapshot};
|
||||
use ely_domain::{BrowserTab, ProfileKind, TabId, UrlText};
|
||||
use gpui::{AnyElement, Bounds, Context, Pixels, Point};
|
||||
|
||||
use crate::services::ProfileDataMode;
|
||||
use crate::services::{ProfileDataMode, servo_live::ServoLivePermissionGrant};
|
||||
|
||||
use super::{
|
||||
ElyShell,
|
||||
@@ -65,11 +67,27 @@ 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
|
||||
.transfer_site_permission_once(
|
||||
grant.profile_id(),
|
||||
grant.origin(),
|
||||
grant.feature(),
|
||||
grant.grant_revision(),
|
||||
)
|
||||
.unwrap_or(false);
|
||||
}
|
||||
for consumed in result.permission_consumptions {
|
||||
permission_changed |= apply_permission_consumption(core, &consumed);
|
||||
}
|
||||
}
|
||||
let sync_changed = self.drain_sync_updates();
|
||||
if url_changed || metadata_changed {
|
||||
self.schedule_cloud_sync_upload(cx);
|
||||
}
|
||||
result.changed || url_changed || metadata_changed || sync_changed
|
||||
result.changed || url_changed || metadata_changed || permission_changed || sync_changed
|
||||
}
|
||||
|
||||
pub(super) fn external_web_surface_tick_delay(&self) -> std::time::Duration {
|
||||
@@ -191,7 +209,7 @@ impl ElyShell {
|
||||
changed |= self.web_surfaces.ensure_surface(
|
||||
&visible.tab,
|
||||
visible.profile_data_mode,
|
||||
&visible.permissions,
|
||||
visible.permissions.as_ref(),
|
||||
);
|
||||
}
|
||||
changed
|
||||
@@ -237,22 +255,37 @@ impl ElyShell {
|
||||
struct VisibleWebSurfaceTab {
|
||||
tab: BrowserTab,
|
||||
profile_data_mode: ProfileDataMode,
|
||||
permissions: Vec<WebSurfaceSitePermission>,
|
||||
permissions: Arc<[WebSurfaceSitePermission]>,
|
||||
}
|
||||
|
||||
fn visible_web_surface_tabs(
|
||||
core: &BrowserCore,
|
||||
tabs: Vec<BrowserTab>,
|
||||
) -> Vec<VisibleWebSurfaceTab> {
|
||||
tabs.into_iter()
|
||||
.filter(|tab| super::web_surface::is_external_web_url(tab.url().as_str()))
|
||||
.filter_map(|tab| {
|
||||
let profile_data_mode =
|
||||
core.profile_kind_for(tab.profile_id()).ok().map(profile_data_mode_from_kind)?;
|
||||
let permissions = web_surface_site_permissions_for_core_tab(core, &tab);
|
||||
Some(VisibleWebSurfaceTab { tab, profile_data_mode, permissions })
|
||||
})
|
||||
.collect()
|
||||
let mut permission_cache = HashMap::new();
|
||||
let mut visible = Vec::new();
|
||||
for tab in tabs {
|
||||
if !super::web_surface::is_external_web_url(tab.url().as_str()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(kind) = core.profile_kind_for(tab.profile_id()) else {
|
||||
continue;
|
||||
};
|
||||
let permissions = permission_cache
|
||||
.entry(tab.profile_id().clone())
|
||||
.or_insert_with(|| {
|
||||
Arc::<[WebSurfaceSitePermission]>::from(web_surface_site_permissions_for_core_tab(
|
||||
core, &tab,
|
||||
))
|
||||
})
|
||||
.clone();
|
||||
visible.push(VisibleWebSurfaceTab {
|
||||
tab,
|
||||
profile_data_mode: profile_data_mode_from_kind(kind),
|
||||
permissions,
|
||||
});
|
||||
}
|
||||
visible
|
||||
}
|
||||
|
||||
fn profile_data_mode_for(tab: &BrowserTab, snapshot: &BrowserSnapshot) -> Option<ProfileDataMode> {
|
||||
@@ -269,3 +302,59 @@ fn profile_data_mode_from_kind(kind: ProfileKind) -> ProfileDataMode {
|
||||
ProfileKind::Private => ProfileDataMode::Transient,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_permission_consumption(
|
||||
core: &mut BrowserCore,
|
||||
consumed: &ServoLivePermissionGrant,
|
||||
) -> bool {
|
||||
let transferred = core
|
||||
.transfer_site_permission_once(
|
||||
consumed.profile_id(),
|
||||
consumed.origin(),
|
||||
consumed.feature(),
|
||||
consumed.grant_revision(),
|
||||
)
|
||||
.unwrap_or(false);
|
||||
let finished = core
|
||||
.finish_site_permission_once(
|
||||
consumed.profile_id(),
|
||||
consumed.origin(),
|
||||
consumed.feature(),
|
||||
consumed.grant_revision(),
|
||||
)
|
||||
.unwrap_or(false);
|
||||
transferred || finished
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
use ely_domain::{
|
||||
BrowserTab, ProfileId, SiteOrigin, SitePermissionDecision, SitePermissionFeature, SpaceId,
|
||||
TabId, UrlText,
|
||||
};
|
||||
|
||||
use crate::services::{
|
||||
ProfileDataMode,
|
||||
servo_live::{ServoLiveEnsureRequest, ServoLiveFrame},
|
||||
};
|
||||
|
||||
use super::{WebSurfaceSitePermission, WebSurfaceStore};
|
||||
use crate::shell::{
|
||||
web_surface_permissions::WebSurfaceSitePermissionState,
|
||||
web_surface_runtime::WebSurfaceRuntime,
|
||||
web_surface_state::WebSurfaceInputOutcome,
|
||||
web_surface_worker::{LiveRuntimeClient, LiveRuntimeClientError},
|
||||
};
|
||||
|
||||
struct AcceptingClient;
|
||||
struct RejectingClient;
|
||||
static REJECTED_ENSURE_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
impl LiveRuntimeClient for AcceptingClient {
|
||||
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> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveRuntimeClient for RejectingClient {
|
||||
fn ensure(
|
||||
&mut self,
|
||||
_request: ServoLiveEnsureRequest,
|
||||
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
|
||||
REJECTED_ENSURE_COUNT.fetch_add(1, Ordering::SeqCst);
|
||||
Err(LiveRuntimeClientError::Message("ensure rejected".to_string()))
|
||||
}
|
||||
|
||||
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_worker_ensure_confirms_allow_once_transfer() -> 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 profile_id = tab.profile_id().clone();
|
||||
|
||||
record_viewport(&mut store, &tab);
|
||||
assert!(store.ensure_surface(
|
||||
&tab,
|
||||
ProfileDataMode::Transient,
|
||||
std::slice::from_ref(&permission),
|
||||
));
|
||||
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(),
|
||||
)],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_worker_ensure_keeps_allow_once_untransferred() -> Result<(), String> {
|
||||
REJECTED_ENSURE_COUNT.store(0, Ordering::SeqCst);
|
||||
let runtime = WebSurfaceRuntime::new_with_client_factory(|_| Ok(Box::new(RejectingClient)));
|
||||
let mut store = WebSurfaceStore::new_with_runtime(runtime);
|
||||
let (tab, permission) = tab_and_permission()?;
|
||||
record_viewport(&mut store, &tab);
|
||||
|
||||
assert!(store.ensure_surface(
|
||||
&tab,
|
||||
ProfileDataMode::Transient,
|
||||
std::slice::from_ref(&permission),
|
||||
));
|
||||
store.flush_runtime_for_test();
|
||||
let result = store.tick(std::slice::from_ref(tab.id()));
|
||||
|
||||
assert!(result.permission_transfers.is_empty());
|
||||
let _ =
|
||||
store.ensure_surface(&tab, ProfileDataMode::Transient, std::slice::from_ref(&permission));
|
||||
store.flush_runtime_for_test();
|
||||
let _ = store.tick(std::slice::from_ref(tab.id()));
|
||||
assert_eq!(REJECTED_ENSURE_COUNT.load(Ordering::SeqCst), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tab_and_permission() -> Result<(BrowserTab, WebSurfaceSitePermission), String> {
|
||||
let profile_id = ProfileId::new();
|
||||
let tab = BrowserTab::new(
|
||||
TabId::new(),
|
||||
SpaceId::new(),
|
||||
profile_id,
|
||||
"Web",
|
||||
UrlText::parse("https://example.com/").map_err(|error| error.to_string())?,
|
||||
);
|
||||
let permission = WebSurfaceSitePermission::new(
|
||||
SiteOrigin::parse("https://example.com").map_err(|error| error.to_string())?,
|
||||
SitePermissionFeature::Camera,
|
||||
WebSurfaceSitePermissionState::Decision(SitePermissionDecision::AllowOnce),
|
||||
7,
|
||||
);
|
||||
Ok((tab, permission))
|
||||
}
|
||||
|
||||
fn record_viewport(store: &mut WebSurfaceStore, tab: &BrowserTab) {
|
||||
let bounds = gpui::Bounds::new(
|
||||
gpui::point(gpui::px(0.0), gpui::px(0.0)),
|
||||
gpui::size(gpui::px(640.0), gpui::px(480.0)),
|
||||
);
|
||||
assert_eq!(store.record_viewport_size(tab.id(), bounds, 1.0), WebSurfaceInputOutcome::Applied);
|
||||
}
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
@@ -1,22 +1,39 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ely_browser_core::BrowserCore;
|
||||
#[cfg(test)]
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_domain::{BrowserTab, SiteOrigin, SitePermissionDecision, SitePermissionFeature};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum WebSurfaceSitePermissionState {
|
||||
Decision(SitePermissionDecision),
|
||||
TransferredAllowOnce,
|
||||
}
|
||||
|
||||
impl WebSurfaceSitePermissionState {
|
||||
pub(super) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Decision(decision) => decision.as_str(),
|
||||
Self::TransferredAllowOnce => "transferred-allow-once",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) struct WebSurfaceSitePermission {
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
decision: SitePermissionDecision,
|
||||
state: WebSurfaceSitePermissionState,
|
||||
revision: u64,
|
||||
}
|
||||
|
||||
impl WebSurfaceSitePermission {
|
||||
pub(super) fn new(
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
decision: SitePermissionDecision,
|
||||
state: WebSurfaceSitePermissionState,
|
||||
revision: u64,
|
||||
) -> Self {
|
||||
Self { origin, feature, decision }
|
||||
Self { origin, feature, state, revision }
|
||||
}
|
||||
|
||||
pub(super) fn origin(&self) -> &SiteOrigin {
|
||||
@@ -27,45 +44,52 @@ impl WebSurfaceSitePermission {
|
||||
self.feature
|
||||
}
|
||||
|
||||
pub(super) fn decision(&self) -> SitePermissionDecision {
|
||||
self.decision
|
||||
pub(super) fn state(&self) -> WebSurfaceSitePermissionState {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn web_surface_site_permissions_for_tab(
|
||||
tab: &BrowserTab,
|
||||
snapshot: &BrowserSnapshot,
|
||||
) -> Vec<WebSurfaceSitePermission> {
|
||||
let Ok(Some(origin)) = SiteOrigin::from_url(tab.url()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
snapshot
|
||||
.site_permissions
|
||||
.iter()
|
||||
.filter(|entry| entry.profile_id() == tab.profile_id())
|
||||
.filter(|entry| entry.origin() == &origin)
|
||||
.map(|entry| {
|
||||
WebSurfaceSitePermission::new(entry.origin().clone(), entry.feature(), entry.decision())
|
||||
})
|
||||
.collect()
|
||||
pub(super) fn revision(&self) -> u64 {
|
||||
self.revision
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn web_surface_site_permissions_for_core_tab(
|
||||
core: &BrowserCore,
|
||||
tab: &BrowserTab,
|
||||
) -> Vec<WebSurfaceSitePermission> {
|
||||
let Ok(Some(origin)) = SiteOrigin::from_url(tab.url()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut latest = HashMap::new();
|
||||
for event in core.site_permission_audit_events_for_profile(tab.profile_id()) {
|
||||
let key = (event.origin().clone(), event.feature());
|
||||
let revision = latest.entry(key).or_insert(0_u64);
|
||||
*revision = revision.saturating_add(1);
|
||||
}
|
||||
|
||||
core.site_permissions_for_profile_origin(tab.profile_id(), &origin)
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
WebSurfaceSitePermission::new(entry.origin().clone(), entry.feature(), entry.decision())
|
||||
})
|
||||
.collect()
|
||||
let mut permissions = Vec::new();
|
||||
for entry in core.site_permissions_for_profile(tab.profile_id()) {
|
||||
let key = (entry.origin().clone(), entry.feature());
|
||||
let revision = latest.remove(&key).unwrap_or(0);
|
||||
permissions.push(WebSurfaceSitePermission::new(
|
||||
entry.origin().clone(),
|
||||
entry.feature(),
|
||||
WebSurfaceSitePermissionState::Decision(entry.decision()),
|
||||
revision,
|
||||
));
|
||||
}
|
||||
for (entry, grant_revision) in core.transferred_site_permissions_for_profile(tab.profile_id()) {
|
||||
let key = (entry.origin().clone(), entry.feature());
|
||||
latest.remove(&key);
|
||||
permissions.push(WebSurfaceSitePermission::new(
|
||||
entry.origin().clone(),
|
||||
entry.feature(),
|
||||
WebSurfaceSitePermissionState::TransferredAllowOnce,
|
||||
grant_revision,
|
||||
));
|
||||
}
|
||||
permissions.sort_by(|left, right| {
|
||||
(left.origin().as_str(), left.feature().as_str())
|
||||
.cmp(&(right.origin().as_str(), right.feature().as_str()))
|
||||
});
|
||||
permissions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -77,7 +101,7 @@ mod tests {
|
||||
BrowserTab, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText,
|
||||
};
|
||||
|
||||
use super::web_surface_site_permissions_for_tab;
|
||||
use super::{WebSurfaceSitePermissionState, web_surface_site_permissions_for_core_tab};
|
||||
|
||||
#[test]
|
||||
fn includes_matching_profile_and_origin_permissions() -> Result<(), Box<dyn Error>> {
|
||||
@@ -91,17 +115,20 @@ mod tests {
|
||||
|
||||
let snapshot = core.snapshot()?;
|
||||
let tab = active_tab(&snapshot)?;
|
||||
let permissions = web_surface_site_permissions_for_tab(tab, &snapshot);
|
||||
let permissions = web_surface_site_permissions_for_core_tab(&core, tab);
|
||||
|
||||
assert_eq!(permissions.len(), 1);
|
||||
assert_eq!(permissions[0].origin().as_str(), "https://example.com");
|
||||
assert_eq!(permissions[0].feature(), SitePermissionFeature::Camera);
|
||||
assert_eq!(permissions[0].decision(), SitePermissionDecision::AllowAlways);
|
||||
assert_eq!(
|
||||
permissions[0].state(),
|
||||
WebSurfaceSitePermissionState::Decision(SitePermissionDecision::AllowAlways),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_other_origins() -> Result<(), Box<dyn Error>> {
|
||||
fn includes_the_complete_profile_snapshot() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = browser_core_for("https://example.com/page")?;
|
||||
core.set_site_permission(
|
||||
SiteOrigin::parse("https://other.test")?,
|
||||
@@ -112,7 +139,30 @@ mod tests {
|
||||
let snapshot = core.snapshot()?;
|
||||
let tab = active_tab(&snapshot)?;
|
||||
|
||||
assert!(web_surface_site_permissions_for_tab(tab, &snapshot).is_empty());
|
||||
let permissions = web_surface_site_permissions_for_core_tab(&core, tab);
|
||||
assert_eq!(permissions.len(), 1);
|
||||
assert_eq!(permissions[0].origin().as_str(), "https://other.test");
|
||||
assert_eq!(permissions[0].revision(), 1);
|
||||
assert_eq!(
|
||||
permissions[0].state(),
|
||||
WebSurfaceSitePermissionState::Decision(SitePermissionDecision::AllowOnce),
|
||||
);
|
||||
|
||||
let profile_id = tab.profile_id().clone();
|
||||
let origin = SiteOrigin::parse("https://other.test")?;
|
||||
assert!(core.transfer_site_permission_once(
|
||||
&profile_id,
|
||||
&origin,
|
||||
SitePermissionFeature::Location,
|
||||
1,
|
||||
)?);
|
||||
let transferred = web_surface_site_permissions_for_core_tab(&core, tab);
|
||||
assert_eq!(transferred[0].state(), WebSurfaceSitePermissionState::TransferredAllowOnce);
|
||||
assert_eq!(transferred[0].revision(), 1);
|
||||
|
||||
core.revoke_site_permission(&origin, SitePermissionFeature::Location)?;
|
||||
let revoked = web_surface_site_permissions_for_core_tab(&core, tab);
|
||||
assert!(revoked.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::PathBuf,
|
||||
thread::JoinHandle,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -9,7 +8,7 @@ use ely_domain::{BrowserTab, TabId};
|
||||
|
||||
use crate::services::{
|
||||
ProfileDataMode,
|
||||
servo_live::{ServoLiveClient, ServoLiveEnsureRequest, ServoLiveSitePermission},
|
||||
servo_live::{ServoLiveEnsureRequest, ServoLiveSitePermission},
|
||||
servo_profile_data::cleanup_stale_transient_profile_data_dirs,
|
||||
};
|
||||
|
||||
@@ -21,13 +20,15 @@ use super::{
|
||||
WebSurfaceRuntimeScope, WebSurfaceSession, config_dir_for_scope, session_for_scope,
|
||||
},
|
||||
web_surface_runtime_wire::{
|
||||
input_requests_history_navigation, log_ensure_submitted, pending_input_kind,
|
||||
scroll_wire_fields,
|
||||
allow_once_grants, input_requests_history_navigation, log_ensure_submitted,
|
||||
pending_input_kind, scroll_wire_fields,
|
||||
},
|
||||
web_surface_state::WebSurfacePendingInput,
|
||||
web_surface_worker::{LiveRuntimeClient, LiveRuntimeWorker, RequestGeneration, WorkerResponse},
|
||||
};
|
||||
use cleanup::{ScopedWorker, shutdown_scoped_worker};
|
||||
use cleanup::{
|
||||
LiveRuntimeClientFactory, ScopedWorker, new_servo_live_client, shutdown_scoped_worker,
|
||||
};
|
||||
|
||||
pub(super) use super::web_surface_runtime_session::{
|
||||
WebSurfaceEnsureResult, WebSurfaceRuntimeFrame, WebSurfaceUrlChange, WebSurfaceUrlChangeKind,
|
||||
@@ -130,7 +131,9 @@ impl WebSurfaceRuntime {
|
||||
hover_x: input.hover_point.map(|point| point.x()),
|
||||
hover_y: input.hover_point.map(|point| point.y()),
|
||||
typed_text: input.typed_text,
|
||||
site_permission_generation: generation.value(),
|
||||
site_permissions: permissions.iter().map(ServoLiveSitePermission::from).collect(),
|
||||
allow_once_grants: allow_once_grants(tab.profile_id(), permissions),
|
||||
};
|
||||
|
||||
let Some(scoped) = self.workers.get(&scope) else {
|
||||
@@ -168,22 +171,24 @@ impl WebSurfaceRuntime {
|
||||
|
||||
let poll_now = Instant::now();
|
||||
for tab_id in visible_tab_ids {
|
||||
let Some(scope) = self.sessions.get(tab_id).and_then(|session| {
|
||||
session.cadence.should_poll(poll_now).then(|| session.scope.clone())
|
||||
let Some((scope, generation)) = self.sessions.get(tab_id).and_then(|session| {
|
||||
session
|
||||
.cadence
|
||||
.should_poll(poll_now)
|
||||
.then(|| (session.scope.clone(), session.generation))
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let Some(generation) = generation else {
|
||||
continue;
|
||||
};
|
||||
if !self.workers.contains_key(&scope) {
|
||||
continue;
|
||||
}
|
||||
let generation = self.next_request_generation();
|
||||
let submitted = self.workers.get(&scope).is_some_and(|scoped| {
|
||||
let _ = self.workers.get(&scope).is_some_and(|scoped| {
|
||||
scoped.worker.submit_poll(generation, tab_id.as_str().to_string())
|
||||
});
|
||||
if let Some(session) = self.sessions.get_mut(tab_id) {
|
||||
if submitted {
|
||||
session.generation = Some(generation);
|
||||
}
|
||||
session.cadence.note_poll_submitted(poll_now);
|
||||
}
|
||||
}
|
||||
@@ -353,6 +358,12 @@ impl WebSurfaceRuntime {
|
||||
}
|
||||
}
|
||||
WorkerResponse::RuntimeUnavailable => runtime_unavailable = true,
|
||||
WorkerResponse::PermissionSnapshotAccepted(grant) => {
|
||||
frames.push(WebSurfaceRuntimeFrame::PermissionSnapshotAccepted(grant));
|
||||
}
|
||||
WorkerResponse::PermissionConsumed(consumed) => {
|
||||
frames.push(WebSurfaceRuntimeFrame::PermissionConsumed(consumed));
|
||||
}
|
||||
}
|
||||
}
|
||||
runtime_unavailable
|
||||
@@ -448,15 +459,6 @@ impl Drop for WebSurfaceRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type LiveRuntimeClientFactory =
|
||||
fn(PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String>;
|
||||
|
||||
fn new_servo_live_client(config_dir: PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String> {
|
||||
ServoLiveClient::new(config_dir)
|
||||
.map(|client| Box::new(client) as Box<dyn LiveRuntimeClient>)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct ScopeRetryState {
|
||||
failure_count: u32,
|
||||
|
||||
@@ -121,12 +121,22 @@ fn pending_poll_advances_deadline_under_worker_backpressure() -> Result<(), Stri
|
||||
let tab = web_tab("Backpressure")?;
|
||||
runtime.ensure_tab(&tab, surface_size(), ProfileDataMode::Transient, &[], pending_input())?;
|
||||
runtime.flush_for_test();
|
||||
let ensure_generation = runtime
|
||||
.sessions
|
||||
.get(tab.id())
|
||||
.and_then(|session| session.generation)
|
||||
.ok_or_else(|| "ensure generation was missing".to_string())?;
|
||||
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
runtime.tick(std::slice::from_ref(tab.id()));
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
runtime.tick(std::slice::from_ref(tab.id()));
|
||||
|
||||
assert_eq!(
|
||||
runtime.sessions.get(tab.id()).and_then(|session| session.generation),
|
||||
Some(ensure_generation),
|
||||
);
|
||||
|
||||
let delay = runtime
|
||||
.next_poll_delay(std::slice::from_ref(tab.id()), std::time::Instant::now())
|
||||
.ok_or_else(|| "visible tab lost its poll deadline".to_string())?;
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
use crate::services::servo_profile_data::TransientProfileDataDir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::LiveRuntimeWorker;
|
||||
use crate::services::{servo_live::ServoLiveClient, servo_profile_data::TransientProfileDataDir};
|
||||
|
||||
use super::{LiveRuntimeClient, LiveRuntimeWorker};
|
||||
|
||||
pub(super) type LiveRuntimeClientFactory =
|
||||
fn(PathBuf) -> Result<Box<dyn LiveRuntimeClient>, String>;
|
||||
|
||||
pub(super) fn new_servo_live_client(
|
||||
config_dir: PathBuf,
|
||||
) -> Result<Box<dyn LiveRuntimeClient>, String> {
|
||||
ServoLiveClient::new(config_dir)
|
||||
.map(|client| Box::new(client) as Box<dyn LiveRuntimeClient>)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub(super) struct ScopedWorker {
|
||||
pub(super) worker: LiveRuntimeWorker,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use crate::services::{
|
||||
ProfileDataMode,
|
||||
servo_live::ServoLivePermissionGrant,
|
||||
servo_profile_data::{
|
||||
TransientProfileDataDir, create_profile_data_dir, default_profile_data_root,
|
||||
transient_profile_data_dir,
|
||||
@@ -102,6 +103,8 @@ pub(super) struct WebSurfaceEnsureResult {
|
||||
pub(super) enum WebSurfaceRuntimeFrame {
|
||||
Ready { tab_id: TabId, frame: Box<WebSurfaceFrame>, url_change: Option<WebSurfaceUrlChange> },
|
||||
Failed { tab_id: TabId, message: String },
|
||||
PermissionSnapshotAccepted(ServoLivePermissionGrant),
|
||||
PermissionConsumed(ServoLivePermissionGrant),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use ely_domain::BrowserTab;
|
||||
use ely_domain::{BrowserTab, ProfileId, SitePermissionDecision};
|
||||
|
||||
use crate::services::servo_live::ServoLiveSitePermission;
|
||||
use crate::services::servo_live::{ServoLivePermissionGrant, ServoLiveSitePermission};
|
||||
|
||||
use super::{
|
||||
web_surface_cadence::WebSurfaceInputKind,
|
||||
web_surface_geometry::{WebSurfaceClickPoint, WebSurfaceScrollDelta, WebSurfaceSize},
|
||||
web_surface_permissions::WebSurfaceSitePermission,
|
||||
web_surface_permissions::{WebSurfaceSitePermission, WebSurfaceSitePermissionState},
|
||||
web_surface_state::WebSurfacePendingInput,
|
||||
};
|
||||
|
||||
@@ -30,6 +30,27 @@ pub(super) fn input_requests_history_navigation(input: &WebSurfacePendingInput)
|
||||
|| input.typed_text.as_deref().is_some_and(|text| text.contains('\n'))
|
||||
}
|
||||
|
||||
pub(super) fn allow_once_grants(
|
||||
profile_id: &ProfileId,
|
||||
permissions: &[WebSurfaceSitePermission],
|
||||
) -> Vec<ServoLivePermissionGrant> {
|
||||
permissions
|
||||
.iter()
|
||||
.filter(|permission| {
|
||||
permission.state()
|
||||
== WebSurfaceSitePermissionState::Decision(SitePermissionDecision::AllowOnce)
|
||||
})
|
||||
.map(|permission| {
|
||||
ServoLivePermissionGrant::new(
|
||||
profile_id.clone(),
|
||||
permission.origin().clone(),
|
||||
permission.feature(),
|
||||
permission.revision(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn pending_input_kind(input: &WebSurfacePendingInput) -> WebSurfaceInputKind {
|
||||
if input.scroll_delta.is_some() {
|
||||
WebSurfaceInputKind::Scroll
|
||||
@@ -74,7 +95,8 @@ impl From<&WebSurfaceSitePermission> for ServoLiveSitePermission {
|
||||
Self::new(
|
||||
permission.origin().as_str(),
|
||||
permission.feature().as_str(),
|
||||
permission.decision(),
|
||||
permission.state().as_str(),
|
||||
permission.revision(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use ely_domain::{ProfileId, TabId};
|
||||
use gpui::{Bounds, Pixels};
|
||||
|
||||
use crate::services::ProfileDataMode;
|
||||
use crate::services::servo_live::ServoLivePermissionGrant;
|
||||
|
||||
use super::{
|
||||
web_surface_cadence::ACTIVE_POLL_INTERVAL,
|
||||
@@ -110,6 +111,8 @@ pub(super) struct WebSurfaceTickResult {
|
||||
pub(super) changed: bool,
|
||||
pub(super) url_changes: Vec<WebSurfaceUrlChange>,
|
||||
pub(super) page_metadata: Vec<WebSurfacePageMetadata>,
|
||||
pub(super) permission_transfers: Vec<ServoLivePermissionGrant>,
|
||||
pub(super) permission_consumptions: Vec<ServoLivePermissionGrant>,
|
||||
}
|
||||
|
||||
/// All per-tab surface invariants in one owner.
|
||||
@@ -196,6 +199,11 @@ impl PerTabSurface {
|
||||
self.last_ensure_key.as_ref() != Some(key) || self.has_pending_input()
|
||||
}
|
||||
|
||||
pub(super) fn should_defer_resize(&self, key: &WebSurfaceEnsureKey, now: Instant) -> bool {
|
||||
self.last_ensure_key.as_ref().is_some_and(|last| last.permissions == key.permissions)
|
||||
&& self.viewport_size_is_settling(now)
|
||||
}
|
||||
|
||||
pub(super) fn has_scope(
|
||||
&self,
|
||||
profile_id: &ProfileId,
|
||||
@@ -368,6 +376,31 @@ mod tests {
|
||||
assert!(surface.should_ensure(&new_key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_change_bypasses_viewport_resize_debounce()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let now = Instant::now();
|
||||
let profile_id = ProfileId::new();
|
||||
let old_key = ensure_key("https://example.com/", 800, 600, &profile_id);
|
||||
let resized_key = ensure_key("https://example.com/", 1024, 768, &profile_id);
|
||||
let mut new_key = ensure_key("https://example.com/", 1024, 768, &profile_id);
|
||||
new_key.permissions.push(WebSurfaceSitePermission::new(
|
||||
ely_domain::SiteOrigin::parse("https://example.com")?,
|
||||
ely_domain::SitePermissionFeature::Camera,
|
||||
crate::shell::web_surface_permissions::WebSurfaceSitePermissionState::Decision(
|
||||
ely_domain::SitePermissionDecision::DenyAlways,
|
||||
),
|
||||
1,
|
||||
));
|
||||
let mut surface = PerTabSurface::new();
|
||||
surface.mark_ensured(old_key);
|
||||
surface.mark_viewport_size_changed(now);
|
||||
|
||||
assert!(surface.should_defer_resize(&resized_key, now));
|
||||
assert!(!surface.should_defer_resize(&new_key, now));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_change_forces_ensure() {
|
||||
let old_key = ensure_key("https://example.com/", 800, 600, &ProfileId::new());
|
||||
|
||||
@@ -7,6 +7,14 @@ use std::{
|
||||
|
||||
use crate::services::servo_live::{
|
||||
ServoLiveClient, ServoLiveEnsureRequest, ServoLiveError, ServoLiveFrame,
|
||||
ServoLivePermissionGrant,
|
||||
};
|
||||
|
||||
#[path = "web_surface_worker_dispatch.rs"]
|
||||
mod dispatch;
|
||||
use dispatch::{
|
||||
dispatch_result, forward_permission_consumptions, preserve_latest_hover,
|
||||
request_has_ordered_input,
|
||||
};
|
||||
|
||||
/// Blocking transport for one profile-scoped Servo sidecar.
|
||||
@@ -22,6 +30,10 @@ pub(super) trait LiveRuntimeClient {
|
||||
fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError>;
|
||||
|
||||
fn close(&mut self, tab_id: String) -> Result<(), LiveRuntimeClientError>;
|
||||
|
||||
fn take_permission_consumptions(&mut self) -> Vec<ServoLivePermissionGrant> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveRuntimeClient for ServoLiveClient {
|
||||
@@ -39,6 +51,10 @@ impl LiveRuntimeClient for ServoLiveClient {
|
||||
fn close(&mut self, tab_id: String) -> Result<(), LiveRuntimeClientError> {
|
||||
ServoLiveClient::close(self, tab_id).map_err(LiveRuntimeClientError::from)
|
||||
}
|
||||
|
||||
fn take_permission_consumptions(&mut self) -> Vec<ServoLivePermissionGrant> {
|
||||
ServoLiveClient::take_permission_consumptions(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -88,6 +104,8 @@ pub(super) enum WorkerResponse {
|
||||
Frame { generation: RequestGeneration, tab_id: String, frame: ServoLiveFrame },
|
||||
Failed { generation: RequestGeneration, tab_id: String, message: String },
|
||||
RuntimeUnavailable,
|
||||
PermissionSnapshotAccepted(ServoLivePermissionGrant),
|
||||
PermissionConsumed(ServoLivePermissionGrant),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
@@ -97,10 +115,14 @@ impl RequestGeneration {
|
||||
pub(super) const fn new(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub(super) const fn value(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkerRequest {
|
||||
Ensure { generation: RequestGeneration, request: ServoLiveEnsureRequest },
|
||||
Ensure { generation: RequestGeneration, request: Box<ServoLiveEnsureRequest> },
|
||||
Poll { generation: RequestGeneration, tab_id: String },
|
||||
}
|
||||
|
||||
@@ -190,7 +212,7 @@ impl LiveRuntimeWorker {
|
||||
let _ = self.response_tx.send(WorkerResponse::Failed { generation, tab_id, message });
|
||||
return;
|
||||
}
|
||||
let mut request = WorkerRequest::Ensure { generation, request };
|
||||
let mut request = WorkerRequest::Ensure { generation, request: Box::new(request) };
|
||||
if let Some(pending) = q.pending.get_mut(&tab_id) {
|
||||
let replace_tail = pending.back().is_some_and(|tail| {
|
||||
matches!(tail, WorkerRequest::Poll { .. })
|
||||
@@ -394,15 +416,26 @@ fn run_worker(
|
||||
let exit_after_dispatch = match work {
|
||||
Work::Close(tab_id) => {
|
||||
let _ = client.close(tab_id);
|
||||
forward_permission_consumptions(&mut *client, &response_tx);
|
||||
false
|
||||
}
|
||||
Work::Request(WorkerRequest::Ensure { generation, request }) => {
|
||||
Work::Request(WorkerRequest::Ensure { generation, mut request }) => {
|
||||
let tab_id = request.tab_id.clone();
|
||||
dispatch_result(&response_tx, generation, tab_id, client.ensure(request))
|
||||
let allow_once_grants = std::mem::take(&mut request.allow_once_grants);
|
||||
let result = client.ensure(*request);
|
||||
if result.is_ok() {
|
||||
for grant in allow_once_grants {
|
||||
let _ = response_tx.send(WorkerResponse::PermissionSnapshotAccepted(grant));
|
||||
}
|
||||
}
|
||||
forward_permission_consumptions(&mut *client, &response_tx);
|
||||
dispatch_result(&response_tx, generation, tab_id, result)
|
||||
}
|
||||
Work::Request(WorkerRequest::Poll { generation, tab_id }) => {
|
||||
let request_tab_id = tab_id.clone();
|
||||
dispatch_result(&response_tx, generation, request_tab_id, client.poll(tab_id))
|
||||
let result = client.poll(tab_id);
|
||||
forward_permission_consumptions(&mut *client, &response_tx);
|
||||
dispatch_result(&response_tx, generation, request_tab_id, result)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -424,60 +457,6 @@ enum Work {
|
||||
Request(WorkerRequest),
|
||||
}
|
||||
|
||||
/// Forward a single client result to the response channel. Returns
|
||||
/// `true` when the worker should exit.
|
||||
fn dispatch_result(
|
||||
response_tx: &mpsc::Sender<WorkerResponse>,
|
||||
generation: RequestGeneration,
|
||||
tab_id: String,
|
||||
result: Result<Option<ServoLiveFrame>, LiveRuntimeClientError>,
|
||||
) -> bool {
|
||||
match result {
|
||||
Ok(Some(frame)) => {
|
||||
let _ = response_tx.send(WorkerResponse::Frame { generation, tab_id, frame });
|
||||
false
|
||||
}
|
||||
Ok(None) => false,
|
||||
Err(error) => {
|
||||
let unavailable = error.is_runtime_unavailable();
|
||||
let message = error.to_string();
|
||||
let _ = response_tx.send(WorkerResponse::Failed { generation, tab_id, message });
|
||||
if unavailable {
|
||||
let _ = response_tx.send(WorkerResponse::RuntimeUnavailable);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn request_has_ordered_input(request: &WorkerRequest) -> bool {
|
||||
let WorkerRequest::Ensure { request, .. } = request else {
|
||||
return false;
|
||||
};
|
||||
request.scroll_delta_x != 0
|
||||
|| request.scroll_delta_y != 0
|
||||
|| request.scroll_point_x.is_some()
|
||||
|| request.scroll_point_y.is_some()
|
||||
|| request.click_x.is_some()
|
||||
|| request.click_y.is_some()
|
||||
|| request.typed_text.is_some()
|
||||
}
|
||||
|
||||
fn preserve_latest_hover(latest: &mut WorkerRequest, previous: &WorkerRequest) {
|
||||
let (
|
||||
WorkerRequest::Ensure { request: latest, .. },
|
||||
WorkerRequest::Ensure { request: previous, .. },
|
||||
) = (latest, previous)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if latest.hover_x.is_none() && latest.hover_y.is_none() {
|
||||
latest.hover_x = previous.hover_x;
|
||||
latest.hover_y = previous.hover_y;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "web_surface_worker_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::sync::mpsc;
|
||||
|
||||
use crate::services::servo_live::ServoLiveFrame;
|
||||
|
||||
use super::{
|
||||
LiveRuntimeClient, LiveRuntimeClientError, RequestGeneration, WorkerRequest, WorkerResponse,
|
||||
};
|
||||
|
||||
pub(super) fn forward_permission_consumptions(
|
||||
client: &mut dyn LiveRuntimeClient,
|
||||
response_tx: &mpsc::Sender<WorkerResponse>,
|
||||
) {
|
||||
for consumption in client.take_permission_consumptions() {
|
||||
let _ = response_tx.send(WorkerResponse::PermissionConsumed(consumption));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_result(
|
||||
response_tx: &mpsc::Sender<WorkerResponse>,
|
||||
generation: RequestGeneration,
|
||||
tab_id: String,
|
||||
result: Result<Option<ServoLiveFrame>, LiveRuntimeClientError>,
|
||||
) -> bool {
|
||||
match result {
|
||||
Ok(Some(frame)) => {
|
||||
let _ = response_tx.send(WorkerResponse::Frame { generation, tab_id, frame });
|
||||
false
|
||||
}
|
||||
Ok(None) => false,
|
||||
Err(error) => {
|
||||
let unavailable = error.is_runtime_unavailable();
|
||||
let message = error.to_string();
|
||||
let _ = response_tx.send(WorkerResponse::Failed { generation, tab_id, message });
|
||||
if unavailable {
|
||||
let _ = response_tx.send(WorkerResponse::RuntimeUnavailable);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn request_has_ordered_input(request: &WorkerRequest) -> bool {
|
||||
let WorkerRequest::Ensure { request, .. } = request else {
|
||||
return false;
|
||||
};
|
||||
request.scroll_delta_x != 0
|
||||
|| request.scroll_delta_y != 0
|
||||
|| request.scroll_point_x.is_some()
|
||||
|| request.scroll_point_y.is_some()
|
||||
|| request.click_x.is_some()
|
||||
|| request.click_y.is_some()
|
||||
|| request.typed_text.is_some()
|
||||
|| request.site_permissions.iter().any(|permission| permission.state == "allow-once")
|
||||
}
|
||||
|
||||
pub(super) fn preserve_latest_hover(latest: &mut WorkerRequest, previous: &WorkerRequest) {
|
||||
let (
|
||||
WorkerRequest::Ensure { request: latest, .. },
|
||||
WorkerRequest::Ensure { request: previous, .. },
|
||||
) = (latest, previous)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if latest.hover_x.is_none() && latest.hover_y.is_none() {
|
||||
latest.hover_x = previous.hover_x;
|
||||
latest.hover_y = previous.hover_y;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::services::servo_live::{ServoLiveEnsureRequest, ServoLiveFrame};
|
||||
use crate::services::servo_live::{
|
||||
ServoLiveEnsureRequest, ServoLiveFrame, ServoLivePermissionGrant, ServoLiveSitePermission,
|
||||
};
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
|
||||
|
||||
use super::{
|
||||
LiveRuntimeClient, LiveRuntimeClientError, LiveRuntimeWorker, RequestGeneration, WorkerResponse,
|
||||
@@ -16,6 +19,7 @@ enum RecordedInput {
|
||||
Click(String),
|
||||
Text(String),
|
||||
Hover(String),
|
||||
Permission(String),
|
||||
}
|
||||
|
||||
struct SlowRecordingClient {
|
||||
@@ -27,6 +31,31 @@ struct SlowRecordingClient {
|
||||
|
||||
struct GenerationClient;
|
||||
|
||||
struct ConsumptionClient {
|
||||
pending: Vec<ServoLivePermissionGrant>,
|
||||
}
|
||||
|
||||
impl LiveRuntimeClient for ConsumptionClient {
|
||||
fn ensure(
|
||||
&mut self,
|
||||
_request: ServoLiveEnsureRequest,
|
||||
) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
|
||||
Err(LiveRuntimeClientError::Message("ensure failed".to_string()))
|
||||
}
|
||||
|
||||
fn poll(&mut self, _tab_id: String) -> Result<Option<ServoLiveFrame>, LiveRuntimeClientError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn close(&mut self, _tab_id: String) -> Result<(), LiveRuntimeClientError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_permission_consumptions(&mut self) -> Vec<ServoLivePermissionGrant> {
|
||||
std::mem::take(&mut self.pending)
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveRuntimeClient for GenerationClient {
|
||||
fn ensure(
|
||||
&mut self,
|
||||
@@ -117,6 +146,52 @@ fn queued_edge_inputs_for_one_tab_are_preserved() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_once_transfer_is_preserved_ahead_of_idle_updates() -> Result<(), String> {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let (first_started_tx, first_started_rx) = mpsc::channel();
|
||||
let (release_first_tx, release_first_rx) = mpsc::channel();
|
||||
let client_calls = calls.clone();
|
||||
let worker = LiveRuntimeWorker::new(move || {
|
||||
Ok(Box::new(SlowRecordingClient {
|
||||
calls: client_calls,
|
||||
first_started_tx: Some(first_started_tx),
|
||||
release_first_rx,
|
||||
return_frame: false,
|
||||
}))
|
||||
})?;
|
||||
|
||||
worker.submit_ensure(
|
||||
RequestGeneration::new(1),
|
||||
ensure_request("tab-a", RecordedInput::Idle("tab-a".to_string())),
|
||||
);
|
||||
first_started_rx.recv_timeout(Duration::from_secs(1)).map_err(|error| error.to_string())?;
|
||||
let mut transfer = ensure_request("tab-a", RecordedInput::Idle("tab-a".to_string()));
|
||||
transfer.site_permissions.push(ServoLiveSitePermission::new(
|
||||
"https://example.com",
|
||||
"camera",
|
||||
"allow-once",
|
||||
7,
|
||||
));
|
||||
worker.submit_ensure(RequestGeneration::new(2), transfer);
|
||||
worker.submit_ensure(
|
||||
RequestGeneration::new(3),
|
||||
ensure_request("tab-a", RecordedInput::Idle("tab-a".to_string())),
|
||||
);
|
||||
release_first_tx.send(()).map_err(|error| error.to_string())?;
|
||||
worker.wait_until_idle();
|
||||
|
||||
assert_eq!(
|
||||
*calls.lock().map_err(|_| "call recorder lock was poisoned".to_string())?,
|
||||
vec![
|
||||
RecordedInput::Idle("tab-a".to_string()),
|
||||
RecordedInput::Permission("tab-a".to_string()),
|
||||
RecordedInput::Idle("tab-a".to_string()),
|
||||
],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_tabs_are_dispatched_round_robin() -> Result<(), String> {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -191,6 +266,32 @@ fn responses_keep_their_request_generation() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_consumption_is_forwarded_after_ensure_error() -> Result<(), String> {
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com").map_err(|error| error.to_string())?;
|
||||
let grant = ServoLivePermissionGrant::new(profile_id, origin, SitePermissionFeature::Camera, 7);
|
||||
let expected = grant.clone();
|
||||
let consumed = grant.clone();
|
||||
let worker = LiveRuntimeWorker::new(move || {
|
||||
Ok(Box::new(ConsumptionClient { pending: vec![consumed] }))
|
||||
})?;
|
||||
|
||||
let mut request = ensure_request("tab-a", RecordedInput::Idle("tab-a".to_string()));
|
||||
request.allow_once_grants.push(grant);
|
||||
worker.submit_ensure(RequestGeneration::new(1), request);
|
||||
worker.wait_until_idle();
|
||||
|
||||
assert!(matches!(
|
||||
worker.drain_responses().as_slice(),
|
||||
[
|
||||
WorkerResponse::PermissionConsumed(consumed),
|
||||
WorkerResponse::Failed { message, .. },
|
||||
] if consumed == &expected && message == "ensure failed"
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_updates_coalesce_to_latest_state() -> Result<(), String> {
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -321,7 +422,9 @@ fn poisoned_queue_still_shuts_down_worker() -> Result<(), String> {
|
||||
|
||||
fn recorded_input(request: &ServoLiveEnsureRequest) -> RecordedInput {
|
||||
let tab_id = request.tab_id.clone();
|
||||
if request.scroll_delta_x != 0 || request.scroll_delta_y != 0 {
|
||||
if request.site_permissions.iter().any(|permission| permission.state == "allow-once") {
|
||||
RecordedInput::Permission(tab_id)
|
||||
} else if request.scroll_delta_x != 0 || request.scroll_delta_y != 0 {
|
||||
RecordedInput::Scroll(tab_id)
|
||||
} else if request.click_x.is_some() {
|
||||
RecordedInput::Click(tab_id)
|
||||
@@ -356,6 +459,8 @@ fn ensure_request(tab_id: &str, input: RecordedInput) -> ServoLiveEnsureRequest
|
||||
hover_x: hover.then_some(1),
|
||||
hover_y: hover.then_some(1),
|
||||
typed_text: text.then(|| "text".to_string()),
|
||||
site_permission_generation: 1,
|
||||
site_permissions: Vec::new(),
|
||||
allow_once_grants: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user