bidi2pdf 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +32 -4
  4. data/README.md +14 -0
  5. data/docker/Dockerfile +1 -1
  6. data/docker/Dockerfile.chromedriver +1 -1
  7. data/docker/Dockerfile.slim +2 -2
  8. data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
  9. data/lib/bidi2pdf/bidi/browser_tab.rb +391 -41
  10. data/lib/bidi2pdf/bidi/client.rb +85 -23
  11. data/lib/bidi2pdf/bidi/command_manager.rb +46 -48
  12. data/lib/bidi2pdf/bidi/commands/base.rb +39 -1
  13. data/lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb +27 -0
  14. data/lib/bidi2pdf/bidi/commands/browsing_context_print.rb +4 -0
  15. data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +5 -0
  16. data/lib/bidi2pdf/bidi/commands.rb +1 -0
  17. data/lib/bidi2pdf/bidi/event_manager.rb +1 -1
  18. data/lib/bidi2pdf/bidi/interceptor.rb +1 -1
  19. data/lib/bidi2pdf/bidi/js_logger_helper.rb +16 -0
  20. data/lib/bidi2pdf/bidi/logger_events.rb +25 -45
  21. data/lib/bidi2pdf/bidi/network_event.rb +15 -0
  22. data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +4 -3
  23. data/lib/bidi2pdf/bidi/network_events.rb +27 -17
  24. data/lib/bidi2pdf/bidi/session.rb +119 -12
  25. data/lib/bidi2pdf/bidi/user_context.rb +62 -0
  26. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +7 -7
  27. data/lib/bidi2pdf/chromedriver_manager.rb +48 -21
  28. data/lib/bidi2pdf/cli.rb +10 -2
  29. data/lib/bidi2pdf/dsl.rb +33 -0
  30. data/lib/bidi2pdf/launcher.rb +30 -0
  31. data/lib/bidi2pdf/notifications/event.rb +52 -0
  32. data/lib/bidi2pdf/notifications/instrumenter.rb +65 -0
  33. data/lib/bidi2pdf/notifications/logging_subscriber.rb +136 -0
  34. data/lib/bidi2pdf/notifications.rb +78 -0
  35. data/lib/bidi2pdf/session_runner.rb +35 -3
  36. data/lib/bidi2pdf/verbose_logger.rb +79 -0
  37. data/lib/bidi2pdf/version.rb +1 -1
  38. data/lib/bidi2pdf.rb +99 -7
  39. data/sig/bidi2pdf/bidi/client.rbs +1 -1
  40. metadata +39 -4
  41. data/lib/bidi2pdf/utils.rb +0 -15
@@ -21,13 +21,13 @@ module Bidi2pdf
21
21
  if event["context"] == context_id
22
22
  handle_response(method, event)
23
23
  else
24
- Bidi2pdf.logger.debug "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
24
+ Bidi2pdf.logger.debug3 "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
25
25
  end
26
26
  rescue StandardError => e
27
27
  Bidi2pdf.logger.error "Error handling network event: #{e.message}"
28
28
  end
29
29
 
30
- # rubocop:disable Metrics/AbcSize
30
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
31
31
  def handle_response(method, event)
32
32
  return unless event && event["request"]
33
33
 
@@ -43,23 +43,33 @@ module Bidi2pdf
43
43
 
44
44
  timestamp = event["timestamp"]
45
45
 
46
- if method == "network.beforeRequestSent"
47
- events[id] ||= NetworkEvent.new(
48
- id: id,
49
- url: url,
50
- timestamp: timestamp,
51
- timing: timing,
52
- state: method,
53
- http_method: http_method
54
- )
55
- elsif events.key?(id)
56
- events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
57
- else
58
- Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
46
+ Bidi2pdf.notification_service.instrument("network_event_received.bidi2pdf",
47
+ {
48
+ id: id,
49
+ method: method,
50
+ url: url,
51
+ http_status_code: http_status_code
52
+ }) do |instrumentation_payload|
53
+ if method == "network.beforeRequestSent"
54
+ events[id] ||= NetworkEvent.new(
55
+ id: id,
56
+ url: url,
57
+ timestamp: timestamp,
58
+ timing: timing,
59
+ state: method,
60
+ http_method: http_method
61
+ )
62
+ elsif events.key?(id)
63
+ events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
64
+ else
65
+ Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
66
+ end
67
+
68
+ instrumentation_payload[:event] = events[id]&.dup
59
69
  end
