lib/opal/cli_runners/chrome.rb
# frozen_string_literal: true
require 'shellwords'
require 'socket'
require 'timeout'
require 'tmpdir'
require 'rbconfig'
require 'opal/os'
module Opal
module CliRunners
class Chrome
SCRIPT_PATH = File.expand_path('chrome_cdp_interface.rb', __dir__).freeze
DEFAULT_CHROME_HOST = 'localhost'
DEFAULT_CHROME_PORT = 9222
def self.call(data)
runner = new(data)
runner.run
end
def initialize(data)
argv = data[:argv]
if argv && argv.any?
warn "warning: ARGV is not supported by the Chrome runner #{argv.inspect}"
end
options = data[:options]
@output = options.fetch(:output, $stdout)
@builder = data[:builder].call
end
attr_reader :output, :exit_status, :builder
def run
mktmpdir do |dir|
with_chrome_server do
prepare_files_in(dir)
env = {
'CHROME_HOST' => chrome_host,
'CHROME_PORT' => chrome_port.to_s,
'NODE_PATH' => File.join(__dir__, 'node_modules'),
'OPAL_CDP_EXT' => builder.output_extension
}
cmd = [
RbConfig.ruby,
"#{__dir__}/../../../exe/opal",
'--no-exit',
'-I', __dir__,
'-r', 'source-map-support-node',
SCRIPT_PATH,
dir
]
Kernel.exec(env, *cmd)
end
end
end
private
def prepare_files_in(dir)
js = builder.to_s
map = builder.source_map.to_json
stack = File.binread("#{__dir__}/source-map-support-browser.js")
ext = builder.output_extension
module_type = ' type="module"' if builder.esm?
# Some maps may contain `</script>` fragment (eg. in strings) which would close our
# `<script>` tag prematurely. For this case, we need to escape the `</script>` tag.
map_json = map.to_json.gsub(/(<\/scr)(ipt>)/i, '\1"+"\2')
# Chrome can't handle huge data passed to `addScriptToEvaluateOnLoad`
# https://groups.google.com/a/chromium.org/forum/#!topic/chromium-discuss/U5qyeX_ydBo
# The only way is to create temporary files and pass them to chrome.
File.binwrite("#{dir}/index.#{ext}", js)
File.binwrite("#{dir}/index.map", map)
File.binwrite("#{dir}/source-map-support.js", stack)
File.binwrite("#{dir}/index.html", <<~HTML)
<html><head>
<meta charset='utf-8'>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
<script src='./source-map-support.js'></script>
<script>
window.opalheadlesschrome = true;
sourceMapSupport.install({
retrieveSourceMap: function(path) {
return path.endsWith('/index.#{ext}') ? {
url: './index.map', map: #{map_json}
} : null;
}
});
</script>
</head><body>
<script src='./index.#{ext}'#{module_type}></script>
</body></html>
HTML
end
def chrome_host
ENV['CHROME_HOST'] || DEFAULT_CHROME_HOST
end
def chrome_port
ENV['CHROME_PORT'] || DEFAULT_CHROME_PORT
end
def with_chrome_server
if chrome_server_running?
yield
else
run_chrome_server { yield }
end
end
def run_chrome_server
raise 'Chrome server can be started only on localhost' if chrome_host != DEFAULT_CHROME_HOST
profile = mktmpprofile
# Disable web security with "--disable-web-security" flag to be able to do XMLHttpRequest (see test_openuri.rb)
# For other options see https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/node/ChromeLauncher.ts
chrome_server_cmd = %{#{OS.shellescape(chrome_executable)} \
--allow-pre-commit-input \
--disable-background-networking \
--enable-features=NetworkServiceInProcess2 \
--disable-background-timer-throttling \
--disable-backgrounding-occluded-windows \
--disable-breakpad \
--disable-client-side-phishing-detection \
--disable-component-extensions-with-background-pages \
--disable-default-apps \
--disable-dev-shm-usage \
--disable-extensions \
--disable-features=Translate,BackForwardCache,AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync \
--disable-hang-monitor \
--disable-ipc-flooding-protection \
--disable-popup-blocking \
--disable-prompt-on-repost \
--disable-renderer-backgrounding \
--disable-sync \
--force-color-profile=srgb \
--metrics-recording-only \
--no-first-run \
--enable-automation \
--password-store=basic \
--use-mock-keychain \
--enable-blink-features=IdleDetection \
--export-tagged-pdf \
--headless \
--user-data-dir=#{profile} \
--hide-scrollbars \
--mute-audio \
--disable-web-security \
--remote-debugging-port=#{chrome_port} \
#{ENV['CHROME_OPTS']}}
chrome_pid = Process.spawn(chrome_server_cmd, in: OS.dev_null, out: OS.dev_null, err: OS.dev_null)
Timeout.timeout(30) do
loop do
break if chrome_server_running?
sleep 0.5
end
end
yield
rescue Timeout::Error
puts 'Failed to start chrome server'
puts 'Make sure that you have it installed and that its version is > 59'
exit(1)
ensure
if OS.windows? && chrome_pid
Process.kill('KILL', chrome_pid) unless system("taskkill /f /t /pid #{chrome_pid} >NUL 2>NUL")
elsif chrome_pid
Process.kill('HUP', chrome_pid)
end
FileUtils.rm_rf(profile) if profile
end
def chrome_server_running?
puts "Connecting to #{chrome_host}:#{chrome_port}..."
TCPSocket.new(chrome_host, chrome_port).close
true
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
false
end
def chrome_executable
ENV['GOOGLE_CHROME_BINARY'] ||
if OS.windows?
[
'C:/Program Files/Google/Chrome Dev/Application/chrome.exe',
'C:/Program Files/Google/Chrome/Application/chrome.exe'
].each do |path|
next unless File.exist? path
return path
end
elsif OS.macos?
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
else
%w[
google-chrome-stable
chromium
chromium-freeworld
chromium-browser
].each do |name|
next unless system('sh', '-c', "command -v #{name.shellescape}", out: '/dev/null')
return name
end
raise 'Cannot find chrome executable'
end
end
def mktmpdir(&block)
Dir.mktmpdir('chrome-opal-', &block)
end
def mktmpprofile
Dir.mktmpdir('chrome-opal-profile-')
end
end
end
end