class Opal::CliRunners::Chrome

Constants

DEFAULT_CHROME_HOST
DEFAULT_CHROME_PORT
SCRIPT_PATH

Attributes

builder[R]
exit_status[R]
output[R]

Public Class Methods

call(data) click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 18
def self.call(data)
  runner = new(data)
  runner.run
end
new(data) click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 23
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

Public Instance Methods

run() click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 36
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 Instance Methods

chrome_executable() click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 193
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
chrome_host() click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 104
def chrome_host
  ENV['CHROME_HOST'] || DEFAULT_CHROME_HOST
end
chrome_port() click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 108
def chrome_port
  ENV['CHROME_PORT'] || DEFAULT_CHROME_PORT
end
chrome_server_running?() click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 185
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
mktmpdir(&block) click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 219
def mktmpdir(&block)
  Dir.mktmpdir('chrome-opal-', &block)
end
mktmpprofile() click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 223
def mktmpprofile
  Dir.mktmpdir('chrome-opal-profile-')
end
prepare_files_in(dir) click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 65
      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
run_chrome_server() { || ... } click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 120
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
with_chrome_server() { || ... } click to toggle source
# File lib/opal/cli_runners/chrome.rb, line 112
def with_chrome_server
  if chrome_server_running?
    yield
  else
    run_chrome_server { yield }
  end
end