bidi2pdf 0.1.14 → 0.1.16
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 +20 -0
- data/CHANGELOG.md +69 -2
- data/README.md +258 -10
- data/docker/Dockerfile +4 -0
- data/docker/Dockerfile.slim +5 -1
- data/lib/bidi2pdf/bidi/browser_tab.rb +45 -4
- data/lib/bidi2pdf/bidi/buffered_web_socket_client.rb +178 -0
- data/lib/bidi2pdf/bidi/client.rb +12 -5
- data/lib/bidi2pdf/bidi/commands/base.rb +2 -0
- data/lib/bidi2pdf/bidi/network_event.rb +12 -3
- data/lib/bidi2pdf/bidi/network_events.rb +9 -1
- data/lib/bidi2pdf/bidi/session.rb +1 -1
- data/lib/bidi2pdf/chromedriver_manager.rb +10 -6
- data/lib/bidi2pdf/cli/json_output.rb +30 -0
- data/lib/bidi2pdf/cli.rb +559 -17
- data/lib/bidi2pdf/diagnose.rb +119 -0
- data/lib/bidi2pdf/error_codes.rb +59 -0
- data/lib/bidi2pdf/exit_codes.rb +44 -0
- data/lib/bidi2pdf/launcher.rb +23 -0
- data/lib/bidi2pdf/manifest.rb +65 -0
- data/lib/bidi2pdf/notifications/json_subscriber.rb +78 -0
- data/lib/bidi2pdf/notifications/logging_subscriber.rb +2 -0
- data/lib/bidi2pdf/pdf_inspection.rb +62 -0
- data/lib/bidi2pdf/recipe/loader.rb +44 -0
- data/lib/bidi2pdf/recipe/runner.rb +229 -0
- data/lib/bidi2pdf/recipe/schema_shape.rb +228 -0
- data/lib/bidi2pdf/recipe/validator.rb +138 -0
- data/lib/bidi2pdf/recipe.rb +83 -0
- data/lib/bidi2pdf/result.rb +67 -0
- data/lib/bidi2pdf/result_collector.rb +150 -0
- data/lib/bidi2pdf/schema.rb +382 -0
- data/lib/bidi2pdf/session_runner.rb +42 -0
- data/lib/bidi2pdf/session_warmer.rb +377 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +4 -2
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +114 -9
- data/sig/bidi2pdf/bidi/browser_tab.rbs +17 -0
- data/sig/bidi2pdf/bidi/buffered_web_socket_client.rbs +81 -0
- data/sig/bidi2pdf/bidi/client.rbs +10 -2
- data/sig/bidi2pdf/bidi/network_event.rbs +11 -1
- data/sig/bidi2pdf/bidi/session.rbs +1 -1
- data/sig/bidi2pdf/chromedriver_manager.rbs +4 -0
- data/sig/bidi2pdf/cli/json_output.rbs +19 -0
- data/sig/bidi2pdf/cli.rbs +112 -2
- data/sig/bidi2pdf/diagnose.rbs +30 -0
- data/sig/bidi2pdf/error_codes.rbs +21 -0
- data/sig/bidi2pdf/exit_codes.rbs +21 -0
- data/sig/bidi2pdf/launcher.rbs +9 -0
- data/sig/bidi2pdf/manifest.rbs +38 -0
- data/sig/bidi2pdf/notifications/json_subscriber.rbs +45 -0
- data/sig/bidi2pdf/pdf_inspection.rbs +33 -0
- data/sig/bidi2pdf/recipe/loader.rbs +20 -0
- data/sig/bidi2pdf/recipe/runner.rbs +92 -0
- data/sig/bidi2pdf/recipe/schema_shape.rbs +85 -0
- data/sig/bidi2pdf/recipe/validator.rbs +54 -0
- data/sig/bidi2pdf/recipe.rbs +72 -0
- data/sig/bidi2pdf/result.rbs +71 -0
- data/sig/bidi2pdf/result_collector.rbs +106 -0
- data/sig/bidi2pdf/schema.rbs +45 -0
- data/sig/bidi2pdf/session_runner.rbs +17 -0
- data/sig/bidi2pdf/session_warmer.rbs +221 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rbs +1 -1
- data/sig/bidi2pdf/version.rbs +1 -1
- data/sig/bidi2pdf.rbs +84 -0
- data/tasks/release_credentials_check.rake +97 -0
- metadata +39 -4
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "websocket"
|
|
8
|
+
|
|
9
|
+
module Bidi2pdf
|
|
10
|
+
module Bidi
|
|
11
|
+
# A threaded WebSocket client on the `websocket` gem's framing - the same shape Selenium's Ruby
|
|
12
|
+
# BiDi client uses. It replaces websocket-client-simple, whose reader pulled one byte at a time
|
|
13
|
+
# (`getc`, then a frame-parse attempt per byte): a printed PDF comes back as one base64 message
|
|
14
|
+
# and every network event for a `data:` navigation echoes the whole URL, so that loop ran
|
|
15
|
+
# millions of times per render - ~730 ms to decode a 700 KB frame, against ~0.5 ms in 16 KB
|
|
16
|
+
# chunks.
|
|
17
|
+
#
|
|
18
|
+
# Emits :open, :message (a WebSocket frame, payload in #data), :error and :close.
|
|
19
|
+
class BufferedWebSocketClient
|
|
20
|
+
READ_CHUNK_BYTES = 16_384
|
|
21
|
+
|
|
22
|
+
attr_reader :url
|
|
23
|
+
|
|
24
|
+
def self.connect(url, options = {})
|
|
25
|
+
client = new
|
|
26
|
+
yield client if block_given?
|
|
27
|
+
client.connect url, options
|
|
28
|
+
client
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@listeners = Hash.new { |hash, event| hash[event] = [] }
|
|
33
|
+
@listeners_mutex = Mutex.new
|
|
34
|
+
@write_mutex = Mutex.new
|
|
35
|
+
# Re-entrant: a failed write inside #close closes again.
|
|
36
|
+
@close_monitor = Monitor.new
|
|
37
|
+
@handshaked = false
|
|
38
|
+
@closed = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def on(event, &listener)
|
|
42
|
+
@listeners_mutex.synchronize { @listeners[event] << listener }
|
|
43
|
+
listener
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def connect(url, options = {})
|
|
47
|
+
return if @socket
|
|
48
|
+
|
|
49
|
+
@url = url
|
|
50
|
+
@socket = open_socket(URI.parse(url), options)
|
|
51
|
+
::WebSocket.should_raise = true
|
|
52
|
+
@handshake = ::WebSocket::Handshake::Client.new url: url, headers: options[:headers]
|
|
53
|
+
|
|
54
|
+
socket = @socket # #close clears the ivar; the reader keeps its own reference
|
|
55
|
+
@thread = Thread.new { read_loop(socket, ::WebSocket::Frame::Incoming::Client.new) }
|
|
56
|
+
write @handshake.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Commands are sent from whichever thread issues them. On a plain TCP socket one IO#write is
|
|
60
|
+
# already atomic, but OpenSSL::SSL::SSLSocket#write is not, so writes share a lock for wss://.
|
|
61
|
+
def send(data, type: :text)
|
|
62
|
+
return unless open?
|
|
63
|
+
|
|
64
|
+
write ::WebSocket::Frame::Outgoing::Client.new(data: data, type: type, version: @handshake.version).to_s
|
|
65
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError => e
|
|
66
|
+
close e
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Safe from any thread, including the reader's own: the reader sees the peer hang up and
|
|
70
|
+
# closes from inside its loop, so it must neither race a caller closing at the same moment
|
|
71
|
+
# nor be killed before :close has been emitted.
|
|
72
|
+
def close(error = nil)
|
|
73
|
+
@close_monitor.synchronize do
|
|
74
|
+
return if @closed
|
|
75
|
+
|
|
76
|
+
say_goodbye unless error
|
|
77
|
+
@closed = true
|
|
78
|
+
@socket&.close
|
|
79
|
+
@socket = nil
|
|
80
|
+
emit :close, error
|
|
81
|
+
@thread.kill if @thread && @thread != Thread.current
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def open? = @handshaked && !@closed
|
|
86
|
+
|
|
87
|
+
def closed? = @closed
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def emit(event, *)
|
|
92
|
+
@listeners_mutex.synchronize { @listeners[event].dup }.each { |listener| listener.call(*) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def write(bytes)
|
|
96
|
+
@write_mutex.synchronize { @socket&.write bytes }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def say_goodbye
|
|
100
|
+
write ::WebSocket::Frame::Outgoing::Client.new(data: nil, type: :close, version: @handshake.version).to_s if @handshaked
|
|
101
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
102
|
+
nil # the peer is already gone; there is nobody left to say goodbye to
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def open_socket(uri, options)
|
|
106
|
+
tcp_socket = TCPSocket.new(uri.host, uri.port || (uri.scheme == "wss" ? 443 : 80))
|
|
107
|
+
return tcp_socket unless %w[https wss].include?(uri.scheme)
|
|
108
|
+
|
|
109
|
+
::OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context(options)).tap do |ssl_socket|
|
|
110
|
+
ssl_socket.sync_close = true
|
|
111
|
+
ssl_socket.hostname = uri.host
|
|
112
|
+
ssl_socket.connect
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ssl_context(options)
|
|
117
|
+
::OpenSSL::SSL::SSLContext.new.tap do |context|
|
|
118
|
+
context.ssl_version = options[:ssl_version] if options[:ssl_version]
|
|
119
|
+
context.verify_mode = options[:verify_mode] if options[:verify_mode]
|
|
120
|
+
context.cert_store = (options[:cert_store] || ::OpenSSL::X509::Store.new).tap(&:set_default_paths)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def read_loop(socket, frame)
|
|
125
|
+
until @closed
|
|
126
|
+
begin
|
|
127
|
+
consume(socket.readpartial(READ_CHUNK_BYTES), frame)
|
|
128
|
+
# Same terminal-error list as #send/#say_goodbye - Errno::ECONNRESET is already a
|
|
129
|
+
# SystemCallError, so listing it separately was both redundant and, worse, incomplete: any
|
|
130
|
+
# *other* SystemCallError or OpenSSL::SSL::SSLError fell through to the generic rescue
|
|
131
|
+
# below, which only emits :error and loops back into readpartial - on a permanently broken
|
|
132
|
+
# socket (not just ECONNRESET) that re-raises the same error every iteration forever
|
|
133
|
+
# instead of ever closing.
|
|
134
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError => e # EOFError is an IOError
|
|
135
|
+
close e
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
emit :error, e
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def consume(chunk, frame)
|
|
143
|
+
if @handshaked
|
|
144
|
+
frame << chunk
|
|
145
|
+
else
|
|
146
|
+
@handshake << chunk
|
|
147
|
+
return unless @handshake.finished?
|
|
148
|
+
|
|
149
|
+
@handshaked = true
|
|
150
|
+
emit :open
|
|
151
|
+
# Anything the server sent straight after its handshake response is already in this chunk.
|
|
152
|
+
frame << @handshake.leftovers unless @handshake.leftovers.to_s.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
while (message = frame.next)
|
|
156
|
+
dispatch_frame(message)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# WebSocket control frames (ping/pong/close) must never reach a JSON-parsing consumer -
|
|
161
|
+
# WebSocketDispatcher tries to parse every :message payload as JSON, so a raw close frame
|
|
162
|
+
# would raise JSON::ParserError, and an unanswered ping can make chromedriver or an
|
|
163
|
+
# intermediate proxy tear down an otherwise healthy connection on its own keepalive timeout.
|
|
164
|
+
def dispatch_frame(message)
|
|
165
|
+
case message.type
|
|
166
|
+
when :ping
|
|
167
|
+
send(message.data, type: :pong)
|
|
168
|
+
when :pong
|
|
169
|
+
nil
|
|
170
|
+
when :close
|
|
171
|
+
close
|
|
172
|
+
else
|
|
173
|
+
emit :message, message
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/bidi2pdf/bidi/client.rb
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
require_relative "buffered_web_socket_client"
|
|
6
5
|
require_relative "web_socket_dispatcher"
|
|
7
6
|
require_relative "command_manager"
|
|
8
7
|
require_relative "connection_manager"
|
|
@@ -44,11 +43,11 @@ module Bidi2pdf
|
|
|
44
43
|
|
|
45
44
|
# Starts the WebSocket client and establishes a connection.
|
|
46
45
|
#
|
|
47
|
-
# @return [
|
|
46
|
+
# @return [BufferedWebSocketClient] The WebSocket connection object.
|
|
48
47
|
def start
|
|
49
48
|
return @socket if started?
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
BufferedWebSocketClient.connect(ws_url) do |socket|
|
|
52
51
|
@socket = socket
|
|
53
52
|
@command_manager = CommandManager.new(@socket)
|
|
54
53
|
|
|
@@ -62,11 +61,19 @@ module Bidi2pdf
|
|
|
62
61
|
@socket
|
|
63
62
|
end
|
|
64
63
|
|
|
65
|
-
# Checks if the WebSocket client has started.
|
|
64
|
+
# Checks if the WebSocket client has started. Unlike #open?, this never flips back to false on
|
|
65
|
+
# its own if the connection dies externally - it only reflects whether #start has run.
|
|
66
66
|
#
|
|
67
67
|
# @return [Boolean] True if the client has started, false otherwise.
|
|
68
68
|
def started? = @started
|
|
69
69
|
|
|
70
|
+
# Checks if the underlying WebSocket connection is actually open right now - false before
|
|
71
|
+
# #start, after #close, or if the reader thread noticed the connection died on its own (a
|
|
72
|
+
# real liveness signal, not just an internal "did we ever start" flag).
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] True if the underlying socket reports itself open.
|
|
75
|
+
def open? = !!@socket&.open?
|
|
76
|
+
|
|
70
77
|
# Waits until the WebSocket connection is open.
|
|
71
78
|
#
|
|
72
79
|
# @param [Integer] timeout The timeout duration in seconds.
|
|
@@ -4,7 +4,7 @@ module Bidi2pdf
|
|
|
4
4
|
module Bidi
|
|
5
5
|
class NetworkEvent
|
|
6
6
|
attr_reader :id, :url, :state, :start_timestamp, :end_timestamp, :timing, :http_status_code,
|
|
7
|
-
:http_method, :bytes_received
|
|
7
|
+
:http_method, :bytes_received, :navigation
|
|
8
8
|
|
|
9
9
|
STATE_MAP = {
|
|
10
10
|
"network.responseStarted" => "started",
|
|
@@ -12,7 +12,13 @@ module Bidi2pdf
|
|
|
12
12
|
"network.fetchError" => "error"
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# @param [String, nil] navigation The BiDi navigation ID this request belongs to - present on
|
|
16
|
+
# the main-frame document request, letting a caller correlate a specific
|
|
17
|
+
# browsingContext.navigate call to the network event carrying its actual HTTP status. A
|
|
18
|
+
# sub-resource request (an image, a script, ...) has no real BiDi navigation ID and stays
|
|
19
|
+
# nil here - #id already distinguishes one event from another in logs, so this field is
|
|
20
|
+
# left alone rather than filled with a value the protocol never sent.
|
|
21
|
+
def initialize(id:, url:, timestamp:, timing:, state:, http_status_code: nil, http_method: nil, navigation: nil)
|
|
16
22
|
@id = id
|
|
17
23
|
@url = url
|
|
18
24
|
@start_timestamp = timestamp
|
|
@@ -20,6 +26,7 @@ module Bidi2pdf
|
|
|
20
26
|
@state = map_state(state)
|
|
21
27
|
@http_status_code = http_status_code
|
|
22
28
|
@http_method = http_method
|
|
29
|
+
@navigation = navigation
|
|
23
30
|
end
|
|
24
31
|
|
|
25
32
|
def update_state(new_state, timestamp: nil, timing: nil, http_status_code: nil, bytes_received: nil)
|
|
@@ -61,6 +68,7 @@ module Bidi2pdf
|
|
|
61
68
|
"method=#{method_str.inspect}, " \
|
|
62
69
|
"url=#{@url.inspect}, " \
|
|
63
70
|
"state=#{@state.inspect}, " \
|
|
71
|
+
"navigation=#{@navigation.inspect}, " \
|
|
64
72
|
"#{http_status}, " \
|
|
65
73
|
"bytes_received=#{bytes_str}, " \
|
|
66
74
|
"start=#{start_str}, " \
|
|
@@ -76,7 +84,8 @@ module Bidi2pdf
|
|
|
76
84
|
timing: @timing&.dup,
|
|
77
85
|
state: @state,
|
|
78
86
|
http_status_code: @http_status_code,
|
|
79
|
-
http_method: @http_method
|
|
87
|
+
http_method: @http_method,
|
|
88
|
+
navigation: @navigation
|
|
80
89
|
).tap do |duped|
|
|
81
90
|
duped.instance_variable_set(:@end_timestamp, @end_timestamp)
|
|
82
91
|
duped.instance_variable_set(:@bytes_received, @bytes_received)
|
|
@@ -41,6 +41,13 @@ module Bidi2pdf
|
|
|
41
41
|
timing = request["timings"]
|
|
42
42
|
http_method = request["method"]
|
|
43
43
|
|
|
44
|
+
# A top-level sibling of "request"/"response" in BiDi's own network.BaseParameters, not
|
|
45
|
+
# nested under either - present for the main-frame document request of a navigation, nil
|
|
46
|
+
# for an unrelated sub-resource fetch (an image, a script, ...). Lets a caller correlate a
|
|
47
|
+
# specific browsingContext.navigate call to the exact network event carrying its real HTTP
|
|
48
|
+
# status, which the navigate command's own response never includes.
|
|
49
|
+
navigation = event["navigation"]
|
|
50
|
+
|
|
44
51
|
timestamp = event["timestamp"]
|
|
45
52
|
|
|
46
53
|
Bidi2pdf.notification_service.instrument("network_event_received.bidi2pdf",
|
|
@@ -57,7 +64,8 @@ module Bidi2pdf
|
|
|
57
64
|
timestamp: timestamp,
|
|
58
65
|
timing: timing,
|
|
59
66
|
state: method,
|
|
60
|
-
http_method: http_method
|
|
67
|
+
http_method: http_method,
|
|
68
|
+
navigation: navigation
|
|
61
69
|
)
|
|
62
70
|
elsif events.key?(id)
|
|
63
71
|
events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
|
|
@@ -62,7 +62,7 @@ module Bidi2pdf
|
|
|
62
62
|
"--disable-breakpad", # Disable the Breakpad crash-reporting library entirely
|
|
63
63
|
"--enable-features=PdfOopif", # Enable out-of-process iframe (OOPIF) architecture for PDF rendering
|
|
64
64
|
"--disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold,IsolateSandboxedIframes",
|
|
65
|
-
"--disable-extensions about:blank"
|
|
65
|
+
"--disable-extensions" # Don't load extensions. One switch per entry: it used to read "--disable-extensions about:blank", which Chrome rejected whole ("Ignoring switch with invalid name").
|
|
66
66
|
].freeze
|
|
67
67
|
|
|
68
68
|
# @return [URI] The URI of the session.
|
|
@@ -194,13 +194,17 @@ module Bidi2pdf
|
|
|
194
194
|
cmd.join(" ")
|
|
195
195
|
end
|
|
196
196
|
|
|
197
|
+
# Chromedriver's own internal log verbosity is independent from this app's - defaults to
|
|
198
|
+
# mirroring Bidi2pdf.logger.level for backward compatibility, but Bidi2pdf.chromedriver_log_level
|
|
199
|
+
# lets a consumer quiet chromedriver's own (often much more verbose, e.g. full BiDi command/
|
|
200
|
+
# response dumps at its own INFO level) output without changing their app's own log level.
|
|
197
201
|
def chromedriver_log_level
|
|
198
|
-
case Bidi2pdf.logger.level
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
Bidi2pdf.chromedriver_log_level || case Bidi2pdf.logger.level
|
|
203
|
+
when Logger::DEBUG then "ALL"
|
|
204
|
+
when Logger::INFO then "INFO"
|
|
205
|
+
when Logger::WARN then "WARNING"
|
|
206
|
+
else "SEVERE"
|
|
207
|
+
end
|
|
204
208
|
end
|
|
205
209
|
|
|
206
210
|
def user_data_dir_arg
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bidi2pdf
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
# Shared machinery for --json/--json-stream/--output - across render/diagnose/run: reserving
|
|
6
|
+
# stdout for exactly one machine-readable payload, and mapping a Result to a process exit
|
|
7
|
+
# status.
|
|
8
|
+
module JsonOutput
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Bidi2pdf.logger (and friends) default to $stdout (see lib/bidi2pdf.rb), so a
|
|
12
|
+
# --json/--output - render has to redirect them for its duration or every log line would
|
|
13
|
+
# land in the same stream as the JSON document / PDF bytes it is trying to keep pure.
|
|
14
|
+
# Logger#reopen swaps the destination in place, so nothing else holding a reference to
|
|
15
|
+
# these loggers needs to know.
|
|
16
|
+
def reserve_stdout_for_machine_output
|
|
17
|
+
loggers = [Bidi2pdf.logger, Bidi2pdf.network_events_logger, Bidi2pdf.browser_console_logger].compact
|
|
18
|
+
loggers.each { |logger| logger.logger.reopen($stderr) }
|
|
19
|
+
|
|
20
|
+
yield
|
|
21
|
+
ensure
|
|
22
|
+
loggers.each { |logger| logger.logger.reopen($stdout) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def exit_for_result(result)
|
|
26
|
+
exit(result.ok? ? Bidi2pdf::ExitCodes::SUCCESS : Bidi2pdf::ExitCodes.for(result.error[:code]))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|