bidi2pdf 0.1.5 → 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 +61 -1
- data/README.md +47 -10
- data/docker/Dockerfile +11 -3
- data/docker/Dockerfile.chromedriver +4 -2
- data/docker/Dockerfile.slim +75 -0
- data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +415 -39
- 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 +66 -0
- data/lib/bidi2pdf/bidi/network_event.rb +40 -7
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +110 -0
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_formatter_utils.rb +53 -0
- data/lib/bidi2pdf/bidi/network_event_formatters/network_event_html_formatter.rb +125 -0
- data/lib/bidi2pdf/bidi/network_event_formatters.rb +11 -0
- data/lib/bidi2pdf/bidi/network_events.rb +46 -17
- data/lib/bidi2pdf/bidi/session.rb +120 -13
- 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 +27 -3
- data/lib/bidi2pdf/dsl.rb +33 -0
- data/lib/bidi2pdf/launcher.rb +34 -2
- 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 +49 -7
- data/lib/bidi2pdf/verbose_logger.rb +79 -0
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +99 -5
- data/sig/bidi2pdf/bidi/client.rbs +1 -1
- metadata +45 -4
- data/lib/bidi2pdf/utils.rb +0 -15
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
module NetworkEventFormatters
|
6
|
+
# rubocop: disable Metrics/AbcSize
|
7
|
+
class NetworkEventConsoleFormatter
|
8
|
+
include NetworkEventFormatterUtils
|
9
|
+
|
10
|
+
attr_reader :color_enabled, :logger
|
11
|
+
|
12
|
+
# ANSI styles
|
13
|
+
RESET = "\e[0m"
|
14
|
+
BOLD = "\e[1m"
|
15
|
+
DIM = "\e[2m"
|
16
|
+
RED = "\e[31m"
|
17
|
+
GREEN = "\e[32m"
|
18
|
+
YELLOW = "\e[33m"
|
19
|
+
CYAN = "\e[36m"
|
20
|
+
GRAY = "\e[90m"
|
21
|
+
|
22
|
+
def initialize(color: true, logger: Bidi2pdf.network_events_logger)
|
23
|
+
@color_enabled = color
|
24
|
+
@logger = logger
|
25
|
+
end
|
26
|
+
|
27
|
+
def log(events)
|
28
|
+
events.each { |event| pretty_log(event).each_line { |line| logger.info(line.chomp) } }
|
29
|
+
end
|
30
|
+
|
31
|
+
def pretty_log(event)
|
32
|
+
status = event.http_status_code ? "HTTP #{event.http_status_code}" : "pending"
|
33
|
+
status_color = color_for_status(event.http_status_code)
|
34
|
+
start = event.format_timestamp(event.start_timestamp)
|
35
|
+
finish = event.end_timestamp ? event.format_timestamp(event.end_timestamp) : dim("...")
|
36
|
+
duration = event.duration_seconds ? cyan("#{event.duration_seconds}s") : dim("in progress")
|
37
|
+
timing_details = format_timing(event)
|
38
|
+
bytes = event.bytes_received ? format_bytes(event.bytes_received) : dim("N/A")
|
39
|
+
displayed_url = shorten_url(event.url)
|
40
|
+
|
41
|
+
<<~LOG.strip
|
42
|
+
#{bold("┌─ Network Event ──────────────────────────────────────")}
|
43
|
+
#{bold("│ Request: ")}#{event.http_method || "?"} #{displayed_url}#{" "}
|
44
|
+
#{bold("│ State: ")}#{event.state}
|
45
|
+
#{bold("│ Status: ")}#{status_color}#{status}#{reset}
|
46
|
+
#{bold("│ Started: ")}#{start}
|
47
|
+
#{bold("│ Ended: ")}#{finish}
|
48
|
+
#{bold("│ Duration:")} #{duration}
|
49
|
+
#{bold("│ Received:")} #{bytes}
|
50
|
+
#{timing_details}
|
51
|
+
#{bold("└──────────────────────────────────────────────────────")}
|
52
|
+
LOG
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def format_timing(event)
|
58
|
+
return "" unless event.timing.is_a?(Hash)
|
59
|
+
|
60
|
+
keys = %w[
|
61
|
+
requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
|
62
|
+
sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
|
63
|
+
]
|
64
|
+
|
65
|
+
visible = keys.map do |key|
|
66
|
+
next unless event.timing[key]
|
67
|
+
|
68
|
+
label = key.gsub(/([A-Z])/, ' \1').capitalize
|
69
|
+
"#{dim("│")} #{label.ljust(20)}: #{event.timing[key].round(2)} ms#{reset}"
|
70
|
+
end.compact
|
71
|
+
|
72
|
+
return "" if visible.empty?
|
73
|
+
|
74
|
+
[dim("│").to_s, dim("│ Timing Phases:").to_s].concat(visible).join("\n")
|
75
|
+
end
|
76
|
+
|
77
|
+
# === Color Helpers ===
|
78
|
+
|
79
|
+
def color_for_status(code)
|
80
|
+
return gray unless code
|
81
|
+
|
82
|
+
case code.to_i
|
83
|
+
when 200..299 then green
|
84
|
+
when 300..499 then yellow
|
85
|
+
when 500..599 then red
|
86
|
+
else gray
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def bold(str) = color_enabled ? "#{BOLD}#{str}#{RESET}" : str
|
91
|
+
|
92
|
+
def dim(str) = color_enabled ? "#{DIM}#{str}#{RESET}" : str
|
93
|
+
|
94
|
+
def green(str = "") = color_enabled ? "#{GREEN}#{str}" : str
|
95
|
+
|
96
|
+
def yellow(str = "") = color_enabled ? "#{YELLOW}#{str}" : str
|
97
|
+
|
98
|
+
def red(str = "") = color_enabled ? "#{RED}#{str}" : str
|
99
|
+
|
100
|
+
def cyan(str = "") = color_enabled ? "#{CYAN}#{str}" : str
|
101
|
+
|
102
|
+
def gray(str = "") = color_enabled ? "#{GRAY}#{str}" : str
|
103
|
+
|
104
|
+
def reset = color_enabled ? RESET : ""
|
105
|
+
|
106
|
+
# rubocop: enable Metrics/AbcSize
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
module Bidi
|
7
|
+
module NetworkEventFormatters
|
8
|
+
module NetworkEventFormatterUtils
|
9
|
+
def format_bytes(size)
|
10
|
+
return "N/A" unless size.is_a?(Numeric)
|
11
|
+
|
12
|
+
units = %w[B KB MB GB TB]
|
13
|
+
idx = 0
|
14
|
+
while size >= 1024 && idx < units.size - 1
|
15
|
+
size /= 1024.0
|
16
|
+
idx += 1
|
17
|
+
end
|
18
|
+
format("%<size>.2f %<unit>s", size: size, unit: units[idx])
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_timing(event)
|
22
|
+
return [] unless event.timing.is_a?(Hash)
|
23
|
+
|
24
|
+
keys = %w[
|
25
|
+
requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
|
26
|
+
sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
|
27
|
+
]
|
28
|
+
|
29
|
+
keys.filter_map do |key|
|
30
|
+
next unless event.timing[key]
|
31
|
+
|
32
|
+
label = key.gsub(/([A-Z])/, ' \1').capitalize
|
33
|
+
{ label: label, key: key, ms: event.timing[key].round(2) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def format_timestamp(timestamp)
|
38
|
+
return "N/A" unless timestamp
|
39
|
+
|
40
|
+
Time.at(timestamp.to_f / 1000).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
|
41
|
+
end
|
42
|
+
|
43
|
+
def shorten_url(url)
|
44
|
+
sanitized_url = CGI.escapeHTML(url)
|
45
|
+
|
46
|
+
return sanitized_url unless sanitized_url.start_with?("data:text/html")
|
47
|
+
|
48
|
+
"data:text/html,..."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
module NetworkEventFormatters
|
6
|
+
class NetworkEventHtmlFormatter
|
7
|
+
include NetworkEventFormatterUtils
|
8
|
+
|
9
|
+
def render(events)
|
10
|
+
return unless Bidi2pdf.network_events_logger.info?
|
11
|
+
|
12
|
+
<<~HTML
|
13
|
+
<!DOCTYPE html>
|
14
|
+
<html lang="en" data-bs-theme="light">
|
15
|
+
<head>
|
16
|
+
<meta charset="UTF-8">
|
17
|
+
<title>Network Events</title>
|
18
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
19
|
+
<style>
|
20
|
+
body { font-family: monospace; padding: 2rem; }
|
21
|
+
.event { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); padding: 1rem; margin-bottom: 2rem; border-radius: .5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
22
|
+
.bar-container { position: relative; height: 20px; background: var(--bs-secondary-bg); border-radius: 4px; }
|
23
|
+
.bar { position: absolute; top: 0; height: 100%; background-color: #0d6efd; opacity: 0.8; }
|
24
|
+
.toc a { text-decoration: none; display: block; margin-bottom: 0.5rem; }
|
25
|
+
|
26
|
+
@media print {
|
27
|
+
.no-break {
|
28
|
+
page-break-inside: avoid;
|
29
|
+
}
|
30
|
+
#{" "}
|
31
|
+
#{" "}
|
32
|
+
.form-select, .form-label, #theme-select {
|
33
|
+
display: none !important; /* Hide theme selector when printing */
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
.event {
|
38
|
+
word-break: break-word;
|
39
|
+
overflow-wrap: anywhere;
|
40
|
+
}
|
41
|
+
</style>
|
42
|
+
<script>
|
43
|
+
function toggleTheme(value) {
|
44
|
+
document.documentElement.setAttribute('data-bs-theme', value);
|
45
|
+
}
|
46
|
+
</script>
|
47
|
+
</head>
|
48
|
+
<body>
|
49
|
+
<h1>Network Events</h1>
|
50
|
+
<div class="mb-4">
|
51
|
+
<label for="theme-select" class="form-label">Theme:</label>
|
52
|
+
<select id="theme-select" class="form-select w-auto d-inline-block" onchange="toggleTheme(this.value)">
|
53
|
+
<option value="light">Light</option>
|
54
|
+
<option value="dark">Dark</option>
|
55
|
+
</select>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<h2>Index</h2>
|
59
|
+
<div class="toc mb-4">
|
60
|
+
#{events.map.with_index { |e, i| toc_entry(e, i) }.join("\n")}
|
61
|
+
</div>
|
62
|
+
|
63
|
+
#{events.map.with_index { |e, i| render_event(e, i) }.join("\n")}
|
64
|
+
</body>
|
65
|
+
</html>
|
66
|
+
HTML
|
67
|
+
end
|
68
|
+
|
69
|
+
def toc_entry(event, index)
|
70
|
+
"<a href=\"#event-#{index}\">[#{index + 1}] #{event.http_method} #{event.url}</a>"
|
71
|
+
end
|
72
|
+
|
73
|
+
# rubocop: disable Metrics/AbcSize
|
74
|
+
def render_event(event, index)
|
75
|
+
timing = parse_timing(event)
|
76
|
+
duration = event.duration_seconds || 0
|
77
|
+
duration_str = event.in_progress? ? "in progress" : "#{duration}s"
|
78
|
+
status = event.http_status_code || "?"
|
79
|
+
method = event.http_method || "?"
|
80
|
+
start = format_timestamp(event.start_timestamp)
|
81
|
+
finish = event.end_timestamp ? format_timestamp(event.end_timestamp) : "..."
|
82
|
+
bytes = event.bytes_received ? format_bytes(event.bytes_received) : "N/A"
|
83
|
+
bars = render_timing_bars(timing)
|
84
|
+
displayed_url = shorten_url(event.url)
|
85
|
+
|
86
|
+
<<~HTML
|
87
|
+
<div class="event no-break" id="event-#{index}">
|
88
|
+
<div><strong>Request:</strong> #{method} #{displayed_url}</div>
|
89
|
+
<div><strong>Status:</strong> HTTP #{status}</div>
|
90
|
+
<div><strong>State:</strong> #{event.state}</div>
|
91
|
+
<div><strong>Start:</strong> #{start}</div>
|
92
|
+
<div><strong>End:</strong> #{finish}</div>
|
93
|
+
<div><strong>Duration:</strong> #{duration_str}</div>
|
94
|
+
<div><strong>Received:</strong> #{bytes}</div>
|
95
|
+
#{bars}
|
96
|
+
</div>
|
97
|
+
HTML
|
98
|
+
end
|
99
|
+
|
100
|
+
# rubocop: enable Metrics/AbcSize
|
101
|
+
|
102
|
+
def render_timing_bars(timing)
|
103
|
+
return "" if timing.empty?
|
104
|
+
|
105
|
+
max_ms = timing.map { |t| t[:ms] }.max
|
106
|
+
scale = max_ms.zero? ? 0 : 100.0 / max_ms
|
107
|
+
|
108
|
+
bars = timing.map do |t|
|
109
|
+
width = (t[:ms] * scale).clamp(1, 100).round(2)
|
110
|
+
<<~HTML
|
111
|
+
<div>
|
112
|
+
<small>#{t[:label]} (#{t[:ms]} ms)</small>
|
113
|
+
<div class="bar-container mb-2">
|
114
|
+
<div class="bar" style="width: #{width}%"></div>
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
HTML
|
118
|
+
end
|
119
|
+
|
120
|
+
"<div class=\"mt-3\"><strong>Timing Waterfall</strong>#{bars.join}</div>"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
module NetworkEventFormatters
|
6
|
+
require_relative "network_event_formatters/network_event_formatter_utils"
|
7
|
+
require_relative "network_event_formatters/network_event_console_formatter"
|
8
|
+
require_relative "network_event_formatters/network_event_html_formatter"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "network_event"
|
4
|
+
require_relative "network_event_formatters"
|
4
5
|
|
5
6
|
module Bidi2pdf
|
6
7
|
module Bidi
|
7
8
|
class NetworkEvents
|
8
|
-
attr_reader :context_id, :events
|
9
|
+
attr_reader :context_id, :events, :network_event_formatter
|
9
10
|
|
10
11
|
def initialize(context_id)
|
11
12
|
@context_id = context_id
|
12
13
|
@events = {}
|
14
|
+
@network_event_formatter = NetworkEventFormatters::NetworkEventConsoleFormatter.new
|
13
15
|
end
|
14
16
|
|
15
17
|
def handle_event(data)
|
@@ -19,46 +21,73 @@ module Bidi2pdf
|
|
19
21
|
if event["context"] == context_id
|
20
22
|
handle_response(method, event)
|
21
23
|
else
|
22
|
-
Bidi2pdf.logger.
|
24
|
+
Bidi2pdf.logger.debug3 "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
|
23
25
|
end
|
24
26
|
rescue StandardError => e
|
25
27
|
Bidi2pdf.logger.error "Error handling network event: #{e.message}"
|
26
28
|
end
|
27
29
|
|
28
|
-
# rubocop:disable Metrics/AbcSize
|
30
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
29
31
|
def handle_response(method, event)
|
30
32
|
return unless event && event["request"]
|
31
33
|
|
32
34
|
request = event["request"]
|
35
|
+
response = event["response"]
|
36
|
+
http_status_code = response&.dig("status")
|
37
|
+
bytes_received = response&.dig("bytesReceived")
|
33
38
|
|
34
39
|
id = request["request"]
|
35
40
|
url = request["url"]
|
36
41
|
timing = request["timings"]
|
42
|
+
http_method = request["method"]
|
37
43
|
|
38
44
|
timestamp = event["timestamp"]
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
52
69
|
end
|
53
70
|
end
|
54
71
|
|
55
|
-
# rubocop:enable Metrics/AbcSize
|
72
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
56
73
|
|
57
74
|
def all_events
|
58
75
|
events.values.sort_by(&:start_timestamp)
|
59
76
|
end
|
60
77
|
|
61
|
-
def
|
78
|
+
def log_network_traffic(format: :console)
|
79
|
+
format = format.to_sym
|
80
|
+
|
81
|
+
if format == :console
|
82
|
+
NetworkEventFormatters::NetworkEventConsoleFormatter.new.log all_events
|
83
|
+
elsif format == :html
|
84
|
+
NetworkEventFormatters::NetworkEventHtmlFormatter.new.render(all_events)
|
85
|
+
else
|
86
|
+
raise ArgumentError, "Unknown network event format: #{format}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def wait_until_network_idle(timeout: 10, poll_interval: 0.01)
|
62
91
|
start_time = Time.now
|
63
92
|
|
64
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
|
13
|
-
|
31
|
+
# Events to subscribe to during the session.
|
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
|