60
70
  end
61
71
 
62
- # rubocop:enable Metrics/AbcSize
72
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
63
73
 
64
74
  def all_events
65
75
  events.values.sort_by(&:start_timestamp)
@@ -77,7 +87,7 @@ module Bidi2pdf
77
87
  end
78
88
  end
79
89
 
80
- def wait_until_network_idle(timeout: 10, poll_interval: 0.1)
90
+ def wait_until_network_idle(timeout: 10, poll_interval: 0.01)
81
91
  start_time = Time.now
82
92
 
83
93
  loop do
@@ -7,14 +7,47 @@ require_relative "client"
7
7
  require_relative "browser"
8
8
  require_relative "user_context"
9
9
 
10
+ # Represents a session for managing browser interactions and communication
11
+ # using the Bidi2pdf library. This class handles the setup, configuration,
12
+ # and execution of browser-related workflows, including session creation,
13
+ # WebSocket communication, and browser management.
14
+ #
15
+ # @example Creating and starting a session
16
+ # session = Bidi2pdf::Bidi::Session.new(session_url: "http://example.com/session", headless: true)
17
+ # session.start
18
+ #
19
+ # @example Retrieving user contexts
20
+ # session.user_contexts
21
+ #
22
+ # @example Closing the session
23
+ # session.close
24
+ #
25
+ # @param [String] session_url The URL for the session.
26
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
27
+ # @param [Array<String>] chrome_args Additional Chrome arguments. Defaults to predefined arguments.
10
28
  module Bidi2pdf
11
29
  module Bidi
12
30
  class Session
31
+ # Events to subscribe to during the session.
13
32
  SUBSCRIBE_EVENTS = %w[script].freeze
33
+
34
+ # Default Chrome arguments for the session.
14
35
  DEFAULT_CHROME_ARGS = %w[--disable-gpu --disable-popup-blocking --disable-hang-monitor].freeze
15
36
 
16
- attr_reader :session_uri, :started, :chrome_args
37
+ # @return [URI] The URI of the session.
38
+ attr_reader :session_uri
39
+
40
+ # @return [Boolean] Whether the session has started.
41
+ attr_reader :started
42
+
43
+ # @return [Array<String>] The Chrome arguments for the session.
44
+ attr_reader :chrome_args
17
45
 
46
+ # Initializes a new session.
47
+ #
48
+ # @param [String] session_url The URL for the session.
49
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
50
+ # @param [Array<String>] chrome_args Additional Chrome arguments. Defaults to predefined arguments.
18
51
  def initialize(session_url:, headless: true, chrome_args: DEFAULT_CHROME_ARGS)
19
52
  @session_uri = URI(session_url)
20
53
  @headless = headless
@@ -22,6 +55,9 @@ module Bidi2pdf
22
55
  @chrome_args = chrome_args.dup
23
56
  end
24
57
 
58
+ # Starts the session and initializes the client.
59
+ #
60
+ # @raise [StandardError] If an error occurs during session start.
25
61
  def start
26
62
  return if started?
27
63
 
@@ -32,41 +68,68 @@ module Bidi2pdf
32
68
  raise e
33
69
  end
34
70
 
71
+ # Returns the WebSocket client for the session.
72
+ #
73
+ # @return [Bidi2pdf::Bidi::Client, nil] The WebSocket client, or nil if the session is not started.
35
74
  def client
36
75
  @client ||= started? ? create_client : nil
37
76
  end
38
77
 
78
+ # Returns the browser instance for the session.
79
+ #
80
+ # @return [Bidi2pdf::Bidi::Browser] The browser instance.
39
81
  def browser
40
82
  @browser ||= create_browser
41
83
  end
42
84
 
85
+ # Closes the session and cleans up resources.
86
+ # rubocop:disable Metrics/AbcSize
43
87
  def close
44
88
  return unless started?
45
89
 
46
90
  2.times do |attempt|
