Class: RubyLens::Clip::ChromePage
- Inherits:
-
Object
- Object
- RubyLens::Clip::ChromePage
- Defined in:
- lib/rubylens/clip/chrome_page.rb
Overview
Runs a headless Chrome/Chromium with an ephemeral DevTools port and a throwaway profile, attaches to the page that loads the showcase HTML, and exposes the two operations clip rendering needs: evaluate and screenshot. The port comes from the profile's DevToolsActivePort file, so parallel clips never race for a fixed port.
Constant Summary collapse
- HEADLESS_FLAGS =
%w[ --headless=new --remote-debugging-port=0 --no-first-run --no-default-browser-check --disable-background-networking --disable-component-update --disable-sync --mute-audio --hide-scrollbars --force-device-scale-factor=1 --force-color-profile=srgb --enable-unsafe-swiftshader ].freeze
- LAUNCH_TIMEOUT_SECONDS =
30
Class Method Summary collapse
Instance Method Summary collapse
- #close ⇒ Object
- #evaluate(expression, await: false, timeout: 30) ⇒ Object
-
#initialize(executable:, url:, width:, height:) ⇒ ChromePage
constructor
A new instance of ChromePage.
- #screenshot_png(timeout: 60) ⇒ Object
Constructor Details
#initialize(executable:, url:, width:, height:) ⇒ ChromePage
Returns a new instance of ChromePage.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/rubylens/clip/chrome_page.rb', line 46 def initialize(executable:, url:, width:, height:) @profile = Dir.mktmpdir("rubylens-clip-") @next_id = 0 @responses = {} flags = HEADLESS_FLAGS.dup # Chrome refuses its sandbox as root (typical for containers and CI). flags << "--no-sandbox" if Process.euid.zero? flags << "--window-size=#{width},#{height}" flags << "--user-data-dir=#{@profile}" @pid = Process.spawn(executable, *flags, url, %i[out err] => File::NULL) port = wait_for_devtools_port @channel = WebSocketChannel.new("127.0.0.1", port, page_target_path(port)) # The headless window includes non-viewport space; pin the viewport so # captures are exactly the stage size. command("Emulation.setDeviceMetricsOverride", { "width" => width, "height" => height, "deviceScaleFactor" => 1, "mobile" => false }, timeout: 30) rescue Errno::ENOENT => error abort_start raise Error, "could not launch Chrome at #{executable}: #{error.}" rescue SystemCallError => error # A startup race (Chrome exiting or closing its port mid-attach) must # surface as a normal rubylens error, not a raw Errno stack trace. abort_start raise Error, "could not attach to Chrome's DevTools endpoint: #{error.}" rescue StandardError abort_start raise end |
Class Method Details
.open(executable:, url:, width:, height:) ⇒ Object
37 38 39 40 41 42 43 44 |
# File 'lib/rubylens/clip/chrome_page.rb', line 37 def self.open(executable:, url:, width:, height:) page = new(executable:, url:, width:, height:) begin yield page ensure page.close end end |
Instance Method Details
#close ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/rubylens/clip/chrome_page.rb', line 93 def close begin command("Browser.close", {}, timeout: 3) if @channel rescue Error terminate end Process.wait(@pid) if @pid rescue Errno::ECHILD nil ensure @channel&.close FileUtils.remove_entry(@profile) if @profile && File.directory?(@profile) end |
#evaluate(expression, await: false, timeout: 30) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rubylens/clip/chrome_page.rb', line 76 def evaluate(expression, await: false, timeout: 30) result = command("Runtime.evaluate", { "expression" => expression, "returnByValue" => true, "awaitPromise" => await, }, timeout:) if (details = result["exceptionDetails"]) raise Error, "the showcase page raised: #{details.dig("exception", "description") || details["text"]}" end result.dig("result", "value") end |
#screenshot_png(timeout: 60) ⇒ Object
88 89 90 91 |
# File 'lib/rubylens/clip/chrome_page.rb', line 88 def screenshot_png(timeout: 60) data = command("Page.captureScreenshot", { "format" => "png", "optimizeForSpeed" => true }, timeout:) Base64.decode64(data.fetch("data")) end |