use std::{ io::Write, thread, time::{Duration, Instant}, }; use ely_domain::TabId; use ely_servo_host::{ KeyboardTextRequest, MouseClickRequest, MouseDragRequest, NavigationRequest, PageZoomRequest, PermissionRequest, ScrollRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, TouchTapRequest, WebViewSnapshot, WebViewState, }; use thiserror::Error; #[path = "ely_servo_sidecar/args.rs"] mod args; #[path = "ely_servo_sidecar/live.rs"] mod live; #[path = "ely_servo_sidecar/live_protocol.rs"] mod live_protocol; #[path = "ely_servo_sidecar/perf.rs"] mod perf; #[path = "ely_servo_sidecar/report.rs"] mod report; use args::{SidecarCommand, SnapshotArgs}; use report::{SnapshotInputChanges, SnapshotReport}; const WAIT_ITERATIONS: usize = 5_000; const WAIT_INTERVAL: Duration = Duration::from_millis(2); const RENDER_TIMEOUT: Duration = Duration::from_secs(20); const VISIBLE_FRAME_SETTLE_TIMEOUT: Duration = Duration::from_millis(700); const INPUT_SETTLE_TIMEOUT: Duration = Duration::from_millis(700); fn main() -> Result<(), SidecarError> { match args::parse_env_command()? { SidecarCommand::Live(args) => live::run_live(args).map_err(SidecarError::Live), SidecarCommand::Snapshot(args) => run_snapshot(args), } } #[derive(Debug, Error)] enum SidecarError { #[error("timed out rendering {url}: {snapshot:?}")] RenderTimeout { url: String, snapshot: Box }, #[error(transparent)] Args(#[from] args::SidecarArgsError), #[error(transparent)] Host(#[from] ServoHostError), #[error(transparent)] Live(#[from] live::LiveSidecarError), #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Json(#[from] serde_json::Error), } fn run_snapshot(args: SnapshotArgs) -> Result<(), SidecarError> { std::fs::create_dir_all(&args.profile_data_dir)?; let mut host = SoftwareServoHost::new_with_config_dir( ServoSurfaceSize::new(args.width, args.height), Some(args.profile_data_dir.clone()), )?; let tab_id = TabId::new(); let webview_id = host.create_webview(tab_id.clone(), args.profile_id.clone())?; apply_site_permissions(&mut host, &webview_id, &args)?; host.set_page_zoom(PageZoomRequest { webview_id: webview_id.clone(), zoom_factor: f32::from(args.page_zoom_percent) / 100.0, })?; host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url: args.url.clone(), })?; let snapshot = wait_for_frame(&mut host, &webview_id, args.url.as_str())?; let (snapshot, scroll_changed_frame) = apply_scroll_if_requested(&mut host, &webview_id, &args, snapshot)?; let (snapshot, click_changed_frame) = apply_click_if_requested(&mut host, &webview_id, &args, snapshot)?; let (snapshot, drag_changed_frame) = apply_drag_if_requested(&mut host, &webview_id, &args, snapshot)?; let (snapshot, touch_changed_frame) = apply_touch_if_requested(&mut host, &webview_id, &args, snapshot)?; let (snapshot, text_changed_frame) = apply_text_if_requested(&mut host, &webview_id, &args, snapshot)?; let frame = host.last_rendered_frame()?; std::fs::write(&args.rgba_out, frame.rgba_bytes())?; let mut stdout = std::io::stdout().lock(); serde_json::to_writer( &mut stdout, &SnapshotReport::new( &args, &snapshot, &frame, SnapshotInputChanges { scroll: scroll_changed_frame, click: click_changed_frame, drag: drag_changed_frame, touch: touch_changed_frame, text: text_changed_frame, }, ), )?; stdout.write_all(b"\n")?; stdout.flush()?; std::process::exit(0); } fn apply_site_permissions( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, args: &SnapshotArgs, ) -> Result<(), SidecarError> { for permission in &args.site_permissions { host.set_permission( PermissionRequest { webview_id: webview_id.clone(), profile_id: args.profile_id.clone(), origin: permission.origin.clone(), feature: permission.feature, }, permission.decision.into(), )?; } Ok(()) } fn apply_scroll_if_requested( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, args: &SnapshotArgs, snapshot: WebViewSnapshot, ) -> Result<(WebViewSnapshot, bool), SidecarError> { if args.scroll_x == 0 && args.scroll_y == 0 { return Ok((snapshot, false)); } let previous_frame_hash = host.last_rendered_frame()?.sample_hash(); host.scroll(ScrollRequest { webview_id: webview_id.clone(), delta_x: args.scroll_x, delta_y: args.scroll_y, })?; wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash) } fn apply_click_if_requested( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, args: &SnapshotArgs, snapshot: WebViewSnapshot, ) -> Result<(WebViewSnapshot, bool), SidecarError> { let Some(click_point) = args.click_point else { return Ok((snapshot, false)); }; let previous_frame_hash = host.last_rendered_frame()?.sample_hash(); host.click(MouseClickRequest { webview_id: webview_id.clone(), x: click_point.x, y: click_point.y, })?; wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash) } fn apply_drag_if_requested( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, args: &SnapshotArgs, snapshot: WebViewSnapshot, ) -> Result<(WebViewSnapshot, bool), SidecarError> { let Some(drag_points) = args.drag_points else { return Ok((snapshot, false)); }; let previous_frame_hash = host.last_rendered_frame()?.sample_hash(); host.drag(MouseDragRequest { webview_id: webview_id.clone(), from_x: drag_points.from.x, from_y: drag_points.from.y, to_x: drag_points.to.x, to_y: drag_points.to.y, })?; wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash) } fn apply_touch_if_requested( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, args: &SnapshotArgs, snapshot: WebViewSnapshot, ) -> Result<(WebViewSnapshot, bool), SidecarError> { let Some(touch_point) = args.touch_point else { return Ok((snapshot, false)); }; let previous_frame_hash = host.last_rendered_frame()?.sample_hash(); host.touch_tap(TouchTapRequest { webview_id: webview_id.clone(), x: touch_point.x, y: touch_point.y, })?; wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash) } fn apply_text_if_requested( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, args: &SnapshotArgs, snapshot: WebViewSnapshot, ) -> Result<(WebViewSnapshot, bool), SidecarError> { let Some(typed_text) = args.typed_text.as_ref() else { return Ok((snapshot, false)); }; let previous_frame_hash = host.last_rendered_frame()?.sample_hash(); host.type_text(KeyboardTextRequest { webview_id: webview_id.clone(), text: typed_text.clone(), })?; wait_for_changed_or_settled_frame(host, webview_id, previous_frame_hash) } fn wait_for_frame( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, url: &str, ) -> Result { let started_at = Instant::now(); let mut latest_rendered_snapshot = None; let mut visible_frame_hash = None; let mut visible_frame_last_changed_at = None; for _ in 0..WAIT_ITERATIONS { if started_at.elapsed() >= RENDER_TIMEOUT { break; } host.tick(); let snapshot = host.snapshot(webview_id)?; if snapshot.has_pending_frame() { host.paint(webview_id)?; } let snapshot = host.snapshot(webview_id)?; if let Ok(frame) = host.last_rendered_frame() && frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0 { let current_hash = frame.sample_hash(); if visible_frame_hash != Some(current_hash) { visible_frame_hash = Some(current_hash); visible_frame_last_changed_at = Some(Instant::now()); } if snapshot.state() == &WebViewState::Complete { return Ok(snapshot); } latest_rendered_snapshot = Some(snapshot.clone()); if snapshot.url().is_some() && visible_frame_last_changed_at .is_some_and(|changed_at| changed_at.elapsed() >= VISIBLE_FRAME_SETTLE_TIMEOUT) { return Ok(snapshot); } } thread::sleep(WAIT_INTERVAL); } if let Some(snapshot) = latest_rendered_snapshot { return Ok(snapshot); } Err(SidecarError::RenderTimeout { url: url.to_string(), snapshot: Box::new(host.snapshot(webview_id)?), }) } fn wait_for_changed_or_settled_frame( host: &mut SoftwareServoHost, webview_id: &ely_domain::WebViewId, previous_frame_hash: u64, ) -> Result<(WebViewSnapshot, bool), SidecarError> { let started_at = Instant::now(); let mut latest_snapshot = host.snapshot(webview_id)?; for _ in 0..WAIT_ITERATIONS { if started_at.elapsed() >= INPUT_SETTLE_TIMEOUT { break; } host.tick(); let snapshot = host.snapshot(webview_id)?; if snapshot.has_pending_frame() { host.paint(webview_id)?; } latest_snapshot = host.snapshot(webview_id)?; let changed_frame = host.last_rendered_frame().is_ok_and(|frame| { frame.non_white_pixel_count() > 0 && frame.sample_hash() != previous_frame_hash }); if changed_frame { return Ok((latest_snapshot, true)); } thread::sleep(WAIT_INTERVAL); } Ok((latest_snapshot, false)) }