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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +32 -4
- data/README.md +14 -0
- data/docker/Dockerfile +1 -1
- data/docker/Dockerfile.chromedriver +1 -1
- data/docker/Dockerfile.slim +2 -2
- data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +391 -41
- data/lib/bidi2pdf/bidi/client.rb +85 -23
- data/lib/bidi2pdf/bidi/command_manager.rb +46 -48
- data/lib/bidi2pdf/bidi/commands/base.rb +39 -1
- data/lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb +27 -0
- data/lib/bidi2pdf/bidi/commands/browsing_context_print.rb +4 -0
- data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +5 -0
- data/lib/bidi2pdf/bidi/commands.rb +1 -0
- data/lib/bidi2pdf/bidi/event_manager.rb +1 -1
- data/lib/bidi2pdf/bidi/interceptor.rb +1 -1
- data/lib/bidi2pdf/bidi/js_logger_helper.rb +16 -0
- data/lib/bidi2pdf/bidi/logger_events.rb +25 -45
- data/lib/bidi2pdf/bidi/network_event.rb +15 -0
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +4 -3
- data/lib/bidi2pdf/bidi/network_events.rb +27 -17
- data/lib/bidi2pdf/bidi/session.rb +119 -12
- data/lib/bidi2pdf/bidi/user_context.rb +62 -0
- data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +7 -7
- data/lib/bidi2pdf/chromedriver_manager.rb +48 -21
- data/lib/bidi2pdf/cli.rb +10 -2
- data/lib/bidi2pdf/dsl.rb +33 -0
- data/lib/bidi2pdf/launcher.rb +30 -0
- data/lib/bidi2pdf/notifications/event.rb +52 -0
- data/lib/bidi2pdf/notifications/instrumenter.rb +65 -0
- data/lib/bidi2pdf/notifications/logging_subscriber.rb +136 -0
- data/lib/bidi2pdf/notifications.rb +78 -0
- data/lib/bidi2pdf/session_runner.rb +35 -3
- data/lib/bidi2pdf/verbose_logger.rb +79 -0
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +99 -7
- data/sig/bidi2pdf/bidi/client.rbs +1 -1
- metadata +39 -4
- 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.
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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.
|
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
|
-
|
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
|
-
|
48
|
-
Bidi2pdf.
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
83
|
-
|
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
|
-
#
|
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:
|
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(&
|
25
|
+
def on_message(&) = socket_events.on(:message, &)
|
26
26
|
|
27
|
-
def on_event(name, &
|
27
|
+
def on_event(name, &) = session_events.on(name, &)
|
28
28
|
|
29
|
-
def on_open(&
|
29
|
+
def on_open(&) = socket_events.on(:open, &)
|
30
30
|
|
31
|
-
def on_close(&
|
31
|
+
def on_close(&) = socket_events.on(:close, &)
|
32
32
|
|
33
|
-
def on_error(&
|
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.
|
55
|
+
Bidi2pdf.logger.debug3 "Dispatching session event: #{method}"
|
56
56
|
that.session_events.dispatch(method, data)
|
57
57
|
else
|
58
|
-
Bidi2pdf.logger.
|
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
|
-
|
8
|
+
include Chromedriver::Binary::Platform
|
9
9
|
|
10
|
-
|
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
|
-
|
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
|
-
|
53
|
+
shutdown_mutex.synchronize do
|
54
|
+
return unless @pid
|
54
55
|
|
55
|
-
|
56
|
+
@started = false
|
56
57
|
|
57
|
-
|
58
|
+
close_session
|
58
59
|
|
59
|
-
|
60
|
+
debug_show_all_children
|
60
61
|
|
61
|
-
|
62
|
+
old_childprocesses = term_chromedriver
|
62
63
|
|
63
|
-
|
64
|
+
detect_zombie_processes old_childprocesses
|
64
65
|
|
65
|
-
|
66
|
+
return unless process_alive?
|
66
67
|
|
67
|
-
|
68
|
-
|
69
|
-
|
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.
|
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",
|
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.
|
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
|