Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ pub fn get_port_for_session(session: &str) -> u16 {
49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)
}

/// Read the actual daemon port from the `.port` file written by the daemon.
/// Falls back to the hash-derived port if the file does not exist or is
/// unreadable (e.g. daemon has not started yet).
#[cfg(windows)]
pub fn resolve_port(session: &str) -> u16 {
let port_path = get_port_path(session);
fs::read_to_string(&port_path)
.ok()
.and_then(|s| s.trim().parse::<u16>().ok())
.unwrap_or_else(|| get_port_for_session(session))
}

pub fn daemon_ready(session: &str) -> bool {
#[cfg(unix)]
{
Expand All @@ -162,7 +174,7 @@ pub fn daemon_ready(session: &str) -> bool {
}
#[cfg(windows)]
{
let port = get_port_for_session(session);
let port = resolve_port(session);
TcpStream::connect_timeout(
&format!("127.0.0.1:{}", port).parse().unwrap(),
Duration::from_millis(50),
Expand Down Expand Up @@ -442,7 +454,7 @@ pub fn ensure_daemon(session: &str, opts: &DaemonOptions) -> Result<DaemonResult
get_socket_dir().join(format!("{}.sock", session)).display()
);
#[cfg(windows)]
let endpoint_info = format!("port: 127.0.0.1:{}", get_port_for_session(session));
let endpoint_info = format!("port: 127.0.0.1:{}", resolve_port(session));

Err(format!("Daemon failed to start ({})", endpoint_info))
}
Expand All @@ -457,7 +469,7 @@ fn connect(session: &str) -> Result<Connection, String> {
}
#[cfg(windows)]
{
let port = get_port_for_session(session);
let port = resolve_port(session);
TcpStream::connect(format!("127.0.0.1:{}", port))
.map(Connection::Tcp)
.map_err(|e| format!("Failed to connect: {}", e))
Expand Down
42 changes: 42 additions & 0 deletions cli/src/native/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,48 @@ async fn handle_snapshot(cmd: &Value, state: &mut DaemonState) -> Result<Value,
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
let session_id = mgr.active_session_id()?.to_string();

// Proactively attach to any iframe targets not yet tracked in iframe_sessions.
// This handles the case where Target.attachedToTarget events arrived before
// the daemon was ready, or were missed for any reason.
let targets_result: Result<Value, String> = mgr
.client
.send_command("Target.getTargets", None, None)
.await;
if let Ok(targets_value) = targets_result {
if let Some(target_infos) = targets_value.get("targetInfos").and_then(|v| v.as_array()) {
let mut to_attach: Vec<String> = Vec::new();
for target_info in target_infos {
let target_type = target_info
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("");
if target_type == "iframe" {
if let Some(tid) = target_info.get("targetId").and_then(|v| v.as_str()) {
if !state.iframe_sessions.contains_key(tid) {
to_attach.push(tid.to_string());
}
}
}
}
// Drop the immutable borrow of state.browser before mutating state.iframe_sessions
let client = mgr.client.clone();
for tid in to_attach {
let attach_result: Result<Value, String> = client
.send_command(
"Target.attachToTarget",
Some(serde_json::json!({ "targetId": tid, "flatten": true })),
None,
)
.await;
if let Ok(resp) = attach_result {
if let Some(iframe_sid) = resp.get("sessionId").and_then(|v| v.as_str()) {
state.iframe_sessions.insert(tid, iframe_sid.to_string());
}
}
}
}
}

let options = SnapshotOptions {
selector: cmd
.get("selector")
Expand Down
36 changes: 30 additions & 6 deletions cli/src/native/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,20 @@ pub async fn run_daemon(session: &str) {
let pid_path = socket_dir.join(format!("{}.pid", session));
let _ = fs::write(&pid_path, process::id().to_string());

// On Unix the daemon listens on a Unix domain socket; on Windows it uses
// TCP, so there is no .sock file — only a .port file written by the server.
let socket_path = socket_dir.join(format!("{}.sock", session));

#[cfg(unix)]
if socket_path.exists() {
let _ = fs::remove_file(&socket_path);
}

#[cfg(windows)]
{
let _ = fs::remove_file(socket_dir.join(format!("{}.port", session)));
}

let stream_path = socket_dir.join(format!("{}.stream", session));
let _ = fs::remove_file(&stream_path);
let _ = fs::remove_file(socket_dir.join(format!("{}.engine", session)));
Expand Down Expand Up @@ -79,7 +87,14 @@ pub async fn run_daemon(session: &str) {
)
.await;

let _ = fs::remove_file(&socket_path);
#[cfg(unix)]
{
let _ = fs::remove_file(&socket_path);
}
#[cfg(windows)]
{
let _ = fs::remove_file(socket_dir.join(format!("{}.port", session)));
}
let _ = fs::remove_file(&pid_path);
let _ = fs::remove_file(&stream_path);
let _ = fs::remove_file(socket_dir.join(format!("{}.engine", session)));
Expand Down Expand Up @@ -206,14 +221,23 @@ async fn run_socket_server(
) -> Result<(), String> {
use tokio::net::TcpListener;

let port = get_port_for_session(session);
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.map_err(|e| format!("Failed to bind TCP: {}", e))?;
let preferred_port = get_port_for_session(session);
// Try the hash-derived port first; if it is blocked (e.g. Windows Hyper-V
// excluded port range), fall back to an OS-assigned ephemeral port.
let listener = match TcpListener::bind(format!("127.0.0.1:{}", preferred_port)).await {
Ok(l) => l,
Err(_) => TcpListener::bind("127.0.0.1:0")
.await
.map_err(|e| format!("Failed to bind TCP: {}", e))?,
};
let actual_port = listener
.local_addr()
.map_err(|e| format!("Failed to get local address: {}", e))?
.port();

let socket_dir = socket_path.parent().unwrap_or(std::path::Path::new("."));
let port_path = socket_dir.join(format!("{}.port", session));
let _ = fs::write(&port_path, port.to_string());
let _ = fs::write(&port_path, actual_port.to_string());

let stream_file: Option<PathBuf> = if stream_server.is_some() {
Some(socket_dir.join(format!("{}.stream", session)))
Expand Down
37 changes: 33 additions & 4 deletions cli/src/native/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,15 +402,20 @@ pub async fn take_snapshot(
continue;
};
let ref_id = node.ref_id.as_deref().unwrap_or("");
if let Ok(child_fid) = resolve_iframe_frame_id(client, session_id, bid).await {
// Snapshot the child frame; errors are silently ignored
// (e.g. cross-origin iframes)

// Try to resolve iframe frame ID (works for same-origin iframes).
// For cross-origin iframes, contentDocument is null due to the
// same-origin policy, so resolve_iframe_frame_id will fail.
let child_fid = resolve_iframe_frame_id(client, session_id, bid).await.ok();

if let Some(fid) = child_fid {
// Same-origin iframe: snapshot using the parent session + frameId.
if let Ok(child_text) = Box::pin(take_snapshot(
client,
session_id,
options,
ref_map,
Some(&child_fid),
Some(&fid),
iframe_sessions,
))
.await
Expand All @@ -422,6 +427,30 @@ pub async fn take_snapshot(
iframe_snapshots.push((ref_id.to_string(), child_text));
}
}
} else {
// Cross-origin iframe: try each dedicated iframe session.
// Each cross-origin iframe has its own session in iframe_sessions;
// we snapshot without a frameId since the session is already scoped.
for (_frame_id, iframe_sid) in iframe_sessions.iter() {
if let Ok(child_text) = Box::pin(take_snapshot(
client,
iframe_sid,
options,
ref_map,
None,
iframe_sessions,
))
.await
{
if !child_text.is_empty()
&& child_text != "(empty page)"
&& child_text != "(no interactive elements)"
{
iframe_snapshots.push((ref_id.to_string(), child_text));
break;
}
}
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions cli/src/native/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use tokio::sync::{broadcast, watch, Mutex, Notify, RwLock};
use tokio_tungstenite::tungstenite::Message;

use super::cdp::client::CdpClient;
#[cfg(windows)]
use crate::connection::get_port_for_session;
use crate::connection::get_socket_dir;
#[cfg(windows)]
use crate::connection::resolve_port;
use crate::install::get_dashboard_dir;

/// Frame metadata from CDP Page.screencastFrame events.
Expand Down Expand Up @@ -1213,7 +1213,7 @@ async fn relay_command_to_daemon(session_name: &str, body: &str) -> Result<Strin

#[cfg(windows)]
let stream = {
let port = get_port_for_session(session_name);
let port = resolve_port(session_name);
tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
.await
.map_err(|e| format!("Failed to connect to daemon: {}", e))?
Expand Down