47
- client&.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::SessionEnd.new, timeout: 1) do |response|
48
- Bidi2pdf.logger.info "Session ended: #{response}"
49
-
50
- cleanup
91
+ success = Bidi2pdf.notification_service.instrument("session_close.bidi2pdf", { session_uri: session_uri.to_s, attempt: attempt + 1 }) do |payload|
92
+ client&.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::SessionEnd.new, timeout: 1) do |response|
93
+ payload[:response] = response
94
+ cleanup
95
+ end
96
+
97
+ true
98
+ rescue CmdTimeoutError => e
99
+ payload[:error] = e
100
+ payload[:retry] = attempt < 1 # whether we'll retry again
101
+
102
+ false
51
103
  end
52
- break
53
- rescue CmdTimeoutError
54
- Bidi2pdf.logger.error "Session end command timed out. Retrying... (#{attempt + 1})"
104
+
105
+ break if success
55
106
  end
107
+ ensure
108
+ @started = false
56
109
  end
57
110
 
111
+ # rubocop: enable Metrics/AbcSize
112
+
113
+ # Retrieves user contexts for the session.
58
114
  def user_contexts
59
115
  send_cmd(Bidi2pdf::Bidi::Commands::GetUserContexts.new) { |resp| Bidi2pdf.logger.debug "User contexts: #{resp}" }
60
116
  end
61
117
 
118
+ # Retrieves the status of the session.
62
119
  def status
63
120
  send_cmd(Bidi2pdf::Bidi::Commands::SessionStatus.new) { |resp| Bidi2pdf.logger.info "Session status: #{resp.inspect}" }
64
121
  end
65
122
 
123
+ # Checks if the session has started.
124
+ #
125
+ # @return [Boolean] True if the session has started, false otherwise.
66
126
  def started?
67
127
  @started
68
128
  end
69
129
 
130
+ # Retrieves the WebSocket URL for the session.
131
+ #
132
+ # @return [String] The WebSocket URL.
70
133
  def websocket_url
71
134
  return @websocket_url if @websocket_url
72
135
 
@@ -79,11 +142,18 @@ module Bidi2pdf
79
142
 
80
143
  private
81
144
 
82
- def send_cmd(command, &block)
83
- client&.send_cmd_and_wait(command, &block)
145
+ # Sends a command to the WebSocket client.
146
+ #
147
+ # @param [Object] command The command to send.
148
+ # @yield [response] A block to handle the response.
149
+ def send_cmd(command, &)
150
+ client&.send_cmd_and_wait(command, &)
84
151
  end
85
152
 
86
- # rubocop: disable Metrics/AbcSize
153
+ # Creates a new browser instance.
154
+ #
155
+ # @return [Bidi2pdf::Bidi::Browser] The browser instance.
156
+ # rubocop:disable Metrics/AbcSize
87
157
  def create_browser
88
158
  start
89
159
  client.start
@@ -103,12 +173,18 @@ module Bidi2pdf
103
173
  Bidi::Browser.new(client)
104
174
  end
105
175
 
106
- # rubocop: enable Metrics/AbcSize
176
+ # rubocop:enable Metrics/AbcSize
107
177
 
178
+ # Creates a new WebSocket client.
179
+ #
180
+ # @return [Bidi2pdf::Bidi::Client] The WebSocket client.
108
181
  def create_client
109
182
  Bidi::Client.new(websocket_url).tap(&:start)
110
183
  end
111
184
 
185
+ # Creates a new session and retrieves the WebSocket URL.
186
+ #
187
+ # @return [String] The WebSocket URL.
112
188
  def create_new_session
113
189
  session_data = exec_api_call(session_request)
114
190
  Bidi2pdf.logger.debug "Session data: #{session_data}"
@@ -124,6 +200,9 @@ module Bidi2pdf
124
200
  ws_url
125
201
  end
126
202
 
203
+ # Builds the session request payload.
204
+ #
205
+ # @return [Hash] The session request payload.
127
206
  def session_request
128
207
  session_chrome_args = chrome_args.dup
129
208
  session_chrome_args << "--headless" if @headless
@@ -142,6 +221,10 @@ module Bidi2pdf
142
221
  }
143
222
  end
144
223
 
224
+ # Executes an API call with the given payload.
225
+ #
226
+ # @param [Hash] payload The payload for the API call.
227
+ # @return [Hash] The parsed response data.
145
228
  def exec_api_call(payload)
146
229
  response = Net::HTTP.post(session_uri, payload.to_json, "Content-Type" => "application/json")
147
230
  body = response.body
