class Opal::CliRunners::Chrome
def self.call(data)
def self.call(data) runner = new(data) runner.run end
def chrome_executable
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 chrome_host
def chrome_host ENV['CHROME_HOST'] || DEFAULT_CHROME_HOST end
def chrome_port
def chrome_port ENV['CHROME_PORT'] || DEFAULT_CHROME_PORT end
def chrome_server_running?
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 initialize(data)
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
def mktmpdir(&block)
def mktmpdir(&block) Dir.mktmpdir('chrome-opal-', &block) end
def mktmpprofile
def mktmpprofile Dir.mktmpdir('chrome-opal-profile-') end
def prepare_files_in(dir)
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 run
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
def run_chrome_server
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 with_chrome_server
def with_chrome_server if chrome_server_running? yield else run_chrome_server { yield } end end