bidi2pdf 0.1.0 → 0.1.2
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 +3 -0
- data/CHANGELOG.md +41 -1
- data/README.md +31 -0
- data/Rakefile +2 -0
- data/cliff.toml +114 -0
- data/docker/Dockerfile.chromedriver +33 -0
- data/docker/docker-compose.yml +24 -0
- data/docker/entrypoint.sh +3 -0
- data/docker/nginx/default.conf +42 -0
- data/docker/nginx/htpasswd +1 -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 +55 -140
- data/lib/bidi2pdf/bidi/command_manager.rb +82 -0
- data/lib/bidi2pdf/bidi/connection_manager.rb +34 -0
- data/lib/bidi2pdf/bidi/session.rb +26 -9
- data/lib/bidi2pdf/chromedriver_manager.rb +50 -6
- data/lib/bidi2pdf/cli.rb +147 -18
- data/lib/bidi2pdf/launcher.rb +19 -6
- data/lib/bidi2pdf/process_tree.rb +71 -0
- data/lib/bidi2pdf/session_runner.rb +5 -5
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +1 -0
- 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 +65 -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
|
@@ -25,22 +25,24 @@ module Bidi2pdf
|
|
25
25
|
"goog:cdp.Page.screencastFrame"
|
26
26
|
].freeze
|
27
27
|
|
28
|
-
attr_reader :
|
28
|
+
attr_reader :session_uri, :started
|
29
29
|
|
30
|
-
def initialize(
|
31
|
-
@
|
30
|
+
def initialize(session_url:, headless: true)
|
31
|
+
@session_uri = URI(session_url)
|
32
32
|
@headless = headless
|
33
33
|
@client = nil
|
34
34
|
@browser = nil
|
35
35
|
@websocket_url = nil
|
36
|
+
@started = false
|
36
37
|
end
|
37
38
|
|
38
39
|
def start
|
40
|
+
@started = true
|
39
41
|
client
|
40
42
|
end
|
41
43
|
|
42
44
|
def client
|
43
|
-
@client ||= create_client
|
45
|
+
@client ||= @started ? create_client : nil
|
44
46
|
end
|
45
47
|
|
46
48
|
def close
|
@@ -88,9 +90,19 @@ module Bidi2pdf
|
|
88
90
|
Bidi::Browser.new(@client)
|
89
91
|
end
|
90
92
|
|
91
|
-
# rubocop: disable Metrics/AbcSize
|
92
93
|
def create_client
|
93
|
-
|
94
|
+
Bidi::Client.new(websocket_url).tap(&:start)
|
95
|
+
end
|
96
|
+
|
97
|
+
# rubocop:disable Metrics/AbcSize
|
98
|
+
def websocket_url
|
99
|
+
return @websocket_url if @websocket_url
|
100
|
+
|
101
|
+
if %w[ws wss].include?(session_uri.scheme)
|
102
|
+
@websocket_url = session_uri.to_s
|
103
|
+
return @websocket_url
|
104
|
+
end
|
105
|
+
|
94
106
|
args = %w[
|
95
107
|
--disable-gpu
|
96
108
|
--disable-popup-blocking
|
@@ -115,7 +127,12 @@ module Bidi2pdf
|
|
115
127
|
}
|
116
128
|
}
|
117
129
|
}
|
118
|
-
|
130
|
+
|
131
|
+
response = Net::HTTP.post(session_uri, session_request.to_json, "Content-Type" => "application/json")
|
132
|
+
|
133
|
+
Bidi2pdf.logger.debug "Response code: #{response.code}"
|
134
|
+
Bidi2pdf.logger.debug "Response body: #{response.body}"
|
135
|
+
|
119
136
|
session_data = JSON.parse(response.body)
|
120
137
|
|
121
138
|
Bidi2pdf.logger.debug "Session data: #{session_data}"
|
@@ -126,10 +143,10 @@ module Bidi2pdf
|
|
126
143
|
Bidi2pdf.logger.info "Created session with ID: #{session_id}"
|
127
144
|
Bidi2pdf.logger.info "WebSocket URL: #{@websocket_url}"
|
128
145
|
|
129
|
-
|
146
|
+
@websocket_url
|
130
147
|
end
|
131
148
|
|
132
|
-
# rubocop:
|
149
|
+
# rubocop:enable Metrics/AbcSize
|
133
150
|
end
|
134
151
|
end
|
135
152
|
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,18 +33,32 @@ 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
|
term_chromedriver
|
44
59
|
|
60
|
+
detect_zombie_processes
|
61
|
+
|
45
62
|
return unless process_alive?
|
46
63
|
|
47
64
|
kill_chromedriver timeout: timeout
|
@@ -51,10 +68,37 @@ module Bidi2pdf
|
|
51
68
|
|
52
69
|
private
|
53
70
|
|
71
|
+
# rubocop:disable Metrics/AbcSize
|
72
|
+
def detect_zombie_processes
|
73
|
+
debug_show_all_children
|
74
|
+
|
75
|
+
child_processes = Bidi2pdf::ProcessTree.new(@pid).children(@pid)
|
76
|
+
Bidi2pdf.logger.debug "Found child processes: #{child_processes.map(&:pid).join(", ")}"
|
77
|
+
|
78
|
+
zombie_processes = child_processes.select { |child| process_alive? child.pid }
|
79
|
+
|
80
|
+
return if zombie_processes.empty?
|
81
|
+
|
82
|
+
printable_zombie_processes = zombie_processes.map { |child| "#{child.name}:#{child.pid}" }
|
83
|
+
printable_zombie_processes_str = printable_zombie_processes.join(", ")
|
84
|
+
|
85
|
+
Bidi2pdf.logger.error "Zombie Processes detected #{printable_zombie_processes_str}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# rubocop:enable Metrics/AbcSize
|
89
|
+
|
90
|
+
def debug_show_all_children
|
91
|
+
Bidi2pdf::ProcessTree.new(@pid).traverse do |process, level|
|
92
|
+
indent = " " * level
|
93
|
+
prefix = level.zero? ? "" : "└─ "
|
94
|
+
Bidi2pdf.logger.debug "#{indent}#{prefix}PID #{process.pid} (#{process.name})"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
54
98
|
def close_session
|
55
99
|
Bidi2pdf.logger.info "Closing session"
|
56
100
|
|
57
|
-
@session
|
101
|
+
@session&.close
|
58
102
|
@session = nil
|
59
103
|
end
|
60
104
|
|
@@ -126,8 +170,8 @@ module Bidi2pdf
|
|
126
170
|
|
127
171
|
# rubocop: enable Metrics/AbcSize
|
128
172
|
|
129
|
-
def process_alive?
|
130
|
-
return false unless
|
173
|
+
def process_alive?(pid = @pid)
|
174
|
+
return false unless pid
|
131
175
|
|
132
176
|
begin
|
133
177
|
Process.waitpid(@pid, Process::WNOHANG)
|
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
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sys/proctable"
|
4
|
+
module Bidi2pdf
|
5
|
+
class ProcessTree
|
6
|
+
include Sys
|
7
|
+
|
8
|
+
def initialize(root_pid = nil)
|
9
|
+
@root_pid = root_pid
|
10
|
+
@process_map = build_process_map
|
11
|
+
connect_children
|
12
|
+
end
|
13
|
+
|
14
|
+
def children(of_pid)
|
15
|
+
return [] unless @process_map[of_pid]
|
16
|
+
|
17
|
+
direct_children = @process_map[of_pid][:children].map do |child_pid|
|
18
|
+
@process_map[child_pid][:info]
|
19
|
+
end
|
20
|
+
|
21
|
+
(direct_children + direct_children.flat_map { |child| children(child.pid) }).uniq
|
22
|
+
end
|
23
|
+
|
24
|
+
def traverse(&handler)
|
25
|
+
handler = method(:print_handler) unless handler.is_a?(Proc)
|
26
|
+
|
27
|
+
root_pids.each { |pid| traverse_branch(pid, &handler) }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def print_handler(process, level)
|
33
|
+
indent = " " * level
|
34
|
+
prefix = level.zero? ? "" : "└─ "
|
35
|
+
puts "#{indent}#{prefix}PID #{process.pid} (#{process.name})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_process_map
|
39
|
+
ProcTable.ps.each_with_object({}) do |process, map|
|
40
|
+
map[process.pid] = { info: process, children: [] }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def connect_children
|
45
|
+
@process_map.each_value do |entry|
|
46
|
+
parent_pid = entry[:info].ppid
|
47
|
+
@process_map[parent_pid][:children] << entry[:info].pid if parent_pid && @process_map.key?(parent_pid)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def root_pids
|
52
|
+
return [@root_pid] if @root_pid
|
53
|
+
|
54
|
+
@process_map.values
|
55
|
+
.select { |entry| entry[:info].ppid.nil? || !@process_map.key?(entry[:info].ppid) }
|
56
|
+
.map { |entry| entry[:info].pid }
|
57
|
+
end
|
58
|
+
|
59
|
+
def traverse_branch(pid, level = 0, &handler)
|
60
|
+
return unless @process_map[pid]
|
61
|
+
|
62
|
+
process = @process_map[pid][:info]
|
63
|
+
|
64
|
+
handler.call(process, level)
|
65
|
+
|
66
|
+
@process_map[pid][:children].each do |child_pid|
|
67
|
+
traverse_branch(child_pid, level + 1, &handler)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -29,25 +29,25 @@ module Bidi2pdf
|
|
29
29
|
browser = @session.browser
|
30
30
|
user_context = browser.create_user_context
|
31
31
|
|
32
|
-
add_cookies(user_context)
|
33
|
-
|
34
32
|
window = user_context.create_browser_window
|
35
33
|
tab = window.create_browser_tab
|
36
34
|
|
37
35
|
@window = window
|
38
36
|
@tab = tab
|
39
37
|
|
38
|
+
add_cookies(tab)
|
39
|
+
|
40
40
|
add_headers
|
41
41
|
add_basic_auth
|
42
42
|
end
|
43
43
|
|
44
|
-
def add_cookies(
|
44
|
+
def add_cookies(tab)
|
45
45
|
@cookies.each do |name, value|
|
46
|
-
|
46
|
+
tab.set_cookie(
|
47
47
|
name: name,
|
48
48
|
value: value,
|
49
49
|
domain: domain,
|
50
|
-
|
50
|
+
secure: uri.scheme == "https"
|
51
51
|
)
|
52
52
|
end
|
53
53
|
end
|
data/lib/bidi2pdf/version.rb
CHANGED