@@ -158,11 +241,20 @@ module Bidi2pdf
158
241
  build_error(error_type, "#{error_description(error_type)} #{e.message}", e.backtrace)
159
242
  end
160
243
 
244
+ # Logs an API error.
245
+ #
246
+ # @param [String] message The error message.
247
+ # @param [Integer] code The response code.
248
+ # @param [String] body The response body.
161
249
  def log_api_error(message, code, body)
162
250
  Bidi2pdf.logger.error "#{message}. Response code: #{code}"
163
251
  Bidi2pdf.logger.error "Response body: #{body}"
164
252
  end
165
253
 
254
+ # Determines the error category based on the exception.
255
+ #
256
+ # @param [Exception] exception The exception to categorize.
257
+ # @return [String] The error category.
166
258
  def error_category(exception)
167
259
  case exception
168
260
  when Errno::ECONNREFUSED then "Connection refused"
@@ -171,6 +263,10 @@ module Bidi2pdf
171
263
  end
172
264
  end
173
265
 
266
+ # Retrieves the error description for a given error type.
267
+ #
268
+ # @param [String] type The error type.
269
+ # @return [String] The error description.
174
270
  def error_description(type)
175
271
  {
176
272
  "Connection refused" => "Could not connect to the session URL:",
@@ -179,6 +275,12 @@ module Bidi2pdf
179
275
  }[type]
180
276
  end
181
277
 
278
+ # Builds an error response.
279
+ #
280
+ # @param [String] error The error type.
281
+ # @param [String] message The error message.
282
+ # @param [Array<String>, nil] backtrace The error backtrace.
283
+ # @return [Hash] The error response.
182
284
  def build_error(error, message, backtrace = nil)
183
285
  {
184
286
  "value" => {
@@ -189,6 +291,10 @@ module Bidi2pdf
189
291
  }
190
292
  end
191
293
 
294
+ # Handles an error response from the session.
295
+ #
296
+ # @param [Hash] value The error response value.
297
+ # @raise [SessionNotStartedError] If the session could not be started.
192
298
  def handle_error(value)
193
299
  error = value["error"]
194
300
  return unless error
@@ -208,6 +314,7 @@ module Bidi2pdf
208
314
  "Session not started. Check logs for more details. Error: #{error} message: #{msg}"
209
315
  end
210
316
 
317
+ # Cleans up resources associated with the session.
211
318
  def cleanup
212
319
  @client&.close
213
320
  @client = @websocket_url = @browser = nil
@@ -4,14 +4,41 @@ require_relative "browser_tab"
4
4
 
5
5
  module Bidi2pdf
6
6
  module Bidi
7
+ # Represents a user context for managing browser interactions and cookies
8
+ # using the Bidi2pdf library. This class provides methods for creating
9
+ # user contexts, setting cookies, and creating browser windows.
10
+ #
11
+ # @example Creating a user context
12
+ # user_context = Bidi2pdf::Bidi::UserContext.new(client)
13
+ #
14
+ # @example Setting a cookie
15
+ # user_context.set_cookie(
16
+ # name: "session",
17
+ # value: "abc123",
18
+ # domain: "example.com",
19
+ # source_origin: "http://example.com"
20
+ # )
21
+ #
22
+ # @example Creating a browser window
23
+ # browser_window = user_context.create_browser_window
24
+ #
25
+ # @param [Object] client The WebSocket client for communication.
7
26
  class UserContext
27
+ # @return [Object] The WebSocket client.
8
28
  attr_reader :client
9
29
 
30
+ # Initializes a new user context.
31
+ #
32
+ # @param [Object] client The WebSocket client for communication.
10
33
  def initialize(client)
11
34
  @client = client
12
35
  @context_id = nil
13
36
  end
14
37
 
38
+ # Retrieves the user context ID, creating it if it does not exist.
39
+ #
40
+ # @return [String] The user context ID.
41
+ # @raise [RuntimeError] If an error occurs while creating the user context.
15
42
  def context_id
16
43
  @context_id ||= begin
17
44
  res = client.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::BrowserCreateUserContext.new) do |response|
@@ -26,6 +53,17 @@ module Bidi2pdf
26
53
  end
27
54
  end
28
55
 
