bidi2pdf 0.1.0
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 +7 -0
- data/.idea/.gitignore +8 -0
- data/.rspec +3 -0
- data/.rubocop.yml +50 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +119 -0
- data/Rakefile +22 -0
- data/docker/Dockerfile +35 -0
- data/docker/docker-compose.yml +1 -0
- data/exe/bidi2pdf +7 -0
- data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +42 -0
- data/lib/bidi2pdf/bidi/auth_interceptor.rb +67 -0
- data/lib/bidi2pdf/bidi/browser.rb +15 -0
- data/lib/bidi2pdf/bidi/browser_tab.rb +180 -0
- data/lib/bidi2pdf/bidi/client.rb +224 -0
- data/lib/bidi2pdf/bidi/event_manager.rb +84 -0
- data/lib/bidi2pdf/bidi/network_event.rb +54 -0
- data/lib/bidi2pdf/bidi/network_events.rb +82 -0
- data/lib/bidi2pdf/bidi/print_parameters_validator.rb +114 -0
- data/lib/bidi2pdf/bidi/session.rb +135 -0
- data/lib/bidi2pdf/bidi/user_context.rb +75 -0
- data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +70 -0
- data/lib/bidi2pdf/chromedriver_manager.rb +160 -0
- data/lib/bidi2pdf/cli.rb +118 -0
- data/lib/bidi2pdf/launcher.rb +46 -0
- data/lib/bidi2pdf/session_runner.rb +123 -0
- data/lib/bidi2pdf/utils.rb +15 -0
- data/lib/bidi2pdf/version.rb +5 -0
- data/lib/bidi2pdf.rb +25 -0
- data/sig/bidi2pdf/chrome/chromedriver_downloader.rbs +11 -0
- data/sig/bidi2pdf/chrome/downloader_helper.rbs +9 -0
- data/sig/bidi2pdf/chrome/finder.rbs +27 -0
- data/sig/bidi2pdf/chrome/platform.rbs +13 -0
- data/sig/bidi2pdf/chrome/version_resolver.rbs +19 -0
- data/sig/bidi2pdf.rbs +4 -0
- data/tmp/.keep +0 -0
- metadata +327 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "browser_tab"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
module Bidi
|
7
|
+
class UserContext
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
def initialize(client)
|
11
|
+
@client = client
|
12
|
+
@context_id = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def context_id
|
16
|
+
@context_id ||= begin
|
17
|
+
res = client.send_cmd_and_wait("browser.createUserContext", {}) do |response|
|
18
|
+
response["result"]["userContext"]
|
19
|
+
end
|
20
|
+
|
21
|
+
Bidi2pdf.logger.debug "User context created: #{res.inspect}"
|
22
|
+
|
23
|
+
res
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_cookie(
|
28
|
+
name:,
|
29
|
+
value:,
|
30
|
+
domain:,
|
31
|
+
source_origin:,
|
32
|
+
path: "/",
|
33
|
+
secure: true,
|
34
|
+
http_only: false,
|
35
|
+
same_site: "strict",
|
36
|
+
ttl: 30
|
37
|
+
)
|
38
|
+
expiry = Time.now.to_i + ttl
|
39
|
+
client.send_cmd_and_wait("storage.setCookie", {
|
40
|
+
cookie: {
|
41
|
+
name: name,
|
42
|
+
value: {
|
43
|
+
type: "string",
|
44
|
+
value: value
|
45
|
+
},
|
46
|
+
domain: domain,
|
47
|
+
path: path,
|
48
|
+
secure: secure,
|
49
|
+
httpOnly: http_only,
|
50
|
+
sameSite: same_site,
|
51
|
+
expiry: expiry
|
52
|
+
},
|
53
|
+
partition: {
|
54
|
+
type: "storageKey",
|
55
|
+
userContext: context_id,
|
56
|
+
sourceOrigin: source_origin
|
57
|
+
}
|
58
|
+
}) do |response|
|
59
|
+
Bidi2pdf.logger.debug "Cookie set: #{response.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_browser_window
|
64
|
+
client.send_cmd_and_wait("browsingContext.create", {
|
65
|
+
type: "window",
|
66
|
+
userContext: context_id
|
67
|
+
}) do |response|
|
68
|
+
browsing_context_id = response["result"]["context"]
|
69
|
+
|
70
|
+
BrowserTab.new(client, browsing_context_id, context_id)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "event_manager"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
module Bidi
|
7
|
+
class WebSocketDispatcher
|
8
|
+
attr_reader :socket_events, :session_events
|
9
|
+
|
10
|
+
def initialize(socket)
|
11
|
+
@socket = socket
|
12
|
+
@socket_events = EventManager.new("socket-event")
|
13
|
+
@session_events = EventManager.new("session-event")
|
14
|
+
end
|
15
|
+
|
16
|
+
def start_listening
|
17
|
+
Bidi2pdf.logger.debug "Registering WebSocket event listeners"
|
18
|
+
|
19
|
+
setup_connection_lifecycle_handlers
|
20
|
+
setup_message_handler
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add listeners
|
24
|
+
|
25
|
+
def on_message(&block) = socket_events.on(:message, &block)
|
26
|
+
|
27
|
+
def on_event(name, &block) = session_events.on(name, &block)
|
28
|
+
|
29
|
+
def on_open(&block) = socket_events.on(:open, &block)
|
30
|
+
|
31
|
+
def on_close(&block) = socket_events.on(:close, &block)
|
32
|
+
|
33
|
+
def on_error(&block) = socket_events.on(:error, &block)
|
34
|
+
|
35
|
+
def remove_message_listener(block) = socket_events.off(:message, block)
|
36
|
+
|
37
|
+
def remove_event_listener(name, block) = session_events.off(name, block)
|
38
|
+
|
39
|
+
def remove_open_listener(block) = socket_events.off(:open, block)
|
40
|
+
|
41
|
+
def remove_close_listener(block) = socket_events.off(:close, block)
|
42
|
+
|
43
|
+
def remove_error_listener(block) = socket_events.off(:error, block)
|
44
|
+
|
45
|
+
def setup_message_handler
|
46
|
+
that = self
|
47
|
+
|
48
|
+
@socket.on(:message) do |msg|
|
49
|
+
data = JSON.parse(msg.data)
|
50
|
+
method = data["method"]
|
51
|
+
|
52
|
+
if method
|
53
|
+
Bidi2pdf.logger.debug "Dispatching session event: #{method}"
|
54
|
+
that.session_events.dispatch(method, data)
|
55
|
+
else
|
56
|
+
Bidi2pdf.logger.debug "Dispatching socket message"
|
57
|
+
that.socket_events.dispatch(:message, data)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_connection_lifecycle_handlers
|
63
|
+
that = self
|
64
|
+
@socket.on(:open) { |e| that.socket_events.dispatch(:open, e) }
|
65
|
+
@socket.on(:close) { |e| that.socket_events.dispatch(:close, e) }
|
66
|
+
@socket.on(:error) { |e| that.socket_events.dispatch(:error, e) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "chromedriver/binary"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
class ChromedriverManager
|
7
|
+
attr_reader :port, :pid, :session
|
8
|
+
|
9
|
+
def initialize(port: 0, headless: true)
|
10
|
+
@port = port
|
11
|
+
@headless = headless
|
12
|
+
@session = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
return @pid if @pid
|
17
|
+
|
18
|
+
update_chromedriver
|
19
|
+
cmd = build_cmd
|
20
|
+
Bidi2pdf.logger.info "Starting Chromedriver with command: #{cmd}"
|
21
|
+
|
22
|
+
r, w = IO.pipe
|
23
|
+
@pid = Process.spawn(cmd, out: w, err: w)
|
24
|
+
w.close # close writer in parent
|
25
|
+
|
26
|
+
parse_port_from_output(r)
|
27
|
+
|
28
|
+
Bidi2pdf.logger.info "Started Chromedriver on port #{@port}, PID #{@pid}"
|
29
|
+
wait_until_chromedriver_ready
|
30
|
+
|
31
|
+
at_exit { stop }
|
32
|
+
|
33
|
+
@session = Bidi::Session.new(@port, headless: @headless)
|
34
|
+
|
35
|
+
@pid
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop(timeout: 5)
|
39
|
+
return unless @pid
|
40
|
+
|
41
|
+
close_session
|
42
|
+
|
43
|
+
term_chromedriver
|
44
|
+
|
45
|
+
return unless process_alive?
|
46
|
+
|
47
|
+
kill_chromedriver timeout: timeout
|
48
|
+
ensure
|
49
|
+
@pid = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def close_session
|
55
|
+
Bidi2pdf.logger.info "Closing session"
|
56
|
+
|
57
|
+
@session.close
|
58
|
+
@session = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def term_chromedriver
|
62
|
+
Bidi2pdf.logger.info "Stopping Chromedriver (PID #{@pid})"
|
63
|
+
|
64
|
+
Process.kill("TERM", @pid)
|
65
|
+
rescue Errno::ESRCH
|
66
|
+
Bidi2pdf.logger.debug "Process already gone"
|
67
|
+
@pid = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def kill_chromedriver(timeout: 5)
|
71
|
+
start_time = Time.now
|
72
|
+
|
73
|
+
while Time.now - start_time < timeout
|
74
|
+
return @pid = nil unless process_alive?
|
75
|
+
|
76
|
+
sleep 0.1
|
77
|
+
end
|
78
|
+
|
79
|
+
Bidi2pdf.logger.warn "ChromeDriver did not terminate gracefully — force killing PID #{@pid}"
|
80
|
+
|
81
|
+
begin
|
82
|
+
Process.kill("KILL", @pid)
|
83
|
+
rescue Errno::ESRCH
|
84
|
+
Bidi2pdf.logger.debug "Process already gone"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_cmd
|
89
|
+
bin = Chromedriver::Binary::ChromedriverDownloader.driver_path
|
90
|
+
user_data_dir = File.join(Dir.tmpdir, "bidi2pdf", "user_data")
|
91
|
+
|
92
|
+
cmd = [bin]
|
93
|
+
cmd << "--port=#{@port}" unless @port.zero?
|
94
|
+
cmd << "--headless" if @headless
|
95
|
+
cmd << "--user-data-dir "
|
96
|
+
cmd << user_data_dir
|
97
|
+
cmd.join(" ")
|
98
|
+
end
|
99
|
+
|
100
|
+
def update_chromedriver
|
101
|
+
Chromedriver::Binary::ChromedriverDownloader.update
|
102
|
+
end
|
103
|
+
|
104
|
+
# rubocop: disable Metrics/AbcSize
|
105
|
+
def parse_port_from_output(io, timeout: 5)
|
106
|
+
Thread.new do
|
107
|
+
io.each_line do |line|
|
108
|
+
Bidi2pdf.logger.debug line.chomp
|
109
|
+
|
110
|
+
next unless line =~ /ChromeDriver was started successfully on port (\d+)/
|
111
|
+
|
112
|
+
Bidi2pdf.logger.debug "Found port: #{::Regexp.last_match(1).to_i} setup port: #{@port}"
|
113
|
+
|
114
|
+
@port = ::Regexp.last_match(1).to_i if @port.nil? || @port.zero?
|
115
|
+
|
116
|
+
break
|
117
|
+
end
|
118
|
+
rescue IOError
|
119
|
+
# reader closed
|
120
|
+
ensure
|
121
|
+
io.close unless io.closed?
|
122
|
+
end.join(timeout)
|
123
|
+
|
124
|
+
raise "Chromedriver did not report a usable port in #{timeout}s" if @port.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
# rubocop: enable Metrics/AbcSize
|
128
|
+
|
129
|
+
def process_alive?
|
130
|
+
return false unless @pid
|
131
|
+
|
132
|
+
begin
|
133
|
+
Process.waitpid(@pid, Process::WNOHANG)
|
134
|
+
true
|
135
|
+
rescue Errno::ESRCH, Errno::EPERM, Errno::ECHILD
|
136
|
+
Bidi2pdf.logger.debug "Process already gone"
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def wait_until_chromedriver_ready(timeout: 5)
|
142
|
+
uri = URI("http://127.0.0.1:#{@port}/status")
|
143
|
+
deadline = Time.now + timeout
|
144
|
+
|
145
|
+
until Time.now > deadline
|
146
|
+
begin
|
147
|
+
response = Net::HTTP.get_response(uri)
|
148
|
+
json = JSON.parse(response.body)
|
149
|
+
return true if json["value"] && json["value"]["ready"]
|
150
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, JSON::ParserError
|
151
|
+
# Just retry
|
152
|
+
end
|
153
|
+
|
154
|
+
sleep 0.1
|
155
|
+
end
|
156
|
+
|
157
|
+
raise "ChromeDriver did not become ready within #{timeout} seconds"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/bidi2pdf/cli.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
module Bidi2pdf
|
6
|
+
class CLI < Thor
|
7
|
+
desc "render", "Render a URL to PDF using Chrome BiDi"
|
8
|
+
long_desc <<~USAGE, wrap: false
|
9
|
+
Example:
|
10
|
+
|
11
|
+
$ bidi2pdf render \\
|
12
|
+
--url http://localhost:3000/report \\
|
13
|
+
--output report.pdf \\
|
14
|
+
--cookie session=abc123 \\
|
15
|
+
--header X-API-KEY=topsecret \\
|
16
|
+
--auth admin:admin \\
|
17
|
+
--headless \\
|
18
|
+
--port 0 \\
|
19
|
+
--wait_window_loaded \\
|
20
|
+
--wait_network_idle \\
|
21
|
+
--log-level debug
|
22
|
+
|
23
|
+
This command will render the given URL to PDF using Chrome via BiDi protocol,
|
24
|
+
optionally passing cookies, headers, and basic authentication.
|
25
|
+
|
26
|
+
Set --port to 0 for a random ChromeDriver port.
|
27
|
+
USAGE
|
28
|
+
|
29
|
+
option :url, required: true, desc: "The URL to render"
|
30
|
+
option :output, default: "output.pdf", desc: "Filename for the output PDF"
|
31
|
+
option :cookie, type: :array, default: [], banner: "name=value", desc: "One or more cookies"
|
32
|
+
option :header, type: :array, default: [], banner: "name=value", desc: "One or more custom headers"
|
33
|
+
option :auth, type: :string, banner: "user:pass", desc: "Basic auth credentials"
|
34
|
+
option :headless, type: :boolean, default: true, desc: "Run Chrome in headless mode"
|
35
|
+
option :port, type: :numeric, default: 0, desc: "Port to run ChromeDriver on (0 = auto)"
|
36
|
+
option :wait_window_loaded,
|
37
|
+
type: :boolean,
|
38
|
+
default: false,
|
39
|
+
desc: "Wait for the window to be fully loaded (windoow.loaded set by your script)"
|
40
|
+
option :wait_network_idle, type: :boolean, default: false, desc: "Wait for network to be idle"
|
41
|
+
option :default_timeout, type: :numeric, default: 60, desc: "Default timeout for commands"
|
42
|
+
option :log_level,
|
43
|
+
type: :string,
|
44
|
+
default: "info", enum: %w[debug info warn error fatal unknown], desc: "Set log level"
|
45
|
+
|
46
|
+
def render
|
47
|
+
configure
|
48
|
+
|
49
|
+
Bidi2pdf.logger.info "Rendering: #{options[:url]} -> #{options[:output]}"
|
50
|
+
|
51
|
+
launcher.launch
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# rubocop:disable Metrics/AbcSize
|
57
|
+
def launcher
|
58
|
+
# rubocop:disable Layout/BeginEndAlignment
|
59
|
+
@launcher ||= begin
|
60
|
+
username, password = parse_auth(options[:auth]) if options[:auth]
|
61
|
+
|
62
|
+
Bidi2pdf::Launcher.new(
|
63
|
+
url: options[:url],
|
64
|
+
output: options[:output],
|
65
|
+
cookies: parse_key_values(options[:cookie]),
|
66
|
+
headers: parse_key_values(options[:header]),
|
67
|
+
auth: { username: username, password: password },
|
68
|
+
port: options[:port],
|
69
|
+
headless: options[:headless],
|
70
|
+
wait_window_loaded: options[:wait_window_loaded],
|
71
|
+
wait_network_idle: options[:wait_network_idle]
|
72
|
+
)
|
73
|
+
end
|
74
|
+
# rubocop:enable Layout/BeginEndAlignment
|
75
|
+
end
|
76
|
+
|
77
|
+
# rubocop:enable Metrics/AbcSize
|
78
|
+
|
79
|
+
def configure
|
80
|
+
Bidi2pdf.configure do |config|
|
81
|
+
config.logger.level = log_level
|
82
|
+
config.default_timeout = options[:default_timeout]
|
83
|
+
|
84
|
+
Chromedriver::Binary.configure do |c|
|
85
|
+
c.logger.level = log_level
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def log_level
|
91
|
+
case options[:log_level]
|
92
|
+
when "debug" then Logger::DEBUG
|
93
|
+
when "warn" then Logger::WARN
|
94
|
+
when "error" then Logger::ERROR
|
95
|
+
when "fatal" then Logger::FATAL
|
96
|
+
when "unknown" then Logger::UNKNOWN
|
97
|
+
else
|
98
|
+
Logger::INFO
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_key_values(pairs)
|
103
|
+
pairs.to_h do |pair|
|
104
|
+
k, v = pair.split("=", 2)
|
105
|
+
raise ArgumentError, "Invalid format for pair: #{pair}" unless k && v
|
106
|
+
|
107
|
+
[k.strip, v.strip]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_auth(auth_string)
|
112
|
+
user, pass = auth_string.split(":", 2)
|
113
|
+
raise ArgumentError, "Auth must be in 'user:pass' format" unless user && pass
|
114
|
+
|
115
|
+
[user, pass]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "chromedriver_manager"
|
4
|
+
require_relative "session_runner"
|
5
|
+
require_relative "bidi/session"
|
6
|
+
|
7
|
+
module Bidi2pdf
|
8
|
+
class Launcher
|
9
|
+
def initialize(url:, output:, cookies:, headers:, auth:, headless: true, port: 0, wait_window_loaded: false,
|
10
|
+
wait_network_idle: false, print_options: {})
|
11
|
+
@url = url
|
12
|
+
@port = port
|
13
|
+
@headless = headless
|
14
|
+
@output = output
|
15
|
+
@cookies = cookies
|
16
|
+
@headers = headers
|
17
|
+
@auth = auth
|
18
|
+
@manager = nil
|
19
|
+
@wait_window_loaded = wait_window_loaded
|
20
|
+
@wait_network_idle = wait_network_idle
|
21
|
+
@print_options = print_options || {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def launch
|
25
|
+
@manager = ChromedriverManager.new(port: @port, headless: @headless)
|
26
|
+
@manager.start
|
27
|
+
|
28
|
+
runner = SessionRunner.new(
|
29
|
+
session: @manager.session,
|
30
|
+
url: @url,
|
31
|
+
output: @output,
|
32
|
+
cookies: @cookies,
|
33
|
+
headers: @headers,
|
34
|
+
auth: @auth,
|
35
|
+
wait_window_loaded: @wait_window_loaded,
|
36
|
+
wait_network_idle: @wait_network_idle,
|
37
|
+
print_options: @print_options
|
38
|
+
)
|
39
|
+
runner.run
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop
|
43
|
+
@manager.stop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
class SessionRunner
|
5
|
+
def initialize(session:, url:, output:, cookies: {}, headers: {}, auth: {}, wait_window_loaded: false,
|
6
|
+
wait_network_idle: false, print_options: {})
|
7
|
+
@session = session
|
8
|
+
@url = url
|
9
|
+
@output = output
|
10
|
+
@cookies = cookies || {}
|
11
|
+
@headers = headers || {}
|
12
|
+
@auth = auth || {}
|
13
|
+
@wait_window_loaded = wait_window_loaded
|
14
|
+
@wait_network_idle = wait_network_idle
|
15
|
+
@print_options = print_options || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
@session.start
|
20
|
+
@session.client.on_close { Bidi2pdf.logger.info "WebSocket closed" }
|
21
|
+
|
22
|
+
setup_browser
|
23
|
+
run_flow
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def setup_browser
|
29
|
+
browser = @session.browser
|
30
|
+
user_context = browser.create_user_context
|
31
|
+
|
32
|
+
add_cookies(user_context)
|
33
|
+
|
34
|
+
window = user_context.create_browser_window
|
35
|
+
tab = window.create_browser_tab
|
36
|
+
|
37
|
+
@window = window
|
38
|
+
@tab = tab
|
39
|
+
|
40
|
+
add_headers
|
41
|
+
add_basic_auth
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_cookies(user_context)
|
45
|
+
@cookies.each do |name, value|
|
46
|
+
user_context.set_cookie(
|
47
|
+
name: name,
|
48
|
+
value: value,
|
49
|
+
domain: domain,
|
50
|
+
source_origin: source_origin
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_headers
|
56
|
+
@headers.each do |name, value|
|
57
|
+
@tab.add_headers(
|
58
|
+
url_patterns: url_patterns,
|
59
|
+
headers: [{ name: name, value: value }]
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_basic_auth
|
65
|
+
return unless @auth[:username] && @auth[:password]
|
66
|
+
|
67
|
+
@tab.basic_auth(
|
68
|
+
url_patterns: url_patterns,
|
69
|
+
username: @auth[:username],
|
70
|
+
password: @auth[:password]
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_flow
|
75
|
+
@session.status
|
76
|
+
@session.user_contexts
|
77
|
+
|
78
|
+
@tab.open_page(@url)
|
79
|
+
|
80
|
+
if @wait_network_idle
|
81
|
+
Bidi2pdf.logger.info "Waiting for network idle"
|
82
|
+
@tab.wait_until_all_finished
|
83
|
+
end
|
84
|
+
|
85
|
+
if @wait_window_loaded
|
86
|
+
Bidi2pdf.logger.info "Waiting for window to be loaded"
|
87
|
+
@tab.execute_script <<-EOF_SCRIPT
|
88
|
+
new Promise(resolve => { const check = () => window.loaded ? resolve('done') : setTimeout(check, 100); check(); });
|
89
|
+
EOF_SCRIPT
|
90
|
+
end
|
91
|
+
|
92
|
+
@tab.print(@output, print_options: @print_options)
|
93
|
+
ensure
|
94
|
+
@tab.close
|
95
|
+
@window.close
|
96
|
+
end
|
97
|
+
|
98
|
+
def uri
|
99
|
+
@uri ||= URI(@url)
|
100
|
+
end
|
101
|
+
|
102
|
+
def domain
|
103
|
+
uri.host
|
104
|
+
end
|
105
|
+
|
106
|
+
def source_origin
|
107
|
+
origin = "#{uri.scheme}://#{uri.host}"
|
108
|
+
origin += ":#{uri.port}" unless [80, 443].include?(uri.port)
|
109
|
+
origin
|
110
|
+
end
|
111
|
+
|
112
|
+
def url_patterns
|
113
|
+
[
|
114
|
+
{
|
115
|
+
type: "pattern",
|
116
|
+
protocol: uri.scheme,
|
117
|
+
hostname: uri.host,
|
118
|
+
port: uri.port.to_s
|
119
|
+
}
|
120
|
+
]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Utils
|
5
|
+
def timed(operation_name)
|
6
|
+
start_time = Time.now
|
7
|
+
result = yield
|
8
|
+
elapsed = Time.now - start_time
|
9
|
+
Bidi2pdf.logger.debug "#{operation_name} completed in #{elapsed.round(3)}s"
|
10
|
+
result
|
11
|
+
end
|
12
|
+
|
13
|
+
module_function :timed
|
14
|
+
end
|
15
|
+
end
|
data/lib/bidi2pdf.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "bidi2pdf/utils"
|
4
|
+
require_relative "bidi2pdf/launcher"
|
5
|
+
require_relative "bidi2pdf/bidi/session"
|
6
|
+
|
7
|
+
require "logger"
|
8
|
+
|
9
|
+
module Bidi2pdf
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
@logger = Logger.new($stdout)
|
13
|
+
@logger.level = Logger::DEBUG
|
14
|
+
|
15
|
+
@default_timeout = 60
|
16
|
+
|
17
|
+
class << self
|
18
|
+
attr_accessor :logger, :default_timeout
|
19
|
+
|
20
|
+
# Allow configuration through a block
|
21
|
+
def configure
|
22
|
+
yield self if block_given?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Bidi2pdf
|
2
|
+
module Chrome
|
3
|
+
class Finder
|
4
|
+
def self.location: -> String
|
5
|
+
|
6
|
+
def self.version: -> String
|
7
|
+
|
8
|
+
def self.win_location: -> String
|
9
|
+
|
10
|
+
def self.mac_location: -> String
|
11
|
+
|
12
|
+
def self.linux_location: -> String
|
13
|
+
|
14
|
+
def self.win_version: (String location) -> String
|
15
|
+
|
16
|
+
def self.linux_version: (String location) -> String
|
17
|
+
|
18
|
+
def self.mac_version: (String location) -> String
|
19
|
+
|
20
|
+
def self.call: (String process, Array[String] arg) -> String
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.find_in_paths: -> String
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|