Class: Obxcura::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/obxcura/client.rb

Overview

The CDP transport: a single WebSocket to the browser endpoint.

Built directly on websocket-driver, because the higher-level websocket-client-simple reads the socket one byte at a time (@socket.getc) — a ~1MB CDP frame there takes seconds and can wedge the read loop. Here a single reader thread pumps raw bytes through the driver (readpartial + driver.parse), which parses frames efficiently.

This object is the driver's I/O adapter: websocket-driver calls #url for the handshake and #write to put bytes on the wire.

Everything on the wire is multiplexed over this one socket. Command replies come back keyed by the id we assigned (globally unique across the connection), so we resolve those regardless of session. Events instead carry a sessionId, so we fan them out to whichever Page subscribed for that session. Browser-level events (no sessionId) go to the :browser slot.

Constant Summary collapse

DEFAULT_TIMEOUT =

Returns default seconds to wait for a command reply.

Returns:

  • (Integer)

    default seconds to wait for a command reply.

30
MAX_MESSAGE_SIZE =

Obscura frames stay well under this; it's just a guard against a runaway allocation — a sane cap on the receive size.

Returns:

  • (Integer)

    largest CDP frame (bytes) the driver will assemble.

64 * 1024 * 1024
READ_CHUNK =

Bytes pulled per read syscall. Small is fine — the driver reassembles frames across reads; this only bounds how much we buffer at once.

Returns:

  • (Integer)
512

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url, timeout: DEFAULT_TIMEOUT) ⇒ Client

Open the WebSocket to the browser and block until the handshake completes.

Parameters:

  • url (String)

    the ws:///wss:// browser endpoint.

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    default seconds to wait for command replies.

Raises:



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/obxcura/client.rb', line 48

def initialize(url, timeout: DEFAULT_TIMEOUT)
  @url = url
  @timeout = timeout
  @command_id = 0
  @mutex = Mutex.new
  @write_mutex = Mutex.new
  @pending = {}
  @subscribers = {}
  @open = Queue.new
  connect
end

Instance Attribute Details

#urlString (readonly)

Returns the WebSocket URL of the browser endpoint.

Returns:

  • (String)

    the WebSocket URL of the browser endpoint.



41
42
43
# File 'lib/obxcura/client.rb', line 41

def url
  @url
end

Instance Method Details

#closevoid

This method returns an undefined value.

Close the driver, socket and reader thread. Safe to call more than once.



109
110
111
112
113
114
115
116
# File 'lib/obxcura/client.rb', line 109

def close
  @closing = true
  @driver&.close
  @socket&.close
  @reader&.kill
rescue IOError, SystemCallError
  nil
end

#closing?Boolean

Returns whether the connection is being (or has been) torn down.

Returns:

  • (Boolean)

    whether the connection is being (or has been) torn down.



119
120
121
# File 'lib/obxcura/client.rb', line 119

def closing?
  @closing == true
end

#command(method, params = {}, session_id: nil, timeout: @timeout) ⇒ Hash

Send a CDP command and block until its reply arrives.

Parameters:

  • method (String)

    the CDP method name, e.g. "Runtime.evaluate".

  • params (Hash) (defaults to: {})

    the method parameters.

  • session_id (String, nil) (defaults to: nil)

    nil sends a browser-level command (target management); a session id routes it to a specific page.

  • timeout (Integer) (defaults to: @timeout)

    seconds to wait for the reply.

Returns:

  • (Hash)

    the command's result object.

Raises:



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/obxcura/client.rb', line 70

def command(method, params = {}, session_id: nil, timeout: @timeout)
  id = next_id
  queue = Queue.new
  @mutex.synchronize { @pending[id] = queue }

  frame = { id: id, method: method, params: params }
  frame[:sessionId] = session_id if session_id
  @driver.text(JSON.generate(frame))

  message = Timeout.timeout(timeout, TimeoutError, "#{method} timed out after #{timeout}s") { queue.pop }
  raise ProtocolError, message["error"]["message"] if message["error"]

  message["result"]
ensure
  @mutex.synchronize { @pending.delete(id) }
end

#subscribe(session_id) {|method, params| ... } ⇒ Proc

Register a handler for events on a given session.

Parameters:

  • session_id (String, Symbol)

    the page session id, or :browser for target-level (session-less) events.

Yield Parameters:

  • method (String)

    the CDP event name.

  • params (Hash)

    the event parameters.

Returns:

  • (Proc)

    the stored handler.



94
95
96
# File 'lib/obxcura/client.rb', line 94

def subscribe(session_id, &block)
  @subscribers[session_id] = block
end

#unsubscribe(session_id) ⇒ Proc?

Remove the handler previously registered for a session.

Parameters:

  • session_id (String, Symbol)

    the session to stop listening on.

Returns:

  • (Proc, nil)

    the removed handler, if any.



102
103
104
# File 'lib/obxcura/client.rb', line 102

def unsubscribe(session_id)
  @subscribers.delete(session_id)
end

#write(data) ⇒ void

This method returns an undefined value.

I/O adapter for websocket-driver: puts outgoing bytes on the wire. Writes are serialized so command threads and the driver's own control frames (ping/pong/close) never interleave and corrupt the stream.

Parameters:

  • data (String)

    raw bytes to write.



129
130
131
132
133
# File 'lib/obxcura/client.rb', line 129

def write(data)
  @write_mutex.synchronize { @socket.write(data) }
rescue IOError, SystemCallError => e
  warn "[Obxcura] websocket write error: #{e}" unless closing?
end