56
+ # Sets a cookie in the user context.
57
+ #
58
+ # @param [String] name The name of the cookie.
59
+ # @param [String] value The value of the cookie.
60
+ # @param [String] domain The domain for the cookie.
61
+ # @param [String] source_origin The source origin for the cookie.
62
+ # @param [String] path The path for the cookie. Defaults to "/".
63
+ # @param [Boolean] secure Whether the cookie is secure. Defaults to true.
64
+ # @param [Boolean] http_only Whether the cookie is HTTP-only. Defaults to false.
65
+ # @param [String] same_site The SameSite attribute for the cookie. Defaults to "strict".
66
+ # @param [Integer] ttl The time-to-live for the cookie in seconds. Defaults to 30.
29
67
  def set_cookie(
30
68
  name:,
31
69
  value:,
@@ -55,6 +93,9 @@ module Bidi2pdf
55
93
  end
56
94
  end
57
95
 
96
+ # Creates a new browser window in the user context.
97
+ #
98
+ # @return [BrowserTab] The newly created browser tab.
58
99
  def create_browser_window
59
100
  cmd = Bidi2pdf::Bidi::Commands::CreateWindow.new(user_context_id: context_id)
60
101
 
@@ -64,6 +105,27 @@ module Bidi2pdf
64
105
  BrowserTab.new(client, browsing_context_id, context_id)
65
106
  end
66
107
  end
108
+
109
+ # Closes the user context.
110
+ #
111
+ # This method removes the user context from the browser, effectively cleaning up
112
+ # any associated resources. If the user context does not exist, the method does nothing.
113
+ #
114
+ # @return [nil]
115
+ # @raise [RuntimeError] If an error occurs while removing the user context.
116
+ def close
117
+ return unless context_id
118
+
119
+ res = client.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::BrowserRemoveUserContext.new(user_context_id: context_id)) do |response|
120
+ raise "Error removing user context: #{response.inspect}" if response["error"]
121
+
122
+ response["result"]
123
+ end
124
+
125
+ Bidi2pdf.logger.debug "User context deleted: #{res.inspect}"
126
+
127
+ @context_id = nil
128
+ end
67
129
  end
68
130
  end
69
131
  end
@@ -22,15 +22,15 @@ module Bidi2pdf
22
22
 
23
23
  # Add listeners
24
24
 
25
- def on_message(&block) = socket_events.on(:message, &block)
25
+ def on_message(&) = socket_events.on(:message, &)
26
26
 
27
- def on_event(name, &block) = session_events.on(name, &block)
27
+ def on_event(name, &) = session_events.on(name, &)
28
28
 
29
- def on_open(&block) = socket_events.on(:open, &block)
29
+ def on_open(&) = socket_events.on(:open, &)
30
30
 
31
- def on_close(&block) = socket_events.on(:close, &block)
31
+ def on_close(&) = socket_events.on(:close, &)
32
32
 
33
- def on_error(&block) = socket_events.on(:error, &block)
33
+ def on_error(&) = socket_events.on(:error, &)
34
34
 
35
35
  def remove_message_listener(block) = socket_events.off(:message, block)
36
36
 
@@ -52,10 +52,10 @@ module Bidi2pdf
52
52
  method = data["method"]
53
53
 
54
54
  if method
55
- Bidi2pdf.logger.debug "Dispatching session event: #{method}"
55
+ Bidi2pdf.logger.debug3 "Dispatching session event: #{method}"
56
56
  that.session_events.dispatch(method, data)
57
57
  else
58
- Bidi2pdf.logger.debug "Dispatching socket message"
58
+ Bidi2pdf.logger.debug3 "Dispatching socket message"
59
59
  that.socket_events.dispatch(:message, data)
60
60
  end
61
61
  end
@@ -5,13 +5,17 @@ require "securerandom"
5
5
 
6
6
  module Bidi2pdf
7
7
  class ChromedriverManager
8
- attr_reader :port, :pid, :started
8
+ include Chromedriver::Binary::Platform
9
9
 
10
- def initialize(port: 0, headless: true)
10
+ attr_reader :port, :pid, :started, :headless, :chrome_args, :shutdown_mutex
11
+
12
+ def initialize(port: 0, headless: true, chrome_args: Bidi::Session::DEFAULT_CHROME_ARGS)
11
13
  @port = port
12
14
  @headless = headless
13
15
  @session = nil
14
16
  @started = false
17
+ @chrome_args = chrome_args
18
+ @shutdown_mutex ||= Mutex.new
15
19
  end
