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 =

Matches Obscura's own 64 MiB frame ceiling, so anything the browser is willing to send, the driver is willing to assemble.

Returns:

  • (Integer)

    largest CDP frame (bytes) the driver will assemble.

64 * 1024 * 1024
READ_CHUNK =

Bytes pulled per read syscall. This has to be large: since Obscura raised its frame ceiling to 64 MiB, multi-megabyte replies are routine, and a small value turns one reply into tens of thousands of syscalls. Measured on a 16 MiB reply: 512B → 0.188s, 64KiB → 0.105s. Past 64KiB the curve flattens, so the extra buffer buys nothing.

Returns:

  • (Integer)
64 * 1024

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:



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/obxcura/client.rb', line 51

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.



44
45
46
# File 'lib/obxcura/client.rb', line 44

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.



112
113
114
115
116
117
118
119
120
# File 'lib/obxcura/client.rb', line 112

def close
  @closing = true
  @driver&.close
  @socket&.close
  @subscribers.clear
  @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.



123
124
125
# File 'lib/obxcura/client.rb', line 123

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:



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

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.



97
98
99
# File 'lib/obxcura/client.rb', line 97

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.



105
106
107
# File 'lib/obxcura/client.rb', line 105

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.



133
134
135
136
137
# File 'lib/obxcura/client.rb', line 133

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