use crate::api::{ConnectOptions, ConnectOverCdpOptions, LaunchOptions};
use crate::error::Result;
use crate::protocol::{Browser, BrowserContext, BrowserContextOptions};
use crate::server::channel::Channel;
use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
use crate::server::connection::{ConnectionExt, ConnectionLike};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::any::Any;
use std::sync::Arc;
#[derive(Clone)]
pub struct BrowserType {
base: ChannelOwnerImpl,
name: String,
executable_path: String,
}
impl BrowserType {
pub fn new(
parent: ParentOrConnection,
type_name: String,
guid: Arc<str>,
initializer: Value,
) -> Result<Self> {
let base = ChannelOwnerImpl::new(parent, type_name, guid, initializer.clone());
let name = initializer["name"]
.as_str()
.ok_or_else(|| {
crate::error::Error::ProtocolError(
"BrowserType initializer missing 'name'".to_string(),
)
})?
.to_string();
let executable_path = initializer["executablePath"]
.as_str()
.unwrap_or_default() .to_string();
Ok(Self {
base,
name,
executable_path,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn executable_path(&self) -> &str {
&self.executable_path
}
pub async fn launch(&self) -> Result<Browser> {
self.launch_with_options(LaunchOptions::default()).await
}
pub async fn launch_with_options(&self, options: LaunchOptions) -> Result<Browser> {
let options = {
#[cfg(windows)]
{
let mut options = options;
let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
if is_ci {
tracing::debug!(
"[playwright-rust] Detected Windows CI environment, adding stability flags"
);
let mut args = options.args.unwrap_or_default();
let ci_flags = vec![
"--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process", ];
for flag in ci_flags {
if !args.iter().any(|a| a == flag) {
args.push(flag.to_string());
}
}
options.args = Some(args);
if options.timeout.is_none() {
options.timeout = Some(60000.0); }
}
options
}
#[cfg(not(windows))]
{
options
}
};
let params = options.normalize();
let response: LaunchResponse = self.base.channel().send("launch", params).await?;
let browser: Browser = self
.connection()
.get_typed::<Browser>(&response.browser.guid)
.await?;
Ok(browser)
}
pub async fn launch_persistent_context(
&self,
user_data_dir: impl Into<String>,
) -> Result<BrowserContext> {
self.launch_persistent_context_with_options(user_data_dir, BrowserContextOptions::default())
.await
}
pub async fn launch_persistent_context_with_options(
&self,
user_data_dir: impl Into<String>,
mut options: BrowserContextOptions,
) -> Result<BrowserContext> {
#[cfg(windows)]
{
let is_ci = std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok();
if is_ci {
tracing::debug!(
"[playwright-rust] Detected Windows CI environment, adding stability flags"
);
let mut args = options.args.unwrap_or_default();
let ci_flags = vec![
"--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process", ];
for flag in ci_flags {
if !args.iter().any(|a| a == flag) {
args.push(flag.to_string());
}
}
options.args = Some(args);
if options.timeout.is_none() {
options.timeout = Some(60000.0); }
}
}
if let Some(path) = &options.storage_state_path {
let file_content = tokio::fs::read_to_string(path).await.map_err(|e| {
crate::error::Error::ProtocolError(format!(
"Failed to read storage state file '{}': {}",
path, e
))
})?;
let storage_state: crate::protocol::StorageState = serde_json::from_str(&file_content)
.map_err(|e| {
crate::error::Error::ProtocolError(format!(
"Failed to parse storage state file '{}': {}",
path, e
))
})?;
options.storage_state = Some(storage_state);
options.storage_state_path = None; }
let mut params = serde_json::to_value(&options).map_err(|e| {
crate::error::Error::ProtocolError(format!(
"Failed to serialize context options: {}",
e
))
})?;
params["userDataDir"] = serde_json::json!(user_data_dir.into());
if params.get("timeout").is_none() {
params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
}
if let Some(ignore) = params.get("ignoreDefaultArgs")
&& let Some(b) = ignore.as_bool()
{
if b {
params["ignoreAllDefaultArgs"] = serde_json::json!(true);
}
params
.as_object_mut()
.expect("params is a JSON object")
.remove("ignoreDefaultArgs");
}
let response: LaunchPersistentContextResponse = self
.base
.channel()
.send("launchPersistentContext", params)
.await?;
let context: BrowserContext = self
.connection()
.get_typed::<BrowserContext>(&response.context.guid)
.await?;
let selectors = self.connection().selectors();
if let Err(e) = selectors.add_context(context.channel().clone()).await {
tracing::warn!("Failed to register BrowserContext with Selectors: {}", e);
}
Ok(context)
}
pub async fn connect(
&self,
ws_endpoint: &str,
options: Option<ConnectOptions>,
) -> Result<Browser> {
use crate::server::connection::Connection;
use crate::server::transport::WebSocketTransport;
let options = options.unwrap_or_default();
let timeout_ms = options.timeout.unwrap_or(30000.0);
tracing::debug!("Connecting to remote browser at {}", ws_endpoint);
let connect_future = WebSocketTransport::connect(ws_endpoint, options.headers);
let (transport, message_rx) = if timeout_ms > 0.0 {
let timeout = std::time::Duration::from_millis(timeout_ms as u64);
tokio::time::timeout(timeout, connect_future)
.await
.map_err(|_| {
crate::error::Error::Timeout(format!(
"Connection to {} timed out after {} ms",
ws_endpoint, timeout_ms
))
})??
} else {
connect_future.await?
};
let (sender, receiver) = transport.into_parts();
let connection = Arc::new(Connection::new(sender, receiver, message_rx));
let conn_for_loop = Arc::clone(&connection);
tokio::spawn(async move {
conn_for_loop.run().await;
});
let playwright_obj = connection.initialize_playwright().await?;
let initializer = playwright_obj.initializer();
let browser_guid = initializer["preLaunchedBrowser"]["guid"]
.as_str()
.ok_or_else(|| {
crate::error::Error::ProtocolError(
"Remote server did not return a pre-launched browser. Ensure server was launched in server mode.".to_string()
)
})?;
let browser: Browser = connection.get_typed::<Browser>(browser_guid).await?;
Ok(browser)
}
pub async fn connect_over_cdp(
&self,
endpoint_url: &str,
options: Option<ConnectOverCdpOptions>,
) -> Result<Browser> {
if self.name() != "chromium" {
return Err(crate::error::Error::ProtocolError(
"Connecting over CDP is only supported in Chromium.".to_string(),
));
}
let options = options.unwrap_or_default();
let headers_array = options.headers.map(|h| {
h.into_iter()
.map(|(name, value)| HeaderEntry { name, value })
.collect::<Vec<_>>()
});
let params = ConnectOverCdpParams {
endpoint_url: endpoint_url.to_string(),
headers: headers_array,
slow_mo: options.slow_mo,
timeout: options.timeout.unwrap_or(crate::DEFAULT_TIMEOUT_MS),
};
let response: ConnectOverCdpResponse =
self.base.channel().send("connectOverCDP", params).await?;
let browser: Browser = self
.connection()
.get_typed::<Browser>(&response.browser.guid)
.await?;
Ok(browser)
}
}
#[derive(Debug, Deserialize, Serialize)]
struct LaunchResponse {
browser: BrowserRef,
}
#[derive(Debug, Deserialize, Serialize)]
struct LaunchPersistentContextResponse {
context: ContextRef,
}
#[derive(Debug, Deserialize, Serialize)]
struct BrowserRef {
#[serde(
serialize_with = "crate::server::connection::serialize_arc_str",
deserialize_with = "crate::server::connection::deserialize_arc_str"
)]
guid: Arc<str>,
}
#[derive(Debug, Deserialize, Serialize)]
struct ContextRef {
#[serde(
serialize_with = "crate::server::connection::serialize_arc_str",
deserialize_with = "crate::server::connection::deserialize_arc_str"
)]
guid: Arc<str>,
}
#[derive(Debug, Serialize)]
struct ConnectOverCdpParams {
#[serde(rename = "endpointURL")]
endpoint_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<Vec<HeaderEntry>>,
#[serde(rename = "slowMo", skip_serializing_if = "Option::is_none")]
slow_mo: Option<f64>,
timeout: f64,
}
#[derive(Debug, Serialize)]
struct HeaderEntry {
name: String,
value: String,
}
#[derive(Debug, Deserialize)]
struct ConnectOverCdpResponse {
browser: BrowserRef,
#[serde(rename = "defaultContext")]
#[allow(dead_code)]
default_context: Option<ContextRef>,
}
impl ChannelOwner for BrowserType {
fn guid(&self) -> &str {
self.base.guid()
}
fn type_name(&self) -> &str {
self.base.type_name()
}
fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
self.base.parent()
}
fn connection(&self) -> Arc<dyn ConnectionLike> {
self.base.connection()
}
fn initializer(&self) -> &Value {
self.base.initializer()
}
fn channel(&self) -> &Channel {
self.base.channel()
}
fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
self.base.dispose(reason)
}
fn adopt(&self, child: Arc<dyn ChannelOwner>) {
self.base.adopt(child)
}
fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
self.base.add_child(guid, child)
}
fn remove_child(&self, guid: &str) {
self.base.remove_child(guid)
}
fn on_event(&self, method: &str, params: Value) {
self.base.on_event(method, params)
}
fn was_collected(&self) -> bool {
self.base.was_collected()
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl std::fmt::Debug for BrowserType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BrowserType")
.field("guid", &self.guid())
.field("name", &self.name)
.field("executable_path", &self.executable_path)
.finish()
}
}