bidi2pdf 0.1.7 → 0.1.9

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -8
  3. data/README.md +14 -0
  4. data/docker/Dockerfile.chromedriver +30 -5
  5. data/docker/entrypoint.sh +41 -0
  6. data/lib/bidi2pdf/bidi/browser_tab.rb +59 -8
  7. data/lib/bidi2pdf/bidi/client.rb +7 -5
  8. data/lib/bidi2pdf/bidi/command_manager.rb +14 -26
  9. data/lib/bidi2pdf/bidi/connection_manager.rb +3 -9
  10. data/lib/bidi2pdf/bidi/event_manager.rb +35 -5
  11. data/lib/bidi2pdf/bidi/interceptor.rb +12 -2
  12. data/lib/bidi2pdf/bidi/navigation_failed_events.rb +41 -0
  13. data/lib/bidi2pdf/bidi/session.rb +6 -1
  14. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +5 -5
  15. data/lib/bidi2pdf/chromedriver_manager.rb +25 -11
  16. data/lib/bidi2pdf/notifications.rb +1 -1
  17. data/lib/bidi2pdf/test_helpers/matchers/contains_pdf_text.rb +50 -0
  18. data/lib/bidi2pdf/test_helpers/matchers/have_pdf_page_count.rb +50 -0
  19. data/lib/bidi2pdf/test_helpers/matchers/match_pdf_text.rb +45 -0
  20. data/lib/bidi2pdf/test_helpers/pdf_reader_utils.rb +89 -0
  21. data/lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb +232 -0
  22. data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +81 -0
  23. data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +103 -0
  24. data/lib/bidi2pdf/test_helpers/testcontainers/shared_docker_network.rb +21 -0
  25. data/lib/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rb +53 -0
  26. data/lib/bidi2pdf/test_helpers/testcontainers.rb +17 -0
  27. data/lib/bidi2pdf/test_helpers.rb +13 -0
  28. data/lib/bidi2pdf/version.rb +1 -1
  29. data/lib/bidi2pdf.rb +32 -3
  30. data/sig/bidi2pdf/bidi/event_manager.rbs +19 -13
  31. metadata +35 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 766b41f0ee642cd7316d0f72d8dd707b0f45aae4a315a46c2b27fb6bb2d176a6
4
- data.tar.gz: aeec0549f82ff7bdd68d1aa658ea6ad2033e5310fd5936f40b94007b4ae6c38f
3
+ metadata.gz: 3d7fa3c853f53e21a110cadb25fcd66bf97b890eb00663f90e73e8ee9d1ce07e
4
+ data.tar.gz: 5900568c47f526e9b00d95a15f8293abeaee88f6c0f323bc6e79cdcb3aba38ef
5
5
  SHA512:
6
- metadata.gz: cc7f1da58549b642521808b9ea2acc4b04068bdb7c877cf52943d2ae69bb989f2ade02601b8bfd0e409440ad8644206bba1e6e16603eb56099b8963a2136e350
7
- data.tar.gz: 6258250ac5de22034cbb7816d3ff33c62680747a3eaee171fdd784d309bf1cd7880ca60e45342da8ee9195814ef1c78ca9b45b2c435f9fcbc4aaa43a8d7f95e6
6
+ metadata.gz: 9ecffa81a6358c413dd24b1cef48c3b2282518f5934710558b2ea834a362b198d5a1e3f01a2b3eb8ed163dedffaae61fe2bb0c5f3edec5e3850ad375daf10304
7
+ data.tar.gz: 0b9d4b19f9d2d01a8babfe2a13c4256b9f93d34f2fcdc1ea2942fb780ce4ad02962050f1adffd9d2c5731e2e6c6d1dd83deaa3d31b348157197c982701a8995b
data/CHANGELOG.md CHANGED
@@ -7,8 +7,65 @@ All notable changes to this project will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.8..HEAD
11
+
10
12
  <!-- generated by git-cliff end -->
