glass/browser/session/
target.rs1use super::*;
7
8impl BrowserSession {
9 pub async fn list_targets(&self) -> BrowserResult<Vec<PageTargetInfo>> {
14 let raw = self.cdp.send_browser("Target.getTargets", None).await?;
15 let active = self.topology.lock().await.active_target_id.clone();
16 let mut targets = Vec::new();
17 for info in raw["targetInfos"].as_array().into_iter().flatten() {
18 if info["type"].as_str() != Some("page") {
19 continue;
20 }
21 let Some(id) = info["targetId"].as_str() else {
22 continue;
23 };
24 validate_topology_id(id)?;
25 if targets.len() == TOPOLOGY_MAX_TARGETS {
26 return Err("page target limit exceeded".into());
27 }
28 targets.push(PageTargetInfo {
29 id: id.to_string(),
30 url: bounded_topology_text(info["url"].as_str().unwrap_or_default()),
31 title: bounded_topology_text(info["title"].as_str().unwrap_or_default()),
32 opener_id: retained_optional_topology_id(info["openerId"].as_str())?,
33 active: active.as_deref() == Some(id),
34 });
35 }
36 self.topology.lock().await.targets = targets.clone();
37 Ok(targets)
38 }
39
40 pub async fn topology_events(&self) -> Vec<TopologyEventSummary> {
45 self.topology.lock().await.events.iter().cloned().collect()
46 }
47
48 pub(crate) async fn route_identity(&self) -> BrowserResult<(String, String)> {
49 if let Some(identity) = self.cdp.operation_identity() {
50 return Ok(identity);
51 }
52
53 let topology = self.topology.lock().await;
54 let (kind, message) = match (
55 topology.active_target_id.as_ref(),
56 topology.active_session_id.as_ref(),
57 topology.active_frame_id.as_ref(),
58 ) {
59 (None, _, _) => (
60 TopologyErrorKind::NoTargetSelected,
61 "no active target is selected; call listTargets to discover available pages",
62 ),
63 (Some(_), None, _) => (
64 TopologyErrorKind::NoPageSession,
65 "active target has no CDP session; the session may need to be re-established",
66 ),
67 (Some(_), Some(_), None) => (
68 TopologyErrorKind::StaleFrame,
69 "active target has no selected frame; call listFrames and selectFrame",
70 ),
71 (Some(_), Some(_), Some(_)) => (
72 TopologyErrorKind::RoutingLost,
73 "active target/frame routing is unavailable; re-synchronize the session",
74 ),
75 };
76 Err(TopologyError::new(kind, message).into())
77 }
78
79 pub(crate) async fn ensured_route_identity(&self) -> BrowserResult<(String, String)> {
80 if let Ok(route) = self.route_identity().await {
81 return Ok(route);
82 }
83 let main_frame = self
84 .list_frames()
85 .await?
86 .into_iter()
87 .find(|frame| frame.parent_id.is_none())
88 .ok_or("active target returned no main frame")?;
89 self.select_frame(&main_frame.id).await?;
90 self.route_identity().await
91 }
92
93 pub async fn create_target(&self, url: &str) -> BrowserResult<PageTargetInfo> {
99 let url = normalize_url(url);
100 self.policy.require_url(&url).await?;
101 let result = self
102 .cdp
103 .send_browser("Target.createTarget", Some(serde_json::json!({"url": url})))
104 .await?;
105 let id = result["targetId"]
106 .as_str()
107 .ok_or("Target.createTarget returned no targetId")?;
108 validate_topology_id(id)?;
109 let targets = self.list_targets().await?;
110 let target = targets
111 .into_iter()
112 .find(|target| target.id == id)
113 .ok_or_else(|| -> Box<dyn Error> { "created target was not discoverable".into() })?;
114 self.record_audit("attach", &url);
115 Ok(target)
116 }
117
118 pub async fn select_target(&self, target_id: &str) -> BrowserResult<PageTargetInfo> {
123 validate_topology_id(target_id)?;
124 let target = self
125 .list_targets()
126 .await?
127 .into_iter()
128 .find(|target| target.id == target_id)
129 .ok_or("page target was not found")?;
130 let attached = self
131 .cdp
132 .send_browser(
133 "Target.attachToTarget",
134 Some(serde_json::json!({"targetId": target_id, "flatten": true})),
135 )
136 .await?;
137 let new_session = attached["sessionId"]
138 .as_str()
139 .ok_or("Target.attachToTarget returned no sessionId")?
140 .to_string();
141 let old_session = self.topology.lock().await.active_session_id.clone();
142 if let Err(error) = self.cdp.enable_observation_events_for(&new_session).await {
143 let _ = self
144 .cdp
145 .send_browser(
146 "Target.detachFromTarget",
147 Some(serde_json::json!({"sessionId": new_session})),
148 )
149 .await;
150 return Err(error.into());
151 }
152 if let Some(interception) = &self.policy_interception {
153 if let Err(error) = enable_fetch_for(&self.cdp, &new_session).await {
154 let _ = self
155 .cdp
156 .send_browser(
157 "Target.detachFromTarget",
158 Some(serde_json::json!({"sessionId": new_session})),
159 )
160 .await;
161 return Err(error);
162 }
163 interception
164 .sessions
165 .lock()
166 .await
167 .insert(new_session.clone());
168 }
169 if let Err(error) = self
170 .cdp
171 .send_to_session(
172 &new_session,
173 "Target.setAutoAttach",
174 Some(serde_json::json!({
175 "autoAttach": true,
176 "waitForDebuggerOnStart": matches!(
177 self.policy.preset(),
178 PolicyPreset::Hardened | PolicyPreset::UntrustedMcp
179 ),
180 "flatten": true
181 })),
182 )
183 .await
184 {
185 let _ = self
186 .cdp
187 .send_browser(
188 "Target.detachFromTarget",
189 Some(serde_json::json!({"sessionId": new_session})),
190 )
191 .await;
192 return Err(error.into());
193 }
194 let prepared = async {
195 let raw_frames = self
196 .cdp
197 .send_to_session(&new_session, "Page.getFrameTree", None)
198 .await?;
199 let mut frames = Vec::new();
200 collect_frames(&raw_frames["frameTree"], None, None, &mut frames)?;
201 let main_frame = frames
202 .iter()
203 .find(|frame| frame.parent_id.is_none())
204 .ok_or("selected target returned no main frame")?
205 .id
206 .clone();
207 Ok::<_, Box<dyn Error>>((frames, main_frame))
208 }
209 .await;
210 let (mut frames, main_frame) = match prepared {
211 Ok(prepared) => prepared,
212 Err(error) => {
213 let _ = self
214 .cdp
215 .send_browser(
216 "Target.detachFromTarget",
217 Some(serde_json::json!({"sessionId": new_session})),
218 )
219 .await;
220 return Err(error);
221 }
222 };
223 for frame in &mut frames {
224 frame.active = frame.id == main_frame;
225 }
226 {
227 let mut topology = self.topology.lock().await;
228 topology.active_target_id = Some(target_id.to_string());
229 topology.active_session_id = Some(new_session.clone());
230 topology.active_target_session_id = Some(new_session.clone());
231 topology.active_frame_id = Some(main_frame.clone());
232 topology.frames = frames;
233 }
234 self.cdp.set_active_target_route(
235 Some(target_id.to_string()),
236 Some(new_session.clone()),
237 Some(main_frame),
238 None,
239 );
240 if let Some(old_session) = old_session {
241 let _ = self
242 .cdp
243 .send_browser(
244 "Target.detachFromTarget",
245 Some(serde_json::json!({"sessionId": old_session})),
246 )
247 .await;
248 }
249 self.invalidate_observation();
250 Ok(PageTargetInfo {
251 active: true,
252 ..target
253 })
254 }
255
256 pub async fn close_target(&self, target_id: &str) -> BrowserResult<()> {
261 validate_topology_id(target_id)?;
262 let result = self
263 .cdp
264 .send_browser(
265 "Target.closeTarget",
266 Some(serde_json::json!({"targetId": target_id})),
267 )
268 .await?;
269 if result["success"].as_bool() != Some(true) {
270 return Err("Chrome refused to close target".into());
271 }
272 let mut topology = self.topology.lock().await;
273 if topology.active_target_id.as_deref() == Some(target_id) {
274 topology.active_target_id = None;
275 topology.active_session_id = None;
276 topology.active_target_session_id = None;
277 topology.active_frame_id = None;
278 topology.frames.clear();
279 topology.frame_sessions.clear();
280 topology.frame_parents.clear();
281 self.cdp.set_active_target_route(None, None, None, None);
282 }
283 topology.targets.retain(|target| target.id != target_id);
284 Ok(())
285 }
286}