16
20
 
17
21
  def start
@@ -23,11 +27,7 @@ module Bidi2pdf
23
27
  cmd = build_cmd
24
28
  Bidi2pdf.logger.info "Starting Chromedriver with command: #{cmd}"
25
29
 
26
- r, w = IO.pipe
27
- @pid = Process.spawn(cmd, out: w, err: w)
28
- w.close # close writer in parent
29
-
30
- parse_port_from_output(r)
30
+ spawn_process(cmd)
31
31
 
32
32
  Bidi2pdf.logger.info "Started Chromedriver on port #{@port}, PID #{@pid}"
33
33
  wait_until_chromedriver_ready
@@ -40,7 +40,7 @@ module Bidi2pdf
40
40
  def session
41
41
  return unless @started
42
42
 
43
- @session ||= Bidi::Session.new(session_url: session_url, headless: @headless)
43
+ @session ||= Bidi::Session.new(session_url: session_url, headless: @headless, chrome_args: @chrome_args)
44
44
  end
45
45
 
46
46
  def session_url
@@ -50,27 +50,54 @@ module Bidi2pdf
50
50
  end
51
51
 
52
52
  def stop(timeout: 5)
53
- return unless @pid
53
+ shutdown_mutex.synchronize do
54
+ return unless @pid
54
55
 
55
- @started = false
56
+ @started = false
56
57
 
57
- close_session
58
+ close_session
58
59
 
59
- debug_show_all_children
60
+ debug_show_all_children
60
61
 
61
- old_childprocesses = term_chromedriver
62
+ old_childprocesses = term_chromedriver
62
63
 
63
- detect_zombie_processes old_childprocesses
64
+ detect_zombie_processes old_childprocesses
64
65
 
65
- return unless process_alive?
66
+ return unless process_alive?
66
67
 
67
- kill_chromedriver timeout: timeout
68
- ensure
69
- @pid = nil
68
+ kill_chromedriver timeout: timeout
69
+ ensure
70
+ @pid = nil
71
+ @started = false
72
+ end
70
73
  end
71
74
 
72
75
  private
73
76
 
77
+ def spawn_process(cmd)
78
+ r, w = IO.pipe
79
+
80
+ options = {
81
+ out: w,
82
+ err: w,
83
+ close_others: true,
84
+ chdir: Dir.tmpdir
85
+ }
86
+
87
+ if platform == "win"
88
+ options[:new_pgroup] = true
89
+ else
90
+ options[:pgroup] = true
91
+ end
92
+
93
+ env = {}
94
+
95
+ @pid = Process.spawn(env, cmd, **options)
96
+ w.close # close writer in parent
97
+
98
+ parse_port_from_output(r)
99
+ end
100
+
74
101
  def detect_zombie_processes(old_childprocesses)
75
102
  Bidi2pdf.logger.debug "Old child processes for #{@pid}: #{old_childprocesses.map(&:pid).join(", ")}"
76
103
 
@@ -99,7 +126,7 @@ module Bidi2pdf
99
126
  Bidi2pdf::ProcessTree.new(@pid).traverse do |process, level|
100
127
  indent = " " * level
101
128
  prefix = level.zero? ? "" : "└─ "
102
- Bidi2pdf.logger.debug "#{indent}#{prefix}PID #{process.pid} (#{process.name})"
129
+ Bidi2pdf.logger.debug2 "#{indent}#{prefix}PID #{process.pid} (#{process.name})"
103
130
  end
104
131
  end
105
132
 
@@ -114,7 +141,7 @@ module Bidi2pdf
114
141
  Bidi2pdf::ProcessTree.new(@pid).children(@pid).tap do |_child_processes|
115
142
  Bidi2pdf.logger.info "Stopping Chromedriver (PID #{@pid})"
116
143
 
117
- Process.kill("TERM", @pid)
144
+ Process.kill("TERM", -@pid) # - meanskill linux pgroup
118
145
  end
119
146
  rescue Errno::ESRCH
120
147
  Bidi2pdf.logger.debug "Process already gone"
@@ -159,7 +186,7 @@ module Bidi2pdf
159
186
  def parse_port_from_output(io, timeout: 5)
160
187
  Thread.new do
161
188
  io.each_line do |line|