11
13
 
14
+ ## [0.1.9] - 2025-05-04
15
+
16
+ ### 🎨 Refactored
17
+
18
+ - Enhance reader thread management in Chromedriver by @dieter-medium
19
+ - Improve event listener logging by @dieter-medium
20
+ - Improve event listener management by @dieter-medium
21
+ - Centralize chromedriver test helpers for reuse in other projects by @dieter-medium
22
+
23
+ ### 🐛 Fixed
24
+
25
+ - Close event socket during session cleanup by @dieter-medium
26
+ - Add test for generating PDFs in parallel by @dieter-medium
27
+
28
+ ### 💄 Style
29
+
30
+ - Update Bootstrap stylesheet link by @dieter-medium
31
+
32
+ ### 🔧 Build
33
+
34
+ - Update Ruby version in action.yml by @dieter-medium
35
+ - Update Dockerfile.chromedriver and chromedriver.yml for enhancements by @dieter-medium
36
+ - Update Dockerfile.chromedriver for improved environment setup and vnc support by @dieter-medium
37
+
38
+ ### 🚀 Added
39
+
40
+ - Enhance testcontainers with shared network support by @dieter-medium
41
+
42
+ ## [0.1.8] - 2025-04-22
43
+
44
+ ### 🎨 Refactored
45
+
46
+ - Modularize ChromedriverContainer implementation by @dieter-medium
47
+ - Replace method calls for clarity and consistency by @dieter-medium
48
+ - Namespace PDFTextSanitizer under Bidi2pdf::TestHelpers by @dieter-medium
49
+ - Refactor command management with concurrent queues by @dieter-medium
50
+
51
+ ### 🐛 Fixed
52
+
53
+ - Update CHANGELOG links to correct Markdown syntax by @dieter-medium
54
+
55
+ ### 📝 Docs
56
+
57
+ - Add Rails integration section to README by @dieter-medium
58
+
59
+ ### 🚀 Added
60
+
61
+ - Update Chromedriver container setup and default image by @dieter-medium
62
+ - Add workflow for pushing Chromedriver Docker image by @dieter-medium
63
+ - Return session status and add test coverage by @dieter-medium
64
+ - Integrate concurrent-ruby for thread safety improvements by @dieter-medium
65
+ - Add specific navigation error classes for better handling by @dieter-medium
66
+ - Enhance navigation error handling in BrowserTab by @dieter-medium
67
+ - Add test helpers and matchers for PDF validation by @dieter-medium
68
+
12
69
  ## [0.1.7] - 2025-04-17
13
70
 
14
71
  ### 🎨 Refactored
@@ -143,12 +200,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
143
200
 
144
201
  - Initial release
145
202
 
