Skip to content

Commit

Permalink
Merge commit '454e1947a6d6387567ca4490022aa0de2c3c8125'
Browse files Browse the repository at this point in the history
#Conflicts:
#	R/chromote.R
#	R/chromote_session.R
#	man/ChromoteSession.Rd
  • Loading branch information
hadley committed Jan 31, 2024
2 parents e2a7a95 + 454e194 commit 2fd9552
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 124 deletions.
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# chromote (development version)

* `--disable-gpu` is no longer included in the default Chrome arguments.

* `ChromoteSession` now records the `targetId`. This eliminates one round-trip to the browser when viewing or closing a session. You can now call the `$respawn()` method if a session terminates and you want to reconnect to the same target (#94).

* `ChromoteSession$screenshot()` gains an `options` argument that accepts a list of additional options to be passed to the Chrome Devtools Protocol's [`Page.captureScreenshot` method](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot). (#129)

* `ChromoteSession$screenshot()` will now infer the image format from the `filename` extension. Alternatively, you can specify the `format` in the list passed to `options`. (#130)
Expand Down
2 changes: 2 additions & 0 deletions R/browser.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ globals <- new.env()

#' Browser base class
#'
#' @description
#' Base class for browsers like Chrome, Chromium, etc. Defines the interface
#' used by various browser implementations. It can represent a local browser
#' process or one running remotely.
#'
#' @details
#' The \code{initialize()} method of an implementation should set private$host
#' and private$port. If the process is local, the \code{initialize()} method
#' should also set private$process.
Expand Down
4 changes: 3 additions & 1 deletion R/chrome.R
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#' Local Chrome process
#'
#' @description
#' This is a subclass of [`Browser`] that represents a local browser. It extends
#' the [`Browser`] class with a [`processx::process`] object, which represents
#' the browser's system process.
Expand Down Expand Up @@ -231,7 +232,8 @@ launch_chrome_impl <- function(path, args, port) {

#' Remote Chrome process
#'
#'
#' @description
#' Remote Chrome process
#'
#' @export
ChromeRemote <- R6Class("ChromeRemote",
Expand Down
87 changes: 45 additions & 42 deletions R/chromote.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#' Chromote class
#'
#' This class represents the browser as a whole.
#'
#' @description
#' A `Chromote` object represents the browser as a whole, and it can have
#' multiple _targets_, which each represent a browser tab. In the Chrome
#' DevTools Protocol, each target can have one or more debugging _sessions_ to
Expand Down Expand Up @@ -38,17 +37,42 @@ Chromote <- R6Class(
private$auto_events <- auto_events
private$multi_session <- multi_session

private$command_callbacks <- fastmap()

# Use a private event loop to drive the websocket
private$child_loop <- create_loop(parent = current_loop())

p <- self$connect(multi_session = multi_session, wait_ = FALSE)

# Populate methods while the connection is being established.
protocol_spec <- jsonlite::fromJSON(self$url("/json/protocol"), simplifyVector = FALSE)
self$protocol <- process_protocol(protocol_spec, self$.__enclos_env__)
lockBinding("protocol", self)
# self$protocol is a list of domains, each of which is a list of
# methods. Graft the entries from self$protocol onto self
list2env(self$protocol, self)

private$event_manager <- EventManager$new(self)
private$is_active_ <- TRUE

self$wait_for(p)

private$register_default_event_listeners()
},

#' @description Re-connect the websocket to the browser. The Chrome browser
#' automatically closes websockets when your computer goes to sleep;
#' you can use this to bring it back to life with a new connection.
#' @param multi_session Should multiple sessions be allowed?
#' @param wait_ If `FALSE`, return a promise; if `TRUE` wait until
#' connection is complete.
connect = function(multi_session = TRUE, wait_ = TRUE) {
if (multi_session) {
chrome_info <- fromJSON(self$url("/json/version"))
} else {
chrome_info <- fromJSON(self$url("/json"))
}

private$command_callbacks <- fastmap()

# Use a private event loop to drive the websocket
private$child_loop <- create_loop(parent = current_loop())

with_loop(private$child_loop, {
private$ws <- WebSocket$new(
chrome_info$webSocketDebuggerUrl,
Expand Down Expand Up @@ -81,23 +105,13 @@ Chromote <- R6Class(
})

private$ws$connect()

# Populate methods while the connection is being established.
protocol_spec <- jsonlite::fromJSON(self$url("/json/protocol"), simplifyVector = FALSE)
self$protocol <- process_protocol(protocol_spec, self$.__enclos_env__)
lockBinding("protocol", self)

# self$protocol is a list of domains, each of which is a list of
# methods. Graft the entries from self$protocol onto self
list2env(self$protocol, self)

private$event_manager <- EventManager$new(self)
private$is_active_ <- TRUE

self$wait_for(p)

private$register_default_event_listeners()
})

if (wait_) {
invisible(self$wait_for(p))
} else {
p
}
},

#' @description Display the current session in the `browser`
Expand Down Expand Up @@ -161,19 +175,13 @@ Chromote <- R6Class(
#' return a `ChromoteSession` object directly.
new_session = function(width = 992, height = 1323, targetId = NULL, wait_ = TRUE) {
self$check_alive()
session <- ChromoteSession$new(self, width, height, targetId, wait_ = FALSE)

# ChromoteSession$new() always returns the object, but the
# initialization is async. To properly wait for initialization, we
# need to call b$init_promise() to get the promise; it resolves
# after initialization is complete.
p <- session$init_promise()

if (wait_) {
self$wait_for(p)
} else {
p
}
create_session(
chromote = self,
width = width,
height = height,
targetId = targetId,
wait_ = wait_
)
},

#' @description Retrieve all [`ChromoteSession`] objects
Expand Down Expand Up @@ -570,8 +578,6 @@ is_missing_linux_user <- cache_value(function() {
#' Default chromote arguments are composed of the following values (when
#' appropriate):
#'
#' * [`"--disable-gpu"`](https://peter.sh/experiments/chromium-command-line-switches/#disable-gpu)
#' * \verb{Disables GPU hardware acceleration. If software renderer is not in place, then the GPU process won't launch.}
#' * [`"--no-sandbox"`](https://peter.sh/experiments/chromium-command-line-switches/#no-sandbox)
#' * Only added when `CI` system environment variable is set, when the
#' user on a Linux system is not set, or when executing inside a Docker container.
Expand All @@ -598,9 +604,6 @@ is_missing_linux_user <- cache_value(function() {
#' @export
default_chrome_args <- function() {
c(
# Better cross platform support
"--disable-gpu",

# > Note: --no-sandbox is not needed if you properly setup a user in the container.
# https://developers.google.com/web/updates/2017/04/headless-chrome
if (is_inside_ci() || is_missing_linux_user() || is_inside_docker()) {
Expand Down Expand Up @@ -652,7 +655,7 @@ reset_chrome_args <- function() {
#' @examples
#' old_chrome_args <- get_chrome_args()
#'
#' # Only disable the gpu and using `/dev/shm`
#' # Disable the gpu and use `/dev/shm`
#' set_chrome_args(c("--disable-gpu", "--disable-dev-shm-usage"))
#'
#' #... Make new `Chrome` or `ChromoteSession` instance
Expand Down
115 changes: 78 additions & 37 deletions R/chromote_session.R
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
# This represents one _session_ in a Chromote object. Note that in the Chrome
# DevTools Protocol a session is a debugging interface connected to a
# _target_; a target is a browser window/tab, or an iframe. A single target
# can have more than one session connected to it.

#' ChromoteSession class
#'
#' @description
#' This represents one _session_ in a Chromote object. Note that in the Chrome
#' DevTools Protocol a session is a debugging session connected to a _target_,
#' which is a browser window/tab or an iframe.
#'
#' A single target can potentially have more than one session connected to it,
#' but this is not currently supported by chromote.
#'
#' @export
#' @param timeout_ Number of seconds for \pkg{chromote} to wait for a Chrome
#' DevTools Protocol response. If `timeout_` is [`rlang::missing_arg()`] and
#' `timeout` is provided, `timeout_` will be set to `2 * timeout / 1000`.
#' @param timeout Number of milliseconds for Chrome DevTools Protocol execute a
#' method.
#' @param width Width, in pixels, of the `Target` to create if `targetId` is
#' `NULL`
#' @param height Height, in pixels, of the `Target` to create if `targetId` is
#' `NULL`
#' @param targetId
#' [Target](https://chromedevtools.github.io/devtools-protocol/tot/Target/)
#' ID of an existing target to attach to. When a `targetId` is provided, the
Expand Down Expand Up @@ -49,6 +44,7 @@ ChromoteSession <- R6Class(
#' from the parent `Chromote` object. If `TRUE`, enable automatic
#' event enabling/disabling; if `FALSE`, disable automatic event
#' enabling/disabling.
#' @param width,height Width and height of the new window.
#' @param wait_ If `FALSE`, return a [promises::promise()] of a new
#' `ChromoteSession` object. Otherwise, block during initialization, and
#' return a `ChromoteSession` object directly.
Expand Down Expand Up @@ -76,9 +72,11 @@ ChromoteSession <- R6Class(
wait_ = FALSE
)$
then(function(value) {
private$target_id <- value$targetId
parent$Target$attachToTarget(value$targetId, flatten = TRUE, wait_ = FALSE)
})
} else {
private$target_id <- targetId
p <- parent$Target$attachToTarget(targetId, flatten = TRUE, wait_ = FALSE)
}

Expand Down Expand Up @@ -131,7 +129,7 @@ ChromoteSession <- R6Class(
# returning p, because the call to ChromoteSession$new() always
# returns the new object. Instead, we'll store it as
# private$init_promise_, and the user can retrieve it with
# b$init_promise().
# b$get_init_promise().
private$init_promise_ <- p$then(function(value) self)
}

Expand All @@ -157,11 +155,9 @@ ChromoteSession <- R6Class(
#' if (interactive()) b$view()
#' ```
view = function() {
tid <- self$Target$getTargetInfo()$targetInfo$targetId

# A data frame of targets, one row per target.
info <- fromJSON(self$parent$url("/json"))
path <- info$devtoolsFrontendUrl[info$id == tid]
path <- info$devtoolsFrontendUrl[info$id == private$target_id]
if (length(path) == 0) {
stop("Target info not found.")
}
Expand Down Expand Up @@ -192,15 +188,12 @@ ChromoteSession <- R6Class(
return(invisible())
}

p <- self$Target$getTargetInfo(wait_ = FALSE)
p <- p$then(function(target) {
tid <- target$targetInfo$targetId
# Even if this session calls Target.closeTarget, the response from
# the browser is sent without a sessionId. In order to wait for the
# correct browser response, we need to invoke this from the parent's
# browser-level methods.
self$parent$protocol$Target$closeTarget(tid, wait_ = FALSE)
})
# Even if this session calls Target.closeTarget, the response from
# the browser is sent without a sessionId. In order to wait for the
# correct browser response, we need to invoke this from the parent's
# browser-level methods.
p <- self$parent$protocol$Target$closeTarget(private$target_id, wait_ = FALSE)

p <- p$then(function(value) {
if (isTRUE(value$success)) {
self$mark_closed()
Expand Down Expand Up @@ -420,23 +413,43 @@ ChromoteSession <- R6Class(
#' when the `ChromoteSession` has created a new session. Otherwise, block
#' until the `ChromoteSession` has created a new session.
new_session = function(width = 992, height = 1323, targetId = NULL, wait_ = TRUE) {
self$parent$new_session(width = width, height = height, targetId = targetId, wait_ = wait_)
create_session(
chromote = self$parent,
width = width,
height = height,
targetId = targetId,
wait_ = wait_
)
},

#' @description
#' Retrieve the session id
#'
#' ## Examples
#'
#' ```r
#' b <- ChromoteSession$new()
#' b$get_session_id()
#' #> [1] "05764F1D439F4292497A21C6526575DA"
#' ```
get_session_id = function() {
private$session_id
},

#' @description
#' Create a new session that connects to the same target (i.e. page)
#' as this session. This is useful if the session has been closed but the target still
#' exists.
respawn = function() {
if (!private$is_active_) {
stop("Can't respawn session; target has been closed.")
}

create_session(
chromote = self$parent,
targetId = private$target_id,
auto_events = private$auto_events
)
},

#' @description
#' Retrieve the target id
get_target_id = function() {
private$target_id
},

#' @description
#' Wait for a Chromote Session to finish. This method will block the R
#' session until the provided promise resolves. The loop from
Expand Down Expand Up @@ -549,7 +562,7 @@ ChromoteSession <- R6Class(
#' @description Initial promise
#'
#' For internal use only.
init_promise = function() {
get_init_promise = function() {
private$init_promise_
},

Expand All @@ -564,6 +577,7 @@ ChromoteSession <- R6Class(

private = list(
session_id = NULL,
target_id = NULL,
is_active_ = NULL,
event_manager = NULL,
pixel_ratio = NULL,
Expand All @@ -576,3 +590,30 @@ ChromoteSession <- R6Class(
}
)
)


# Wrapper around ChromoteSession$new() that can return a promise
create_session <- function(chromote = default_chromote_object(),
width = 992,
height = 1323,
targetId = NULL,
wait_ = TRUE,
auto_events = NULL) {

session <- ChromoteSession$new(
parent = chromote,
width = width,
height = height,
targetId,
auto_events = auto_events,
wait_ = wait_
)

if (wait_) {
session
} else {
# ChromoteSession$new() must return a ChromoteSession object so we need a
# side-channel to return a promise
session$get_init_promise()
}
}
8 changes: 2 additions & 6 deletions man/Browser.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 2fd9552

Please sign in to comment.