Electron Adventures: Episode 97: Ferrum and Chrome DevTools Protocol
Tags: javascript, electron, ruby, chrome
Published: 2021-11-13 01:42:26

We explored a wide variety of "a browser stapled to some backend" solutions, but why don't we just use a browser directly?

This almost works.

### [Ferrum gem](https://github.com/rubycdp/ferrum)

We'll be using Ruby for this episode, but everything I say is applicable to any other language, and all the issues are the same.

Ferrum starts by trying to find whichever version of Chrome or Chromium you might have installed. And if it's something unusual, you can pass in a custom path. Or just install Chrome/Chromium - that's not a huge ask these days.

Electron has a bit of an advantage here that you'll be using known version of Chrome, but really "any Chrome" is far better than something like neutralino or pywebkit are doing (pywebkit can literally run your app on Internet Explorer 11 even when Chrome is installed!).

Ferrum starts Chrome passing 32 command line arguments to isolate it and make it more controllable from the outside.

And then Ferrum uses Chrome DevTools Protocol to control the browser.

### make_screenshot

Let's write one such program.

```ruby
#!/usr/bin/env ruby

require "ferrum"

browser = Ferrum::Browser.new
browser.go_to("https://en.wikipedia.org/wiki/Cat")
browser.screenshot(path: "cat.png")
browser.quit
```

The browser is launched in headless mode, so you won't even see anything. The result is what you'd expect:

![Episode 97 Screenshot A](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yas3u6osbgm9rna5nc1h.png)

Ferrum starts the browser, issues a few commands to it, then quits.

That is super useful, either for end-to-end testing your app, or for web crawling. To make an app, we need quite a few more steps.

### wikipedia_browser

Let's try to make an app. First, obviously disable headless mode. We run into the first problem that Ferrum wants to quit as soon as it's done with commands, so let's just add infinite sleeping loop at the end:

```ruby
#!/usr/bin/env ruby

require "ferrum"

browser = Ferrum::Browser.new(
  headless: false
)
browser.go_to("https://en.wikipedia.org/wiki/Cat")

loop do
  sleep 60
end
```

That's what we get:

![Episode 97 Screenshot B](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c8jr6v3b4botzpcbbygs.png)

Well, that address bar and tab interface is not what we want. Ironically I tried to get tabs in Electron for CSV Editor, instead of creating tons of windows, and that was never possible. But now we want to get rid of that.

### wikipedia_browser_2

Chrome has something called "kiosk mode" that strips that extra UI, and just leaves the main page. It took a few tried to get it working (`--kiosk` option is officially there).

```ruby
#!/usr/bin/env ruby

require "ferrum"

browser = Ferrum::Browser.new(
  headless: false,
  browser_options: {
    "app" => "https://en.wikipedia.org/wiki/Cat",
  },
)

loop do
  sleep 60
end
```

And we got it looking like we want:

![Episode 97 Screenshot C](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmv1k8fzg848pm7iqk35.png)

### print_version

OK, that was cute, but to have a working app, we need a way to talk with the frontend. Well, Chrome DevTools Protocol lets us send commands. We can use `browser.client.command` - and that's what Ferrum gem does under the hood. Many of the commands are actually quite complicated, and it's great that Ferrum handles that busywork, but we can do some simple ones:

```ruby
#!/usr/bin/env ruby

require "ferrum"

browser = Ferrum::Browser.new
pp browser.client.command("Browser.getVersion")
browser.quit
```

Which prints:

```
{"protocolVersion"=>"1.3",
 "product"=>"HeadlessChrome/95.0.4638.69",
 "revision"=>"@6a1600ed572fedecd573b6c2b90a22fe6392a410",
 "userAgent"=>
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/95.0.4638.69 Safari/537.36",
 "jsVersion"=>"9.5.172.25"}
```

### search_wikipedia

For some more complex commands let's just use what Ferrum provides:

```ruby
#!/usr/bin/env ruby

require "ferrum"

browser = Ferrum::Browser.new(headless: false)
browser.go_to("https://en.wikipedia.org")

input = browser.at_css("input[name=search]")
input.focus.type("bengal tiger", :Enter)

loop do
  sleep 60
end
```

This does exactly what we want:

![Episode 97 Screenshot D](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hkwmwzq51f0ygal4c6x.png)

### Sending data to the backend

If we disregard minor issues with kiosk mode and that sleep loop, the only major missing thing is the ability of frontend to send data to the backend... And it's just not there.

I'm actually quite baffled by this, as it's so close to being viable for making apps.

Now to be fair we can emulate it. We can start an HTTP server, or a websocket, or have backend keep polling some promise pool on the frontend, or one of many such approaches.

I think if someone adds this, this will be a viable Electron alternative, as Chrome DevTools Protocol works with any backend language, and nearly everyone has Chrome installed already. Unfortunately until someone does, we sort of reached a dead end here.

### Results

The series is nearing completion, but In the next episode we'll try one more overdue thing.

As usual, [all the code for the episode is here](episode-97-ruby-ferrum).