146
- [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.7..HEAD
147
-
148
- [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.6..v0.1.7
149
-
150
- [0.1.6]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.5..v0.1.6
151
-
152
- [0.1.5]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.4..v0.1.5
153
203
 
154
- [0.1.4]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.3..v0.1.4
204
+ - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.9..HEAD)
205
+ - [0.1.9](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.8..v0.1.9)
206
+ - [0.1.8](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.7..v0.1.8)
207
+ - [0.1.7](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.6..v0.1.7)
208
+ - [0.1.6](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.5..v0.1.6)
209
+ - [0.1.5](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.4..v0.1.5)
210
+ - [0.1.4](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.3..v0.1.4)
data/README.md CHANGED
@@ -257,6 +257,20 @@ docker compose -f docker/docker-compose.yml down
257
257
 
258
258
  ---
259
259
 
260
+ ## 🚂 Rails Integration
261
+
262
+ Rails integration is available as an additional gem:
263
+
264
+ ```ruby
265
+ # In your Gemfile
266
+ gem 'bidi2pdf-rails'
267
+ ```
268
+
269
+ For full documentation and usage examples,
270
+ visit: [https://github.com/dieter-medium/bidi2pdf-rails](https://github.com/dieter-medium/bidi2pdf-rails)
271
+
272
+ ---
273
+
260
274
  ## 🛠 Development
261
275
 
262
276
  ```bash
@@ -1,13 +1,16 @@
1
- FROM ruby:3.3
1
+ FROM debian:bookworm-slim
2
2
 
3
3
  ARG CHROMEDRIVER_PORT=3000
4
4
 
5
5
  ENV DEBIAN_FRONTEND=noninteractive
6
+ ENV LANG=en_US.UTF-8
6
7
 
7
8
  # Install dependencies
8
- RUN apt-get update && apt-get upgrade -y && \
9
+ RUN echo "deb http://deb.debian.org/debian bookworm contrib non-free" > /etc/apt/sources.list.d/contrib.list &&\
10
+ echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections &&\
11
+ apt-get update && apt-get upgrade -y && \
9
12
  apt-get install -y --no-install-recommends\
10
- chromium \
13
+ chromium chromium-driver chromium-l10n chromium-sandbox\
11
14
  libglib2.0-0 \
12
15
  libnss3 \
13
16
  libxss1 \
@@ -18,23 +21,45 @@ RUN apt-get update && apt-get upgrade -y && \
18
21
  curl \
19
22
  unzip \
20
23
  xvfb \
24
+ x11vnc \
25
+ fluxbox \
26
+ xterm \
27
+ wmctrl \
28
+ net-tools xauth \
29
+ fonts-liberation fonts-dejavu-core fonts-noto-core fonts-noto-cjk fonts-noto-color-emoji fonts-symbola fontconfig ttf-mscorefonts-installer\
30
+ libnss3 libatk1.0-0 \
31
+ libx11-6 libxss1 libgtk-3-0 libgbm1 \
32
+ locales sed \
33
+ && sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
34
+ && locale-gen en_US.UTF-8 \
21
35
  && rm -rf /var/lib/apt/lists/*
22
36
 
23
37
  # Create a non-root user
24
- RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
38
+ RUN groupadd -r appuser && useradd -r -g appuser -G audio,video -m -d /home/appuser appuser
25
39
 
26
40
  COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
27
41
  RUN chmod +x /usr/local/bin/entrypoint.sh
28
42
 
43
+ # ARM compatibility workaround:
44
+ # On ARM architectures (such as Apple Silicon), downloading chromedriver via automated scripts may fail or cause ELF binary errors,
45
+ # such as "rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2".
46
+ # To avoid these issues, we directly install 'chromium-driver' via the package manager and explicitly create a symlink in the expected location.
47
+
48
+ RUN mkdir -p /home/appuser/.webdrivers && ln -s /usr/bin/chromedriver /home/appuser/.webdrivers/chromedriver
49
+
29
50
  # Set working directory
30
51
  WORKDIR /app
31
52
 
53
+ RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
54
+
32
55
  # Switch to non-root user
33
56
  USER appuser
34
57
 
35
- RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
58
+ # RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
36
59
 
37
60
  ENV CHROMEDRIVER_PORT=${CHROMEDRIVER_PORT}
38
61
  EXPOSE ${CHROMEDRIVER_PORT}
62
+ # VNC
63
+ EXPOSE 5900
39
64
 
40
65
  CMD ["/usr/local/bin/entrypoint.sh"]
data/docker/entrypoint.sh CHANGED
@@ -3,6 +3,47 @@
3
3
  USER_DATA_DIR=/home/appuser/.cache
4
4
  mkdir -p ${USER_DATA_DIR}
5
5
 
6
+ if [ "$ENABLE_XVFB" = "true" ]; then
7
+ rm -rf /tmp/.X99-lock
8
+
9
+ export DISPLAY=:99
10
+ Xvfb :99 -screen 0 1920x1080x24 &
11
+
12
+ old_umask=$(umask)
13
+ umask 077
14
+
15
+ touch /home/appuser/.Xauthority
16
+ export XAUTHORITY=/home/appuser/.Xauthority
17
+
18
+ xauth generate :99 . trusted
19
+
20
+ umask $old_umask
21
+
22
+
23
+
24
+ until xdpyinfo -display ${DISPLAY} >/dev/null 2>&1; do
25
+ sleep 0.2
26
+ done
27
+
28
+ fluxbox &
29
+
30
+ until wmctrl -m > /dev/null 2>&1; do
31
+ sleep 0.2
32
+ done
33
+ fi
34
+
35
+ if [ "$ENABLE_VNC" = "true" ]; then
36
+ VNC_PASS=${VNC_PASS:-$(tr -dc A-Za-z0-9 </dev/urandom | head -c 12)}
37
+ echo "VNC password: $VNC_PASS"
38
+ old_umask=$(umask)
39
+ umask 077
40
+ mkdir -p /home/appuser/.vnc
41
+ x11vnc -storepasswd $VNC_PASS /home/appuser/.vnc/passwd
42
+ umask $old_umask
43
+ x11vnc -display WAIT:99 -xkb -noxrecord -noxfixes -noxdamage -forever -shared -noshm -usepw -rfbauth /home/appuser/.vnc/passwd &
44
+ fi
45
+
46
+ # DISPLAY=:99 /home/appuser/.webdrivers/chromedriver --port=33259 --whitelisted-ips="" --allowed-origins="*" --disable-dev-shm-usage --disable-gpu --verbose
6
47
  /home/appuser/.webdrivers/chromedriver --port=${CHROMEDRIVER_PORT} \
7
48
  --headless \
8
49
  --whitelisted-ips="" \
@@ -4,6 +4,7 @@ require "base64"
4
4
 
5
5
  require_relative "network_events"
6
6
  require_relative "logger_events"
7
+ require_relative "navigation_failed_events"
7
8
  require_relative "auth_interceptor"
8
9
  require_relative "add_headers_interceptor"
9
10
  require_relative "js_logger_helper"
@@ -32,6 +33,11 @@ require_relative "js_logger_helper"
32
33
  # @param [String] user_context_id The ID of the user context.
33
34
  module Bidi2pdf
34
35
  module Bidi
36
+ # Represents a browser tab for managing interactions and communication
37
+ # using the Bidi2pdf library. This class provides methods for creating
38
+ # browser tabs, managing cookies, navigating to URLs, executing scripts,
39
+ # handling network events, and general tab lifecycle management.
40
+ #
35
41
  class BrowserTab
36
42
  include JsLoggerHelper
37
43
 
@@ -56,6 +62,9 @@ module Bidi2pdf
56
62
  # @return [LoggerEvents] The logger events handler.
57
63
  attr_reader :logger_events
58
64
 
65
+ # @return [NavigationFailedEvents] The navigation failed events handler.
66
+ attr_reader :navigation_failed_events
67
+
59
68
  # Initializes a new browser tab.
60
69
  #
61
70
  # @param [Object] client The WebSocket client for communication.
@@ -68,6 +77,7 @@ module Bidi2pdf
68
77
  @tabs = []
69
78
  @network_events = NetworkEvents.new browsing_context_id
70
79
  @logger_events = LoggerEvents.new browsing_context_id
80
+ @navigation_failed_events = NavigationFailedEvents.new browsing_context_id
71
81
  @open = true
72
82
  end
73
83
 
@@ -131,7 +141,7 @@ module Bidi2pdf
131
141
  headers:,
132
142
  url_patterns:
133
143
  )
134
- AddHeadersInterceptor.new(
144
+ @header_interceptor = AddHeadersInterceptor.new(
135
145
  context: browsing_context_id,
136
146
  url_patterns: url_patterns,
137
147
  headers: headers
@@ -145,7 +155,7 @@ module Bidi2pdf
145
155
  # @param [Array<String>] url_patterns The URL patterns to match.
146
156
  # @return [AuthInterceptor] The interceptor instance.
147
157
  def basic_auth(username:, password:, url_patterns:)
148
- AuthInterceptor.new(
158
+ @basic_auth_interceptor = AuthInterceptor.new(
149
159
  context: browsing_context_id,
150
160
  url_patterns: url_patterns,
151
161
  username: username, password: password
@@ -154,8 +164,21 @@ module Bidi2pdf
154
164
 
155
165
  # Navigates the browser tab to a specified URL.
156
166
  #
167
+ # This method registers necessary event listeners and sends a navigation
168
+ # command to the browser tab, instructing it to load the specified URL.
169
+ # It validates that the URL is properly formatted before attempting navigation.
170
+ #
157
171
  # @param [String] url The URL to navigate to.
172
+ # @raise [NavigationError] If the URL is invalid or improperly formatted.
173
+ # @example
174
+ # browser_tab.navigate_to("https://example.com")
158
175
  def navigate_to(url)
176
+ begin
177
+ URI.parse(url)
178
+ rescue URI::InvalidURIError => e
179
+ raise NavigationError, "Invalid URL: #{url} - #{e.message}"
180
+ end
181
+
159
182
  Bidi2pdf.notification_service.instrument("navigate_to.bidi2pdf", url: url) do
160
183
  navigate_with_listeners url
161
184
  end
@@ -389,18 +412,32 @@ module Bidi2pdf
389
412
  client.send_cmd_and_wait(cmd) do |response|
390
413
  Bidi2pdf.logger.debug "Navigated to page url: #{url} response: #{response}"
391
414
  end
415
+ rescue Bidi2pdf::CmdError => e
416
+ msg = e.response["message"]
417
+ case msg
418
+ when /^net::ERR_INVALID_AUTH_CREDENTIALS/
419
+ raise NavigationAuthError.new(url, msg)
420
+ when /^net::ERR_NAME_NOT_RESOLVED/
421
+ raise NavigationDNSError.new(url, msg)
422
+ when /^net::/
423
+ raise NavigationError, "Connection error: #{url} #{msg}"
424
+ else
425
+ raise e
426
+ end
392
427
  end
393
428
 
394
429
  def register_event_listeners
395
430
  return if @event_handlers_registered
396
431
 
397
432
  @event_handlers_registered = true
433
+ @listener_refs ||= {}
398
434
 
399
- client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
400
- &network_events.method(:handle_event))
435
+ @listener_refs[:network] = client.on_event("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
436
+ &network_events.method(:handle_event))
401
437
 
402
- client.on_event("log.entryAdded",
403
- &logger_events.method(:handle_event))
438
+ @listener_refs[:logger] = client.on_event("log.entryAdded", &logger_events.method(:handle_event))
439
+
440
+ @listener_refs[:navigation_failed] = client.on_event("browsingContext.navigationFailed", &navigation_failed_events.method(:handle_event))
404
441
  end
405
442
 
406
443
  def handle_injection_exception(response, url, exception_class)
@@ -531,13 +568,27 @@ module Bidi2pdf
531
568
  end
532
569
 
533
570
  # Removes event listeners for the browser tab.
571
+ # rubocop:disable Metrics/AbcSize
534
572
  def remove_event_listeners
573
+ return if @listener_refs.nil? || @listener_refs.empty?
574
+
535
575
  Bidi2pdf.logger.debug2 "Network events: #{network_events.all_events.map(&:to_s)}"
536
576
 
537
- client.remove_event_listener "network.responseStarted", "network.responseCompleted", "network.fetchError",
538
- &network_events.method(:handle_event)
577
+ client.remove_event_listener("network.beforeRequestSent", "network.responseStarted", "network.responseCompleted", "network.fetchError",
578
+ @listener_refs[:network])
579
+
580
+ client.remove_event_listener("log.entryAdded", @listener_refs[:logger])
581
+
582
+ @header_interceptor&.unregister_with_client(client: client)
583
+ @basic_auth_interceptor&.unregister_with_client(client: client)
584
+
585
+ @header_interceptor = nil
586
+ @basic_auth_interceptor = nil
587
+ @listener_refs = {}
539
588
  end
540
589
 
590
+ # rubocop:enable Metrics/AbcSize
591
+
541
592
  # Closes all tabs associated with the browser tab.
542
593
  def close_tabs
543
594
  tabs.each do |tab|
@@ -125,23 +125,25 @@ module Bidi2pdf
125
125
  #
126
126
  # @param [Array<String>] names The names of the events to subscribe to.
127
127
  # @yield [event_data] A block to handle the event data.
128
- def on_event(*names, &block)
129
- names.each { |name| dispatcher.on_event(name, &block) }
128
+ def on_event(*names, &)
129
+ listener = dispatcher.on_event(*names, &)
130
130
  cmd = Bidi2pdf::Bidi::Commands::SessionSubscribe.new(events: names)
131
131
  send_cmd(cmd) if names.any?
132
+
133
+ listener
132
134
  end
133
135
 
134
136
  # Removes a message listener.
135
137
  #
136
138
  # @param [Proc] block The listener block to remove.
137
- def remove_message_listener(block) = dispatcher.remove_message_listener(block)
139
+ def remove_message_listener(listener) = dispatcher.remove_message_listener(listener)
138
140
 
139
141
  # Removes event listeners for specific events.
140
142
  #
141
143
  # @param [Array<String>] names The names of the events to unsubscribe from.
142
144
  # @param [Proc] block The listener block to remove.
143
- def remove_event_listener(*names, &block)
144
- names.each { |event_name| dispatcher.remove_event_listener(event_name, block) }
145
+ def remove_event_listener(*names, listener)
146
+ names.each { |event_name| dispatcher.remove_event_listener(event_name, listener) }
145
147
  end
146
148
 
147
149
  # Closes the WebSocket connection.
@@ -5,11 +5,10 @@ module Bidi2pdf
5
5
  class CommandManager
6
6
  class << self
7
7
  def initialize_counter
8
- @id = 0
9
- @id_mutex = Mutex.new
8
+ @id = Concurrent::AtomicFixnum.new(0)
10
9
  end
11
10
 
12
- def next_id = @id_mutex.synchronize { @id += 1 }
11
+ def next_id = @id.increment
13
12
  end
14
13
 
15
14
  initialize_counter
@@ -17,19 +16,14 @@ module Bidi2pdf
17
16
  def initialize(socket)
18
17
  @socket = socket
19
18
 
20
- @pending_responses = {}
21
- @initiated_cmds = {}
19
+ @pending_responses = Concurrent::Hash.new
22
20
  end
23
21
 
24
- def send_cmd(cmd, store_response: false)
22
+ def send_cmd(cmd, result_queue: nil)
25
23
  id = next_id
26
24
 
27
25
  Bidi2pdf.notification_service.instrument("send_cmd.bidi2pdf", id: id, cmd: cmd) do |instrumentation_payload|
28
- if store_response
29
- init_queue_for id
30
- else
31
- @initiated_cmds[id] = true
32
- end
26
+ init_queue_for id, result_queue
33
27
 
34
28
  payload = cmd.as_payload(id)
35
29
 
@@ -42,17 +36,20 @@ module Bidi2pdf
42
36
  end
43
37
 
44
38
  def send_cmd_and_wait(cmd, timeout: Bidi2pdf.default_timeout, &block)
39
+ result_queue = Thread::Queue.new
40
+
45
41
  Bidi2pdf.notification_service.instrument("send_cmd_and_wait.bidi2pdf", cmd: cmd, timeout: timeout) do |instrumentation_payload|
46
- id = send_cmd(cmd, store_response: true)
42
+ id = send_cmd(cmd, result_queue: result_queue)
47
43
 
48
44
  instrumentation_payload[:id] = id
49
45
 
50
- response = pop_response id, timeout: timeout
46
+ response = result_queue.pop(timeout: timeout)
51
47
 
52
48
  instrumentation_payload[:response] = response
53
49
 
54
50
  raise CmdTimeoutError, "Timeout waiting for response to command ID #{id}" if response.nil?
55
- raise CmdError, "Error response: #{response["error"]} #{cmd.inspect}" if response["error"]
51
+
52
+ raise Bidi2pdf::CmdError.new(cmd, response) if response["error"]
56
53
 
57
54
  block ? block.call(response) : response
58
55
  ensure
@@ -60,14 +57,6 @@ module Bidi2pdf
60
57
  end
61
58
  end
62
59
 
63
- def pop_response(id, timeout:)
64
- raise CmdResponseNotStoredError, "No response stored for command ID #{id} or already popped or this command was not send" unless @pending_responses.key?(id)
65
-
66
- @pending_responses[id].pop(timeout: timeout)
67
- ensure
68
- @pending_responses.delete(id)
69
- end
70
-
71
60
  def handle_response(data)
72
61
  Bidi2pdf.notification_service.instrument("handle_response.bidi2pdf", data: data) do |instrumentation_payload|
73
62
  instrumentation_payload[:error] = data["error"] if data["error"]
@@ -78,9 +67,6 @@ module Bidi2pdf
78
67
 
79
68
  if @pending_responses.key?(id)
80
69
  @pending_responses[id]&.push(data)
81
- return true
82
- elsif @initiated_cmds.key?(id)
83
- @initiated_cmds.delete(id)
84
70
 
85
71
  return true
86
72
  end
@@ -89,12 +75,14 @@ module Bidi2pdf
89
75
  instrumentation_payload[:handled] = false
90
76
 
91
77
  false
78
+ ensure
79
+ @pending_responses.delete id
92
80
  end
93
81
  end
94
82
 
95
83
  private
96
84
 
97
- def init_queue_for(id) = @pending_responses[id] = Thread::Queue.new
85
+ def init_queue_for(id, result_queue) = @pending_responses[id] = result_queue
98
86
 
99
87
  def next_id = self.class.next_id
100
88
  end
@@ -6,7 +6,7 @@ module Bidi2pdf
6
6
  def initialize(logger:)
7
7
  @logger = logger
8
8
  @connected = false
9
- @connection_queue = Thread::Queue.new
9
+ @connection_latch = Concurrent::CountDownLatch.new(1)
10
10
  end
11
11
 
12
12
  def mark_connected
@@ -14,7 +14,7 @@ module Bidi2pdf
14
14
 
15
15
  @connected = true
16
16
  @logger.debug "WebSocket connection is open"
17
- @connection_queue.push(true)
17
+ @connection_latch.count_down
18
18
  end
19
19
 
20
20
  def wait_until_open(timeout:)
@@ -22,13 +22,7 @@ module Bidi2pdf
22
22
 
23
23
  @logger.debug "Waiting for WebSocket connection to open"
24
24
 
25
- begin
26
- Timeout.timeout(timeout) do
27
- @connection_queue.pop
28
- end
29
- rescue Timeout::Error
30
- raise Bidi2pdf::WebsocketError, "WebSocket connection did not open in time #{timeout} sec."
31
- end
25
+ raise Bidi2pdf::WebsocketError, "WebSocket connection did not open in time #{timeout} sec." unless @connection_latch.wait(timeout)
32
26
 
33
27
  true
34
28
  end
@@ -3,20 +3,50 @@
3
3
  module Bidi2pdf
4
4
  module Bidi
5
5
  class EventManager
6
+ Listener = Struct.new(:block, :id, :source_location) do
7
+ def initialize(block, id = SecureRandom.uuid)
8
+ super
9
+ self.source_location = block.source_location
10
+ end
11
+
12
+ def call(*args)
13
+ block.call(*args)
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(Listener) && id == other.id
18
+ end
19
+
20
+ alias_method :eql?, :==
21
+
22
+ def hash
23
+ id.hash
24
+ end
25
+ end
26
+
6
27
  attr_reader :type
7
28
 
8
29
  def initialize(type)
9
- @listeners = Hash.new { |h, k| h[k] = [] }
30
+ @listeners = Concurrent::Hash.new { |h, k| h[k] = [] }
10
31
  @type = type
11
32
  end
12
33
 
13
34
  def on(*event_names, &block)
14
- event_names.each { |event_name| @listeners[event_name.to_sym] << block }
15
-
16
- block
35
+ Listener.new(block).tap do |listener|
36
+ event_names.each do |event_name|
37
+ @listeners[event_name.to_sym] << listener
38
+ log_msg("Adding #{event_name} listener", listener)
39
+ end
40
+ end
17
41
  end
18
42
 
19
- def off(event_name, block) = @listeners[event_name.to_sym].delete(block)
43
+ def off(event_name, listener)
44
+ raise ArgumentError, "Listener not registered" unless listener.is_a?(Listener)
45
+
46
+ log_msg("Removing #{event_name} listener", listener)
47
+
48
+ @listeners[event_name.to_sym].delete(listener)
49
+ end
20
50
 
21
51
  def dispatch(event_name, *args)
22
52
  listeners = @listeners[event_name.to_sym] || []
@@ -27,14 +27,24 @@ module Bidi2pdf
27
27
  client.send_cmd_and_wait(cmd) do |response|
28
28
  @interceptor_id = response["result"]["intercept"]
29
29
 
30
- Bidi2pdf.logger.debug "Interceptor added: #{@interceptor_id}"
30
+ Bidi2pdf.logger.debug2 "Interceptor added: #{@interceptor_id}"
31
31
 
32
- client.on_event(*self.class.events, &method(:handle_event))
32
+ @handle_event_listener = client.on_event(*self.class.events, &method(:handle_event))
33
33
 
34
34
  self
35
35
  end
36
36
  end
37
37
 
38
+ def unregister_with_client(client:)
39
+ return unless @handle_event_listener
40
+
41
+ client.remove_event_listener(*self.class.events, @handle_event_listener)
42
+
43
+ Bidi2pdf.logger.debug2 "Interceptor removed: #{@interceptor_id}"
44
+
45
+ @handle_event_listener = nil
46
+ end
47
+
38
48
  # rubocop: disable Metrics/AbcSize
39
49
  def handle_event(response)
40
50
  event_response = response["params"]
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "browser_console_logger"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ class NavigationFailedEvents
8
+ attr_reader :context_id, :browser_console_logger
9
+
10
+ def initialize(context_id)
11
+ @context_id = context_id
12
+ end
13
+
14
+ def handle_event(data)
15
+ event = data["params"]
16
+ method = data["method"]
17
+
18
+ if event["context"] == context_id
19
+ handle_response(method, event)
20
+ else
21
+ Bidi2pdf.logger.debug2 "Ignoring Log event: #{method}, context_id: #{context_id}, params: #{event}"
22
+ end
23
+ end
24
+
25
+ def handle_response(_method, event)
26
+ url = event["url"]
27
+ navigation = event["navigation"]
28
+ timestamp = event["timestamp"]
29
+
30
+ Bidi2pdf.notification_service.instrument("navigation_failed_received.bidi2pdf",
31
+ {
32
+ url: url,
33
+ timestamp: timestamp,
34
+ navigation: navigation
35
+ })
36
+
37
+ Bidi2pdf.logger.error "Navigation failed for URL: #{url}, Navigation: #{navigation}"
38
+ end
39
+ end
40
+ end
41
+ end