Skip to main content

glass/browser/session/
frame.rs

1//! Frame tree discovery and selection.
2//!
3//! Lists all frames in the active page target's frame tree and provides
4//! explicit frame selection for subsequent operations.
5
6use super::*;
7
8impl BrowserSession {
9    /// List all frames in the active target's frame tree.
10    ///
11    /// Discovers iframe/out-of-process frames and marks the currently active
12    /// frame. Call this before `select_frame` to discover available frame IDs.
13    pub async fn list_frames(&self) -> BrowserResult<Vec<FrameInfo>> {
14        let (target_id, target_session, active) = {
15            let topology = self.topology.lock().await;
16            let target_id = topology.active_target_id.clone();
17            (
18                target_id.clone(),
19                topology.active_target_session_id.clone(),
20                topology.active_frame_id.clone(),
21            )
22        };
23        if target_id.is_none() {
24            return Err(TopologyError::new(
25                TopologyErrorKind::NoTargetSelected,
26                "no active target is selected; call listTargets to discover available pages",
27            )
28            .into());
29        }
30        if target_session.is_none() {
31            let mut topology = self.topology.lock().await;
32            topology.active_target_id = None;
33            topology.active_session_id = None;
34            topology.active_target_session_id = None;
35            topology.active_frame_id = None;
36            topology.frames.clear();
37            topology.frame_sessions.clear();
38            topology.frame_parents.clear();
39            self.cdp.set_active_target_route(None, None, None, None);
40            return Err(TopologyError::new(
41                TopologyErrorKind::NoTargetSelected,
42                "no active target is selected; call listTargets to discover available pages",
43            )
44            .into());
45        }
46        let target_session = target_session.ok_or_else(|| {
47            TopologyError::new(
48                TopologyErrorKind::NoPageSession,
49                "active target has no CDP session; the session may need to be re-established",
50            )
51        })?;
52        let raw = match self
53            .cdp
54            .send_to_session(&target_session, "Page.getFrameTree", None)
55            .await
56        {
57            Ok(raw) => raw,
58            Err(error) => {
59                // A crashed page can leave Target.targetCrashed and the
60                // detached-session notification queued behind this request.
61                // The page route is unusable regardless of which notification
62                // arrives first, so make the recovery state deterministic.
63                let mut topology = self.topology.lock().await;
64                topology.active_target_id = None;
65                topology.active_session_id = None;
66                topology.active_target_session_id = None;
67                topology.active_frame_id = None;
68                topology.frames.clear();
69                topology.frame_sessions.clear();
70                topology.frame_parents.clear();
71                self.cdp.set_active_target_route(None, None, None, None);
72                let _ = error;
73                return Err(TopologyError::new(
74                    TopologyErrorKind::NoTargetSelected,
75                    "no active target is selected; call listTargets to discover available pages",
76                )
77                .into());
78            }
79        };
80        let mut frames = Vec::new();
81        collect_frames(&raw["frameTree"], None, active.as_deref(), &mut frames)?;
82        let (attached_sessions, frame_parents) = {
83            let topology = self.topology.lock().await;
84            (
85                topology.frame_sessions.clone(),
86                topology.frame_parents.clone(),
87            )
88        };
89        let mut discovered_frame_sessions = Vec::new();
90        let mut stale_frame_sessions = HashSet::new();
91        let mut queried_sessions = HashSet::new();
92        for (attached_frame_id, session_id) in &attached_sessions {
93            if !queried_sessions.insert(session_id.clone()) {
94                continue;
95            }
96            let oopif_tree = match self
97                .cdp
98                .send_to_session(session_id, "Page.getFrameTree", None)
99                .await
100            {
101                Ok(tree) => tree,
102                Err(error) if error.code == -32_001 => {
103                    stale_frame_sessions.insert(session_id.clone());
104                    continue;
105                }
106                Err(error) => return Err(error.into()),
107            };
108            let start = frames.len();
109            collect_frames(
110                &oopif_tree["frameTree"],
111                frame_parents.get(attached_frame_id).map(String::as_str),
112                active.as_deref(),
113                &mut frames,
114            )?;
115            for frame in &frames[start..] {
116                discovered_frame_sessions.push((frame.id.clone(), session_id.clone()));
117            }
118            for frame in &mut frames[start..] {
119                frame.out_of_process = true;
120            }
121        }
122        let mut topology = self.topology.lock().await;
123        topology
124            .frame_sessions
125            .retain(|_, session| !stale_frame_sessions.contains(session));
126        for (frame_id, session_id) in discovered_frame_sessions {
127            topology.frame_sessions.insert(frame_id, session_id);
128        }
129        topology.frames = frames.clone();
130        Ok(frames)
131    }
132
133    /// Select a frame as the active context for subsequent operations.
134    ///
135    /// The frame must exist in the current target's frame tree. Selecting an
136    /// iframe creates an isolated execution context for JavaScript evaluation.
137    /// Invalidates the observation cache.
138    pub async fn select_frame(&self, frame_id: &str) -> BrowserResult<FrameInfo> {
139        validate_topology_id(frame_id)?;
140        let frame = self
141            .list_frames()
142            .await?
143            .into_iter()
144            .find(|frame| frame.id == frame_id)
145            .ok_or_else(|| {
146                TopologyError::new(
147                    TopologyErrorKind::NoSuchFrame,
148                    format!("frame {frame_id} was not found; call listFrames to discover available frames"),
149                )
150            })?;
151        let session_id = {
152            let topology = self.topology.lock().await;
153            topology
154                .frame_sessions
155                .get(frame_id)
156                .cloned()
157                .or_else(|| topology.active_target_session_id.clone())
158                .ok_or_else(|| {
159                    TopologyError::new(
160                        TopologyErrorKind::NoPageSession,
161                        "active target has no CDP session; the session may need to be re-established",
162                    )
163                })?
164        };
165        let context_id = if frame.parent_id.is_none() {
166            None
167        } else {
168            let world = self
169                .cdp
170                .send_to_session(
171                    &session_id,
172                    "Page.createIsolatedWorld",
173                    Some(serde_json::json!({"frameId": frame_id, "worldName":"glass"})),
174                )
175                .await?;
176            Some(
177                world["executionContextId"]
178                    .as_i64()
179                    .ok_or("Page.createIsolatedWorld returned no executionContextId")?,
180            )
181        };
182        self.cdp.set_active_route(
183            Some(session_id.clone()),
184            Some(frame_id.to_string()),
185            context_id,
186        );
187        {
188            let mut topology = self.topology.lock().await;
189            topology.active_frame_id = Some(frame_id.to_string());
190            topology.active_session_id = Some(session_id);
191        }
192        self.invalidate_observation();
193        Ok(FrameInfo {
194            active: true,
195            ..frame
196        })
197    }
198}