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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -8
- data/README.md +14 -0
- data/docker/Dockerfile.chromedriver +30 -5
- data/docker/entrypoint.sh +41 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +59 -8
- data/lib/bidi2pdf/bidi/client.rb +7 -5
- data/lib/bidi2pdf/bidi/command_manager.rb +14 -26
- data/lib/bidi2pdf/bidi/connection_manager.rb +3 -9
- data/lib/bidi2pdf/bidi/event_manager.rb +35 -5
- data/lib/bidi2pdf/bidi/interceptor.rb +12 -2
- data/lib/bidi2pdf/bidi/navigation_failed_events.rb +41 -0
- data/lib/bidi2pdf/bidi/session.rb +6 -1
- data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +5 -5
- data/lib/bidi2pdf/chromedriver_manager.rb +25 -11
- data/lib/bidi2pdf/notifications.rb +1 -1
- data/lib/bidi2pdf/test_helpers/matchers/contains_pdf_text.rb +50 -0
- data/lib/bidi2pdf/test_helpers/matchers/have_pdf_page_count.rb +50 -0
- data/lib/bidi2pdf/test_helpers/matchers/match_pdf_text.rb +45 -0
- data/lib/bidi2pdf/test_helpers/pdf_reader_utils.rb +89 -0
- data/lib/bidi2pdf/test_helpers/pdf_text_sanitizer.rb +232 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_container.rb +81 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +103 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/shared_docker_network.rb +21 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/testcontainers_refinement.rb +53 -0
- data/lib/bidi2pdf/test_helpers/testcontainers.rb +17 -0
- data/lib/bidi2pdf/test_helpers.rb +13 -0
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +32 -3
- data/sig/bidi2pdf/bidi/event_manager.rbs +19 -13
- metadata +35 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d7fa3c853f53e21a110cadb25fcd66bf97b890eb00663f90e73e8ee9d1ce07e
|
4
|
+
data.tar.gz: 5900568c47f526e9b00d95a15f8293abeaee88f6c0f323bc6e79cdcb3aba38ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
[
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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|
|
data/lib/bidi2pdf/bidi/client.rb
CHANGED
@@ -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, &
|
129
|
-
|
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(
|
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,
|
144
|
-
names.each { |event_name| dispatcher.remove_event_listener(event_name,
|
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 = @
|
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,
|
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
|
-
|
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,
|
42
|
+
id = send_cmd(cmd, result_queue: result_queue)
|
47
43
|
|
48
44
|
instrumentation_payload[:id] = id
|
49
45
|
|
50
|
-
response =
|
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
|
-
|
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] =
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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,
|
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.
|
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
|