use super::*;
impl BrowserSession {
pub async fn list_targets(&self) -> BrowserResult<Vec<PageTargetInfo>> {
let raw = self.cdp.send_browser("Target.getTargets", None).await?;
let active = self.topology.lock().await.active_target_id.clone();
let mut targets = Vec::new();
for info in raw["targetInfos"].as_array().into_iter().flatten() {
if info["type"].as_str() != Some("page") {
continue;
}
let Some(id) = info["targetId"].as_str() else {
continue;
};
validate_topology_id(id)?;
if targets.len() == TOPOLOGY_MAX_TARGETS {
return Err("page target limit exceeded".into());
}
targets.push(PageTargetInfo {
id: id.to_string(),
url: bounded_topology_text(info["url"].as_str().unwrap_or_default()),
title: bounded_topology_text(info["title"].as_str().unwrap_or_default()),
opener_id: retained_optional_topology_id(info["openerId"].as_str())?,
active: active.as_deref() == Some(id),
});
}
self.topology.lock().await.targets = targets.clone();
Ok(targets)
}
pub async fn topology_events(&self) -> Vec<TopologyEventSummary> {
self.topology.lock().await.events.iter().cloned().collect()
}
pub(crate) async fn route_identity(&self) -> BrowserResult<(String, String)> {
if let Some(identity) = self.cdp.operation_identity() {
return Ok(identity);
}
let topology = self.topology.lock().await;
let (kind, message) = match (
topology.active_target_id.as_ref(),
topology.active_session_id.as_ref(),
topology.active_frame_id.as_ref(),
) {
(None, _, _) => (
TopologyErrorKind::NoTargetSelected,
"no active target is selected; call listTargets to discover available pages",
),
(Some(_), None, _) => (
TopologyErrorKind::NoPageSession,
"active target has no CDP session; the session may need to be re-established",
),
(Some(_), Some(_), None) => (
TopologyErrorKind::StaleFrame,
"active target has no selected frame; call listFrames and selectFrame",
),
(Some(_), Some(_), Some(_)) => (
TopologyErrorKind::RoutingLost,
"active target/frame routing is unavailable; re-synchronize the session",
),
};
Err(TopologyError::new(kind, message).into())
}
pub(crate) async fn ensured_route_identity(&self) -> BrowserResult<(String, String)> {
if let Ok(route) = self.route_identity().await {
return Ok(route);
}
let main_frame = self
.list_frames()
.await?
.into_iter()
.find(|frame| frame.parent_id.is_none())
.ok_or("active target returned no main frame")?;
self.select_frame(&main_frame.id).await?;
self.route_identity().await
}
pub async fn create_target(&self, url: &str) -> BrowserResult<PageTargetInfo> {
let url = normalize_url(url);
self.policy.require_url(&url).await?;
let result = self
.cdp
.send_browser("Target.createTarget", Some(serde_json::json!({"url": url})))
.await?;
let id = result["targetId"]
.as_str()
.ok_or("Target.createTarget returned no targetId")?;
validate_topology_id(id)?;
let targets = self.list_targets().await?;
let target = targets
.into_iter()
.find(|target| target.id == id)
.ok_or_else(|| -> Box<dyn Error> { "created target was not discoverable".into() })?;
self.record_audit("attach", &url);
Ok(target)
}
pub async fn select_target(&self, target_id: &str) -> BrowserResult<PageTargetInfo> {
validate_topology_id(target_id)?;
let target = self
.list_targets()
.await?
.into_iter()
.find(|target| target.id == target_id)
.ok_or("page target was not found")?;
let attached = self
.cdp
.send_browser(
"Target.attachToTarget",
Some(serde_json::json!({"targetId": target_id, "flatten": true})),
)
.await?;
let new_session = attached["sessionId"]
.as_str()
.ok_or("Target.attachToTarget returned no sessionId")?
.to_string();
let old_session = self.topology.lock().await.active_session_id.clone();
if let Err(error) = self.cdp.enable_observation_events_for(&new_session).await {
let _ = self
.cdp
.send_browser(
"Target.detachFromTarget",
Some(serde_json::json!({"sessionId": new_session})),
)
.await;
return Err(error.into());
}
if let Some(interception) = &self.policy_interception {
if let Err(error) = enable_fetch_for(&self.cdp, &new_session).await {
let _ = self
.cdp
.send_browser(
"Target.detachFromTarget",
Some(serde_json::json!({"sessionId": new_session})),
)
.await;
return Err(error);
}
interception
.sessions
.lock()
.await
.insert(new_session.clone());
}
if let Err(error) = self
.cdp
.send_to_session(
&new_session,
"Target.setAutoAttach",
Some(serde_json::json!({
"autoAttach": true,
"waitForDebuggerOnStart": matches!(
self.policy.preset(),
PolicyPreset::Hardened | PolicyPreset::UntrustedMcp
),
"flatten": true
})),
)
.await
{
let _ = self
.cdp
.send_browser(
"Target.detachFromTarget",
Some(serde_json::json!({"sessionId": new_session})),
)
.await;
return Err(error.into());
}
let prepared = async {
let raw_frames = self
.cdp
.send_to_session(&new_session, "Page.getFrameTree", None)
.await?;
let mut frames = Vec::new();
collect_frames(&raw_frames["frameTree"], None, None, &mut frames)?;
let main_frame = frames
.iter()
.find(|frame| frame.parent_id.is_none())
.ok_or("selected target returned no main frame")?
.id
.clone();
Ok::<_, Box<dyn Error>>((frames, main_frame))
}
.await;
let (mut frames, main_frame) = match prepared {
Ok(prepared) => prepared,
Err(error) => {
let _ = self
.cdp
.send_browser(
"Target.detachFromTarget",
Some(serde_json::json!({"sessionId": new_session})),
)
.await;
return Err(error);
}
};
for frame in &mut frames {
frame.active = frame.id == main_frame;
}
{
let mut topology = self.topology.lock().await;
topology.active_target_id = Some(target_id.to_string());
topology.active_session_id = Some(new_session.clone());
topology.active_target_session_id = Some(new_session.clone());
topology.active_frame_id = Some(main_frame.clone());
topology.frames = frames;
}
self.cdp.set_active_target_route(
Some(target_id.to_string()),
Some(new_session.clone()),
Some(main_frame),
None,
);
if let Some(old_session) = old_session {
let _ = self
.cdp
.send_browser(
"Target.detachFromTarget",
Some(serde_json::json!({"sessionId": old_session})),
)
.await;
}
self.invalidate_observation();
Ok(PageTargetInfo {
active: true,
..target
})
}
pub async fn close_target(&self, target_id: &str) -> BrowserResult<()> {
validate_topology_id(target_id)?;
let result = self
.cdp
.send_browser(
"Target.closeTarget",
Some(serde_json::json!({"targetId": target_id})),
)
.await?;
if result["success"].as_bool() != Some(true) {
return Err("Chrome refused to close target".into());
}
let mut topology = self.topology.lock().await;
if topology.active_target_id.as_deref() == Some(target_id) {
topology.active_target_id = None;
topology.active_session_id = None;
topology.active_target_session_id = None;
topology.active_frame_id = None;
topology.frames.clear();
topology.frame_sessions.clear();
topology.frame_parents.clear();
self.cdp.set_active_target_route(None, None, None, None);
}
topology.targets.retain(|target| target.id != target_id);
Ok(())
}
}