bidi2pdf 0.1.1 → 0.1.3
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 +13 -0
- data/CHANGELOG.md +40 -2
- data/README.md +10 -0
- data/Rakefile +2 -0
- data/cliff.toml +138 -0
- data/docker/Dockerfile.chromedriver +33 -0
- data/docker/docker-compose.yml +7 -0
- data/docker/entrypoint.sh +3 -0
- data/docker/nginx/default.conf +6 -0
- data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +1 -1
- data/lib/bidi2pdf/bidi/auth_interceptor.rb +1 -1
- data/lib/bidi2pdf/bidi/client.rb +62 -146
- data/lib/bidi2pdf/bidi/command_manager.rb +82 -0
- data/lib/bidi2pdf/bidi/connection_manager.rb +34 -0
- data/lib/bidi2pdf/bidi/session.rb +30 -22
- data/lib/bidi2pdf/chromedriver_manager.rb +40 -16
- data/lib/bidi2pdf/cli.rb +147 -18
- data/lib/bidi2pdf/launcher.rb +19 -6
- data/lib/bidi2pdf/version.rb +1 -1
- data/sig/bidi2pdf/bidi/add_headers_interceptor.rbs +20 -0
- data/sig/bidi2pdf/bidi/auth_interceptor.rbs +17 -0
- data/sig/bidi2pdf/bidi/browser.rbs +38 -0
- data/sig/bidi2pdf/bidi/browser_tab.rbs +42 -0
- data/sig/bidi2pdf/bidi/client.rbs +72 -0
- data/sig/bidi2pdf/bidi/event_manager.rbs +29 -0
- data/sig/bidi2pdf/bidi/network_event.rbs +51 -0
- data/sig/bidi2pdf/bidi/network_events.rbs +55 -0
- data/sig/bidi2pdf/bidi/print_parameters_validator.rbs +44 -0
- data/sig/bidi2pdf/bidi/session.rbs +52 -0
- data/sig/bidi2pdf/bidi/user_context.rbs +50 -0
- data/sig/bidi2pdf/bidi/web_socket_dispatcher.rbs +53 -0
- data/sig/bidi2pdf/chromedriver_manager.rbs +42 -0
- data/sig/bidi2pdf/cli.rbs +21 -0
- data/sig/bidi2pdf/launcher.rbs +38 -0
- data/sig/bidi2pdf/process_tree.rbs +27 -0
- data/sig/bidi2pdf/session_runner.rbs +51 -0
- data/sig/bidi2pdf/utils.rbs +5 -0
- data/sig/vendor/thor.rbs +13 -0
- data/tasks/changelog.rake +29 -0
- data/tasks/generate_rbs.rake +64 -0
- metadata +48 -8
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
class CommandManager
|
6
|
+
def initialize(socket, logger:)
|
7
|
+
@socket = socket
|
8
|
+
@logger = logger
|
9
|
+
|
10
|
+
@id = 0
|
11
|
+
@next_id_mutex = Mutex.new
|
12
|
+
@pending_responses = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_cmd(method, params = {})
|
16
|
+
id = next_id
|
17
|
+
payload = { id: id, method: method, params: params }
|
18
|
+
|
19
|
+
@logger.debug "Sending command: #{redact_sensitive_fields(payload).inspect}"
|
20
|
+
@socket.send(payload.to_json)
|
21
|
+
|
22
|
+
id
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout)
|
26
|
+
id = send_cmd(method, params)
|
27
|
+
queue = @pending_responses[id]
|
28
|
+
|
29
|
+
response = queue.pop(timeout: timeout)
|
30
|
+
raise_timeout_error(id, method, params) if response.nil?
|
31
|
+
raise "Error response: #{response["error"]}" if response["error"]
|
32
|
+
|
33
|
+
block_given? ? yield(response) : response
|
34
|
+
ensure
|
35
|
+
@pending_responses.delete(id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def queue_for(id)
|
39
|
+
@pending_responses[id]
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_response(data)
|
43
|
+
if (id = data["id"]) && @pending_responses.key?(id)
|
44
|
+
@pending_responses[id]&.push(data)
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def next_id
|
53
|
+
@next_id_mutex.synchronize do
|
54
|
+
@id += 1
|
55
|
+
@pending_responses[@id] = Thread::Queue.new
|
56
|
+
@id
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def redact_sensitive_fields(obj, sensitive_keys = %w[value token password authorization username])
|
61
|
+
case obj
|
62
|
+
when Hash
|
63
|
+
obj.transform_values.with_index do |v, idx|
|
64
|
+
k = obj.keys[idx]
|
65
|
+
sensitive_keys.include?(k.to_s.downcase) ? "[REDACTED]" : redact_sensitive_fields(v, sensitive_keys)
|
66
|
+
end
|
67
|
+
when Array
|
68
|
+
obj.map { |item| redact_sensitive_fields(item, sensitive_keys) }
|
69
|
+
else
|
70
|
+
obj
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def raise_timeout_error(id, method, params)
|
75
|
+
# rubocop:disable Layout/LineLength
|
76
|
+
@logger.error "Timeout waiting for response to command #{id}, cmd: #{method}, params: #{redact_sensitive_fields(params).inspect}"
|
77
|
+
# rubocop:enable Layout/LineLength
|
78
|
+
raise "Timeout waiting for response to command ID #{id}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
class ConnectionManager
|
6
|
+
def initialize(logger:)
|
7
|
+
@logger = logger
|
8
|
+
@connected = false
|
9
|
+
@mutex = Mutex.new
|
10
|
+
@cv = ConditionVariable.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def mark_connected
|
14
|
+
@mutex.synchronize do
|
15
|
+
@connected = true
|
16
|
+
@cv.broadcast
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def wait_until_open(timeout:)
|
21
|
+
@mutex.synchronize do
|
22
|
+
unless @connected
|
23
|
+
@logger.debug "Waiting for WebSocket connection to open"
|
24
|
+
@cv.wait(@mutex, timeout)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
raise "WebSocket connection did not open in time" unless @connected
|
29
|
+
|
30
|
+
@logger.debug "WebSocket connection is open"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -10,42 +10,35 @@ require_relative "user_context"
|
|
10
10
|
module Bidi2pdf
|
11
11
|
module Bidi
|
12
12
|
class Session
|
13
|
-
SUBSCRIBE_EVENTS = [
|
14
|
-
|
15
|
-
|
16
|
-
"log",
|
17
|
-
"script",
|
18
|
-
"goog:cdp.Debugger.scriptParsed",
|
19
|
-
"goog:cdp.CSS.styleSheetAdded",
|
20
|
-
"goog:cdp.Runtime.executionContextsCleared",
|
21
|
-
# Tracing
|
22
|
-
"goog:cdp.Tracing.tracingComplete",
|
23
|
-
"goog:cdp.Network.requestWillBeSent",
|
24
|
-
"goog:cdp.Debugger.scriptParsed",
|
25
|
-
"goog:cdp.Page.screencastFrame"
|
13
|
+
SUBSCRIBE_EVENTS = %w[
|
14
|
+
log
|
15
|
+
script
|
26
16
|
].freeze
|
27
17
|
|
28
|
-
attr_reader :
|
18
|
+
attr_reader :session_uri, :started
|
29
19
|
|
30
|
-
def initialize(
|
31
|
-
@
|
20
|
+
def initialize(session_url:, headless: true)
|
21
|
+
@session_uri = URI(session_url)
|
32
22
|
@headless = headless
|
33
23
|
@client = nil
|
34
24
|
@browser = nil
|
35
25
|
@websocket_url = nil
|
26
|
+
@started = false
|
36
27
|
end
|
37
28
|
|
38
29
|
def start
|
30
|
+
@started = true
|
39
31
|
client
|
40
32
|
end
|
41
33
|
|
42
34
|
def client
|
43
|
-
@client ||= create_client
|
35
|
+
@client ||= @started ? create_client : nil
|
44
36
|
end
|
45
37
|
|
46
38
|
def close
|
47
39
|
client&.send_cmd_and_wait("session.end", {}) do |response|
|
48
40
|
Bidi2pdf.logger.debug "Session ended: #{response}"
|
41
|
+
@client&.close
|
49
42
|
@client = nil
|
50
43
|
@websocket_url = nil
|
51
44
|
@browser = nil
|
@@ -88,9 +81,19 @@ module Bidi2pdf
|
|
88
81
|
Bidi::Browser.new(@client)
|
89
82
|
end
|
90
83
|
|
91
|
-
# rubocop: disable Metrics/AbcSize
|
92
84
|
def create_client
|
93
|
-
|
85
|
+
Bidi::Client.new(websocket_url).tap(&:start)
|
86
|
+
end
|
87
|
+
|
88
|
+
# rubocop:disable Metrics/AbcSize
|
89
|
+
def websocket_url
|
90
|
+
return @websocket_url if @websocket_url
|
91
|
+
|
92
|
+
if %w[ws wss].include?(session_uri.scheme)
|
93
|
+
@websocket_url = session_uri.to_s
|
94
|
+
return @websocket_url
|
95
|
+
end
|
96
|
+
|
94
97
|
args = %w[
|
95
98
|
--disable-gpu
|
96
99
|
--disable-popup-blocking
|
@@ -115,7 +118,12 @@ module Bidi2pdf
|
|
115
118
|
}
|
116
119
|
}
|
117
120
|
}
|
118
|
-
|
121
|
+
|
122
|
+
response = Net::HTTP.post(session_uri, session_request.to_json, "Content-Type" => "application/json")
|
123
|
+
|
124
|
+
Bidi2pdf.logger.debug "Response code: #{response.code}"
|
125
|
+
Bidi2pdf.logger.debug "Response body: #{response.body}"
|
126
|
+
|
119
127
|
session_data = JSON.parse(response.body)
|
120
128
|
|
121
129
|
Bidi2pdf.logger.debug "Session data: #{session_data}"
|
@@ -126,10 +134,10 @@ module Bidi2pdf
|
|
126
134
|
Bidi2pdf.logger.info "Created session with ID: #{session_id}"
|
127
135
|
Bidi2pdf.logger.info "WebSocket URL: #{@websocket_url}"
|
128
136
|
|
129
|
-
|
137
|
+
@websocket_url
|
130
138
|
end
|
131
139
|
|
132
|
-
# rubocop:
|
140
|
+
# rubocop:enable Metrics/AbcSize
|
133
141
|
end
|
134
142
|
end
|
135
143
|
end
|
@@ -4,17 +4,20 @@ require "chromedriver/binary"
|
|
4
4
|
|
5
5
|
module Bidi2pdf
|
6
6
|
class ChromedriverManager
|
7
|
-
attr_reader :port, :pid, :
|
7
|
+
attr_reader :port, :pid, :started
|
8
8
|
|
9
9
|
def initialize(port: 0, headless: true)
|
10
10
|
@port = port
|
11
11
|
@headless = headless
|
12
12
|
@session = nil
|
13
|
+
@started = false
|
13
14
|
end
|
14
15
|
|
15
16
|
def start
|
16
17
|
return @pid if @pid
|
17
18
|
|
19
|
+
@started = true
|
20
|
+
|
18
21
|
update_chromedriver
|
19
22
|
cmd = build_cmd
|
20
23
|
Bidi2pdf.logger.info "Starting Chromedriver with command: #{cmd}"
|
@@ -30,19 +33,33 @@ module Bidi2pdf
|
|
30
33
|
|
31
34
|
at_exit { stop }
|
32
35
|
|
33
|
-
@session = Bidi::Session.new(@port, headless: @headless)
|
34
|
-
|
35
36
|
@pid
|
36
37
|
end
|
37
38
|
|
39
|
+
def session
|
40
|
+
return unless @started
|
41
|
+
|
42
|
+
@session ||= Bidi::Session.new(session_url: session_url, headless: @headless)
|
43
|
+
end
|
44
|
+
|
45
|
+
def session_url
|
46
|
+
return unless @started
|
47
|
+
|
48
|
+
"http://localhost:#{@port}/session"
|
49
|
+
end
|
50
|
+
|
38
51
|
def stop(timeout: 5)
|
39
52
|
return unless @pid
|
40
53
|
|
54
|
+
@started = false
|
55
|
+
|
41
56
|
close_session
|
42
57
|
|
43
|
-
|
58
|
+
debug_show_all_children
|
59
|
+
|
60
|
+
old_childprocesses = term_chromedriver
|
44
61
|
|
45
|
-
detect_zombie_processes
|
62
|
+
detect_zombie_processes old_childprocesses
|
46
63
|
|
47
64
|
return unless process_alive?
|
48
65
|
|
@@ -53,14 +70,10 @@ module Bidi2pdf
|
|
53
70
|
|
54
71
|
private
|
55
72
|
|
56
|
-
|
57
|
-
|
58
|
-
debug_show_all_children
|
73
|
+
def detect_zombie_processes(old_childprocesses)
|
74
|
+
Bidi2pdf.logger.debug "Old child processes for #{@pid}: #{old_childprocesses.map(&:pid).join(", ")}"
|
59
75
|
|
60
|
-
|
61
|
-
Bidi2pdf.logger.debug "Found child processes: #{child_processes.map(&:pid).join(", ")}"
|
62
|
-
|
63
|
-
zombie_processes = child_processes.select { |child| process_alive? child.pid }
|
76
|
+
zombie_processes = old_childprocesses.select { |child| process_alive? child.pid }
|
64
77
|
|
65
78
|
return if zombie_processes.empty?
|
66
79
|
|
@@ -68,9 +81,18 @@ module Bidi2pdf
|
|
68
81
|
printable_zombie_processes_str = printable_zombie_processes.join(", ")
|
69
82
|
|
70
83
|
Bidi2pdf.logger.error "Zombie Processes detected #{printable_zombie_processes_str}"
|
84
|
+
|
85
|
+
term_zombie_processes zombie_processes
|
71
86
|
end
|
72
87
|
|
73
|
-
|
88
|
+
def term_zombie_processes(zombie_processes)
|
89
|
+
Bidi2pdf.logger.info "Terminating zombie processes: #{zombie_processes.map(&:pid).join(", ")}"
|
90
|
+
|
91
|
+
zombie_processes.each do |child|
|
92
|
+
Bidi2pdf.logger.debug "Terminating PID #{child.pid} (#{child.name})"
|
93
|
+
Process.kill("TERM", child.pid)
|
94
|
+
end
|
95
|
+
end
|
74
96
|
|
75
97
|
def debug_show_all_children
|
76
98
|
Bidi2pdf::ProcessTree.new(@pid).traverse do |process, level|
|
@@ -83,14 +105,16 @@ module Bidi2pdf
|
|
83
105
|
def close_session
|
84
106
|
Bidi2pdf.logger.info "Closing session"
|
85
107
|
|
86
|
-
@session
|
108
|
+
@session&.close
|
87
109
|
@session = nil
|
88
110
|
end
|
89
111
|
|
90
112
|
def term_chromedriver
|
91
|
-
Bidi2pdf.
|
113
|
+
Bidi2pdf::ProcessTree.new(@pid).children(@pid).tap do |_child_processes|
|
114
|
+
Bidi2pdf.logger.info "Stopping Chromedriver (PID #{@pid})"
|
92
115
|
|
93
|
-
|
116
|
+
Process.kill("TERM", @pid)
|
117
|
+
end
|
94
118
|
rescue Errno::ESRCH
|
95
119
|
Bidi2pdf.logger.debug "Process already gone"
|
96
120
|
@pid = nil
|
data/lib/bidi2pdf/cli.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "thor"
|
4
|
+
require "yaml"
|
4
5
|
|
5
6
|
module Bidi2pdf
|
7
|
+
# rubocop:disable Metrics/AbcSize
|
6
8
|
class CLI < Thor
|
9
|
+
class_option :config, type: :string, desc: "Load configuration from YAML file"
|
10
|
+
|
7
11
|
desc "render", "Render a URL to PDF using Chrome BiDi"
|
8
12
|
long_desc <<~USAGE, wrap: false
|
9
13
|
Example:
|
@@ -26,11 +30,11 @@ module Bidi2pdf
|
|
26
30
|
Set --port to 0 for a random ChromeDriver port.
|
27
31
|
USAGE
|
28
32
|
|
29
|
-
option :url,
|
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"
|
33
|
+
option :url, desc: "The URL to render"
|
34
|
+
option :output, default: "output.pdf", desc: "Filename for the output PDF", aliases: "-o"
|
35
|
+
option :cookie, type: :array, default: [], banner: "name=value", desc: "One or more cookies", aliases: "-C"
|
36
|
+
option :header, type: :array, default: [], banner: "name=value", desc: "One or more custom headers", aliases: "-H"
|
37
|
+
option :auth, type: :string, banner: "user:pass", desc: "Basic auth credentials", aliases: "-a"
|
34
38
|
option :headless, type: :boolean, default: true, desc: "Run Chrome in headless mode"
|
35
39
|
option :port, type: :numeric, default: 0, desc: "Port to run ChromeDriver on (0 = auto)"
|
36
40
|
option :wait_window_loaded,
|
@@ -39,36 +43,156 @@ module Bidi2pdf
|
|
39
43
|
desc: "Wait for the window to be fully loaded (windoow.loaded set by your script)"
|
40
44
|
option :wait_network_idle, type: :boolean, default: false, desc: "Wait for network to be idle"
|
41
45
|
option :default_timeout, type: :numeric, default: 60, desc: "Default timeout for commands"
|
46
|
+
option :remote_browser_url, type: :string, desc: "Remote browser URL for ChromeDriver"
|
42
47
|
option :log_level,
|
43
48
|
type: :string,
|
44
49
|
default: "info", enum: %w[debug info warn error fatal unknown], desc: "Set log level"
|
45
50
|
|
51
|
+
option :background, type: :boolean, default: true, desc: "Print background graphics"
|
52
|
+
option :margin_top, type: :numeric, default: 1.0, desc: "Top margin in inches"
|
53
|
+
option :margin_bottom, type: :numeric, default: 1.0, desc: "Bottom margin in inches"
|
54
|
+
option :margin_left, type: :numeric, default: 1.0, desc: "Left margin in inches"
|
55
|
+
option :margin_right, type: :numeric, default: 1.0, desc: "Right margin in inches"
|
56
|
+
option :orientation, type: :string, default: "portrait", enum: %w[portrait landscape], desc: "Page orientation"
|
57
|
+
option :page_width, type: :numeric, default: 21.59, desc: "Page width in cm (min 0.0352)"
|
58
|
+
option :page_height, type: :numeric, default: 27.94, desc: "Page height in cm (min 0.0352)"
|
59
|
+
option :page_ranges, type: :array, desc: "Page ranges to print (e.g., 1-2 4 6)"
|
60
|
+
option :scale, type: :numeric, default: 1.0, desc: "Scale between 0.1 and 2.0"
|
61
|
+
option :shrink_to_fit, type: :boolean, default: true, desc: "Shrink content to fit page"
|
62
|
+
|
46
63
|
def render
|
64
|
+
load_config
|
65
|
+
|
66
|
+
validate_required_options! :url
|
67
|
+
|
47
68
|
configure
|
48
69
|
|
49
|
-
Bidi2pdf.logger.info "Rendering: #{
|
70
|
+
Bidi2pdf.logger.info "Rendering: #{merged_options[:url]} -> #{merged_options[:output]}"
|
71
|
+
Bidi2pdf.logger.info "Print options: #{print_options.inspect}" if print_options
|
72
|
+
|
73
|
+
validate_print_options(print_options) if print_options
|
50
74
|
|
51
75
|
launcher.launch
|
76
|
+
ensure
|
77
|
+
launcher.stop if defined?(@launcher) && @launcher
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "version", "Show bidi2pdf version"
|
81
|
+
|
82
|
+
def version
|
83
|
+
puts "bidi2pdf #{Bidi2pdf::VERSION}"
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "template", "Generate a config file template"
|
87
|
+
option :output, default: "bidi2pdf.yml", desc: "Output configuration filename"
|
88
|
+
|
89
|
+
def template
|
90
|
+
config = {
|
91
|
+
"url" => "https://example.com",
|
92
|
+
"output" => "output.pdf",
|
93
|
+
"headless" => true,
|
94
|
+
"print_options" => {
|
95
|
+
"background" => true,
|
96
|
+
"orientation" => "portrait",
|
97
|
+
"margin" => {
|
98
|
+
"top" => 1.0,
|
99
|
+
"bottom" => 1.0,
|
100
|
+
"left" => 1.0,
|
101
|
+
"right" => 1.0
|
102
|
+
}
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
File.write(merged_options[:output], config.to_yaml)
|
107
|
+
puts "Config template written to #{merged_options[:output]}"
|
52
108
|
end
|
53
109
|
|
54
110
|
private
|
55
111
|
|
56
|
-
|
112
|
+
def load_config
|
113
|
+
return unless options[:config] && File.exist?(options[:config])
|
114
|
+
|
115
|
+
YAML.load_file(options[:config]).transform_keys(&:to_sym)
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate_required_options!(*keys)
|
119
|
+
keys.each do |key|
|
120
|
+
raise Thor::Error, "Missing required option: --#{key.to_s.tr("_", "-")}" unless merged_options[key]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_print_options(opts)
|
125
|
+
Bidi2pdf::Bidi::PrintParametersValidator.validate!(opts)
|
126
|
+
rescue ArgumentError => e
|
127
|
+
raise Thor::Error, "Invalid print option: #{e.message}"
|
128
|
+
end
|
129
|
+
|
130
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
131
|
+
def print_options
|
132
|
+
opts = {}
|
133
|
+
|
134
|
+
assign_if_provided(opts, :background)
|
135
|
+
assign_if_provided(opts, :orientation)
|
136
|
+
opts[:pageRanges] = merged_options[:page_ranges] if merged_options[:page_ranges]
|
137
|
+
|
138
|
+
if option_provided?(:scale)
|
139
|
+
scale = merged_options[:scale]
|
140
|
+
raise ArgumentError, "Scale must be between 0.1 and 2.0" unless (0.1..2.0).include?(scale)
|
141
|
+
|
142
|
+
opts[:scale] = scale
|
143
|
+
end
|
144
|
+
|
145
|
+
assign_if_provided(opts, :shrinkToFit, :shrink_to_fit)
|
146
|
+
|
147
|
+
# Margins
|
148
|
+
margin_keys = {
|
149
|
+
top: :margin_top,
|
150
|
+
bottom: :margin_bottom,
|
151
|
+
left: :margin_left,
|
152
|
+
right: :margin_right
|
153
|
+
}
|
154
|
+
margins = {}
|
155
|
+
margin_keys.each do |short, full|
|
156
|
+
assign_if_provided(margins, short, full)
|
157
|
+
end
|
158
|
+
opts[:margin] = margins unless margins.empty?
|
159
|
+
|
160
|
+
# Page size
|
161
|
+
page = {}
|
162
|
+
assign_if_provided(page, :width, :page_width)
|
163
|
+
assign_if_provided(page, :height, :page_height)
|
164
|
+
opts[:page] = page unless page.empty?
|
165
|
+
|
166
|
+
opts.empty? ? nil : opts
|
167
|
+
end
|
168
|
+
|
169
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
170
|
+
|
171
|
+
def option_provided?(key)
|
172
|
+
ARGV.include?("--#{key.to_s.tr("_", "-")}") || ARGV.include?("--#{key}")
|
173
|
+
end
|
174
|
+
|
175
|
+
def assign_if_provided(hash, key, option_key = key)
|
176
|
+
hash[key] = merged_options[option_key] if option_provided?(option_key)
|
177
|
+
end
|
178
|
+
|
57
179
|
def launcher
|
58
180
|
# rubocop:disable Layout/BeginEndAlignment
|
59
181
|
@launcher ||= begin
|
60
|
-
username, password = parse_auth(
|
182
|
+
username, password = parse_auth(merged_options[:auth]) if merged_options[:auth]
|
61
183
|
|
62
184
|
Bidi2pdf::Launcher.new(
|
63
|
-
url:
|
64
|
-
output:
|
65
|
-
cookies: parse_key_values(
|
66
|
-
headers: parse_key_values(
|
185
|
+
url: merged_options[:url],
|
186
|
+
output: merged_options[:output],
|
187
|
+
cookies: parse_key_values(merged_options[:cookie]),
|
188
|
+
headers: parse_key_values(merged_options[:header]),
|
67
189
|
auth: { username: username, password: password },
|
68
|
-
port:
|
69
|
-
|
70
|
-
|
71
|
-
|
190
|
+
port: merged_options[:port],
|
191
|
+
remote_browser_url: merged_options[:remote_browser_url],
|
192
|
+
headless: merged_options[:headless],
|
193
|
+
wait_window_loaded: merged_options[:wait_window_loaded],
|
194
|
+
wait_network_idle: merged_options[:wait_network_idle],
|
195
|
+
print_options: print_options
|
72
196
|
)
|
73
197
|
end
|
74
198
|
# rubocop:enable Layout/BeginEndAlignment
|
@@ -79,7 +203,7 @@ module Bidi2pdf
|
|
79
203
|
def configure
|
80
204
|
Bidi2pdf.configure do |config|
|
81
205
|
config.logger.level = log_level
|
82
|
-
config.default_timeout =
|
206
|
+
config.default_timeout = merged_options[:default_timeout]
|
83
207
|
|
84
208
|
Chromedriver::Binary.configure do |c|
|
85
209
|
c.logger.level = log_level
|
@@ -88,7 +212,7 @@ module Bidi2pdf
|
|
88
212
|
end
|
89
213
|
|
90
214
|
def log_level
|
91
|
-
case
|
215
|
+
case merged_options[:log_level]
|
92
216
|
when "debug" then Logger::DEBUG
|
93
217
|
when "warn" then Logger::WARN
|
94
218
|
when "error" then Logger::ERROR
|
@@ -114,5 +238,10 @@ module Bidi2pdf
|
|
114
238
|
|
115
239
|
[user, pass]
|
116
240
|
end
|
241
|
+
|
242
|
+
def merged_options
|
243
|
+
defaults = load_config || {}
|
244
|
+
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(options))
|
245
|
+
end
|
117
246
|
end
|
118
247
|
end
|
data/lib/bidi2pdf/launcher.rb
CHANGED
@@ -6,8 +6,9 @@ require_relative "bidi/session"
|
|
6
6
|
|
7
7
|
module Bidi2pdf
|
8
8
|
class Launcher
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
9
10
|
def initialize(url:, output:, cookies:, headers:, auth:, headless: true, port: 0, wait_window_loaded: false,
|
10
|
-
wait_network_idle: false, print_options: {})
|
11
|
+
wait_network_idle: false, print_options: {}, remote_browser_url: nil)
|
11
12
|
@url = url
|
12
13
|
@port = port
|
13
14
|
@headless = headless
|
@@ -19,14 +20,14 @@ module Bidi2pdf
|
|
19
20
|
@wait_window_loaded = wait_window_loaded
|
20
21
|
@wait_network_idle = wait_network_idle
|
21
22
|
@print_options = print_options || {}
|
23
|
+
@remote_browser_url = remote_browser_url
|
22
24
|
end
|
23
25
|
|
24
|
-
|
25
|
-
@manager = ChromedriverManager.new(port: @port, headless: @headless)
|
26
|
-
@manager.start
|
26
|
+
# rubocop:enable Metrics/ParameterLists
|
27
27
|
|
28
|
+
def launch
|
28
29
|
runner = SessionRunner.new(
|
29
|
-
session:
|
30
|
+
session: session,
|
30
31
|
url: @url,
|
31
32
|
output: @output,
|
32
33
|
cookies: @cookies,
|
@@ -40,7 +41,19 @@ module Bidi2pdf
|
|
40
41
|
end
|
41
42
|
|
42
43
|
def stop
|
43
|
-
@manager
|
44
|
+
@manager&.stop
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def session
|
50
|
+
if @remote_browser_url
|
51
|
+
Bidi::Session.new(session_url: @remote_browser_url, headless: @headless)
|
52
|
+
else
|
53
|
+
@manager = ChromedriverManager.new(port: @port, headless: @headless)
|
54
|
+
@manager.start
|
55
|
+
@manager.session
|
56
|
+
end
|
44
57
|
end
|
45
58
|
end
|
46
59
|
end
|
data/lib/bidi2pdf/version.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bidi2pdf
|
2
|
+
module Bidi
|
3
|
+
class AddHeadersInterceptor
|
4
|
+
@id: String
|
5
|
+
@client: untyped
|
6
|
+
@headers: Hash[String, String]
|
7
|
+
|
8
|
+
attr_reader id: String
|
9
|
+
attr_reader headers: Hash[String, String]
|
10
|
+
|
11
|
+
def initialize: (String id, Hash[String, String] headers, untyped client) -> void
|
12
|
+
|
13
|
+
def handle_event: (Hash[String, untyped] response) -> (nil | untyped)
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader client: untyped
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Bidi2pdf
|
2
|
+
module Bidi
|
3
|
+
class AuthInterceptor
|
4
|
+
@credentials: Hash[Symbol, String]?
|
5
|
+
|
6
|
+
def initialize: (?credentials: Hash[Symbol, String]?) -> void
|
7
|
+
|
8
|
+
def intercept: (untyped request) -> untyped
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def add_auth_header: (untyped request) -> void
|
13
|
+
|
14
|
+
def auth_header_value: () -> String?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|