162
- Bidi2pdf.logger.debug line.chomp
189
+ Bidi2pdf.logger.debug1 line.chomp
163
190
 
164
191
  next unless line =~ /ChromeDriver was started successfully on port (\d+)/
165
192
 
data/lib/bidi2pdf/cli.rb CHANGED
@@ -51,6 +51,11 @@ module Bidi2pdf
51
51
  option :log_level,
52
52
  type: :string,
53
53
  default: "info", enum: %w[debug info warn error fatal unknown], desc: "Set log level"
54
+ verbosity_levels = Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS.keys.sort_by { |k| Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS[k] }
55
+ option :verbosity,
56
+ type: :string,
57
+ default: verbosity_levels.first, enum: Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS.keys.sort_by { |k| Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS[k] }.map(&:to_s),
58
+ desc: "Set debug verbosity level", aliases: "-v"
54
59
  option :log_network_traffic, type: :boolean, default: false, desc: "Log network traffic", aliases: "-n"
55
60
  option :network_log_format,
56
61
  type: :string,
@@ -216,12 +221,12 @@ module Bidi2pdf
216
221
  end
217
222
  end
218
223
 
219
- # rubocop:enable Metrics/AbcSize
220
-
221
224
  def configure
222
225
  Bidi2pdf.configure do |config|
223
226
  config.logger.level = log_level
224
227
 
228
+ config.logger.verbosity = merged_options[:verbosity]
229
+
225
230
  config.network_events_logger.level = Logger::INFO if merged_options[:log_network_traffic]
226
231
 
227
232
  config.default_timeout = merged_options[:default_timeout]
@@ -232,6 +237,8 @@ module Bidi2pdf
232
237
  end
233
238
  end
234
239
 
240
+ # rubocop: enable Metrics/MethodLength
241
+
235
242
  def log_level
236
243
  case merged_options[:log_level]
237
244
  when "debug" then Logger::DEBUG
@@ -266,3 +273,4 @@ module Bidi2pdf
266
273
  end
267
274
  end
268
275
  end
276
+ # rubocop:enable Metrics/AbcSize
data/lib/bidi2pdf/dsl.rb CHANGED
@@ -4,7 +4,39 @@ require "bidi2pdf"
4
4
 
5
5
  module Bidi2pdf
6
6
  module DSL
7
+ # Provides a DSL for managing browser sessions and tabs
8
+ # using the Bidi2pdf library. This module includes a method to create and manage
9
+ # browser tabs within a controlled session.
10
+
7
11
  # rubocop: disable Metrics/AbcSize
12
+ #
13
+ # Executes a block of code within the context of a browser tab.
14
+ #
15
+ # This method handles the setup and teardown of a browser session, user context,
16
+ # browser window, and tab. It ensures that resources are properly cleaned up
17
+ # after the block is executed.
18
+ #
19
+ # @param [String, nil] remote_browser_url The URL of a remote browser to connect to.
20
+ # If provided, the session will connect to this browser in headless mode.
21
+ # @param [Integer] port The port to use for the local browser session. Defaults to 0 (chooses a random port).
22
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
23
+ # @param [Array<String>] chrome_args Additional arguments to pass to the Chrome browser.
24
+ # Defaults to the `DEFAULT_CHROME_ARGS` from the `Bidi2pdf::Bidi::Session` class.
25
+ #
26
+ # @yield [tab] The browser tab created within the session.
27
+ # @yieldparam [Object] tab The browser tab object.
28
+ #
29
+ # @example Using a local browser session
30
+ # Bidi2pdf::DSL.with_tab(port: 9222, headless: false) do |tab|
31
+ # # Perform actions with the tab
32
+ # end
33
+ #
34
+ # @example Using a remote browser session
35
+ # Bidi2pdf::DSL.with_tab(remote_browser_url: "http://remote-browser:9222/session") do |tab|
36
+ # # Perform actions with the tab
37
+ # end
38
+ #
39
+ # @return [void]
8
40
  def self.with_tab(remote_browser_url: nil, port: 0, headless: true, chrome_args: Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS.dup)
9
41
  manager = nil
10
42
  session = nil
@@ -35,6 +67,7 @@ module Bidi2pdf
35
67
  ensure
36
68
  tab&.close
37
69
  window&.close
70
+ context&.close
38
71
  session&.close
39
72
  manager&.stop
40
73
  end