rstudio / chromote

Chrome Remote Interface for R
https://rstudio.github.io/chromote/
156 stars 20 forks source link

Compatibility with httpuv #43

Open RLesur opened 3 years ago

RLesur commented 3 years ago

My R session hangs when I try to open a local web page served by a httpuv server with wait_=TRUE. It seems that the httpuv loop does not run (this may be related to https://github.com/rstudio/httpuv/issues/250, feel free to close this issue).

Here is an example:

library(chromote)
library(servr)

b <- ChromoteSession$new()

# works fine
b$Page$navigate(url = "https://rstudio.com")

# launch a httpuv server
svr <- httd(file.path(R.home("doc"), "manual"))
faq_url <- paste(svr$url, "R-FAQ.html", sep = "/")
browseURL(faq_url)

# works fine if one uses wait_ = FALSE
b$Page$navigate(url = faq_url, wait_ = FALSE)

# the R session hangs with wait_ = TRUE
b$Page$navigate(url = faq_url)
wch commented 3 years ago

The reason this happens is because the Chromote object creates its own event loop, which is a "child" of the global event loop.

With the later package, there is a global event loop, and it is possible to create event loops which are children of the global loop. (And similarly, it is possible to create children of children.) When an event loop is run, it also runs its children. However, when a child event loop is run, it does not run the parent. (Also, the global loop runs automatically when the R console is idle.)

The purpose of the child event loops is so that asynchronous code can be run "synchronously", without accidentally affecting other asynchronous code. In Chromote, the synchronize() function essentially takes a promise, and blocks and runs the event loop until that promise is resolved.

Here's an example to illustrate why child loops are important. Shiny apps work by using the global event loop: somewhere in the code it calls later::run_now() to run the global loop. When that happens, it (1) handles incoming messages, (2) executes reactives, and (3) sends outgoing messages. Now suppose you have observer which did something like this:

observe({
  b <- ChromoteSession$new()
  b$Page$navigate(url = input$url)
  b$screenshot()
  b$close()
})

This observer is triggered by an incoming message that says that input$url has changed. When it runs, Shiny is in step 2 of the cycle described above. If Chromote used the global event loop, the Chromote commands would cause the global event loop to run -- but this would be re-entrant: it would be calling later::run_now() in the middle of calling later::run_now(). This could cause Shiny code to run in an unexpected order.

Chromote avoids this problem by using child event loop. When a child event loop is run, it does not cause the global loop to run, and so the observer above is safe.

The problem you have here is that the httpuv application uses the global loop, but when a Chromote command is issued with wait_=TRUE, it keeps running the child loop. When the child loop is running, it blocks the global loop from running, and so httpuv never gets a chance to handle incoming messages.


As for working around the problem, I tried starting the httpuv app with the Chromote object's child loop, using the with_loop() function. I expected this work, but it did not. There is probably some detail that I'm forgetting about how Chromote's synchronize() function works.

library(chromote)
library(servr)

b <- ChromoteSession$new()
b$view()

# launch a httpuv server
later::with_loop(b$get_child_loop(), {
  svr <- httd(file.path(R.home("doc"), "manual"))
})
faq_url <- paste(svr$url, "R-FAQ.html", sep = "/")
browseURL(faq_url)

# Works
b$Page$navigate(url = faq_url, wait_ = FALSE)

# Still hangs with wait_ = TRUE
b$Page$navigate(url = faq_url)

Note that for httpuv's unit tests, we have a similar issue, where use curl to fetch data from the httpuv application. If we used the regular, blocking function curl_fetch_memory(), then the httpuv app would never get a chance to service the request. Instead, we use curl_fetch_multi(), which is asynchronous and nonblocking. See:

https://github.com/rstudio/httpuv/blob/13465f036761952755a1d81c14e0dae017c1c5c0/tests/testthat/helper-app.R#L5-L22

It is called indirectly using fetch() in tests like this: https://github.com/rstudio/httpuv/blob/13465f036761952755a1d81c14e0dae017c1c5c0/tests/testthat/test-http-parse.R#L3-L33

I don't have a good workaround at the moment, but that does not mean that it's impossible to make work.

RLesur commented 3 years ago

Thanks for the explanations. I have already noticed that servr only uses the global events loop. Maybe this issue is mostly on the httpuv side. For now, here is my workaround:

library(chromote)
library(servr)

b <- ChromoteSession$new()
b$view()
b$parent$debug_messages(TRUE)

# launch a httpuv server
svr <- httd(file.path(R.home("doc"), "manual"))
faq_url <- paste(svr$url, "R-FAQ.html", sep = "/")
browseURL(faq_url)

{
  p <- b$Page$navigate(url = faq_url, wait_ = FALSE)
  chromote:::synchronize(p, loop = later::global_loop())
}
markwsac commented 3 years ago

Bug has been introduced after the recent update. b <- ChromoteSession$new() returns this error. I checked on multiple versions of R. Same code works fine with prior version of chromote.

Error in onRejected(reason) : code: -32601 message: 'Inspector.enable' wasn't found

wch commented 3 years ago

@markwsac Can you file a new issue about the Inspector.enable problem?

markwsac commented 3 years ago

Sure, I apologize for spamming here. I thought it's related to the discussion.