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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +41 -1
  4. data/README.md +31 -0
  5. data/Rakefile +2 -0
  6. data/cliff.toml +114 -0
  7. data/docker/Dockerfile.chromedriver +33 -0
  8. data/docker/docker-compose.yml +24 -0
  9. data/docker/entrypoint.sh +3 -0
  10. data/docker/nginx/default.conf +42 -0
  11. data/docker/nginx/htpasswd +1 -0
  12. data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +1 -1
  13. data/lib/bidi2pdf/bidi/auth_interceptor.rb +1 -1
  14. data/lib/bidi2pdf/bidi/client.rb +55 -140
  15. data/lib/bidi2pdf/bidi/command_manager.rb +82 -0
  16. data/lib/bidi2pdf/bidi/connection_manager.rb +34 -0
  17. data/lib/bidi2pdf/bidi/session.rb +26 -9
  18. data/lib/bidi2pdf/chromedriver_manager.rb +50 -6
  19. data/lib/bidi2pdf/cli.rb +147 -18
  20. data/lib/bidi2pdf/launcher.rb +19 -6
  21. data/lib/bidi2pdf/process_tree.rb +71 -0
  22. data/lib/bidi2pdf/session_runner.rb +5 -5
  23. data/lib/bidi2pdf/version.rb +1 -1
  24. data/lib/bidi2pdf.rb +1 -0
  25. data/sig/bidi2pdf/bidi/add_headers_interceptor.rbs +20 -0
  26. data/sig/bidi2pdf/bidi/auth_interceptor.rbs +17 -0
  27. data/sig/bidi2pdf/bidi/browser.rbs +38 -0
  28. data/sig/bidi2pdf/bidi/browser_tab.rbs +42 -0
  29. data/sig/bidi2pdf/bidi/client.rbs +72 -0
  30. data/sig/bidi2pdf/bidi/event_manager.rbs +29 -0
  31. data/sig/bidi2pdf/bidi/network_event.rbs +51 -0
  32. data/sig/bidi2pdf/bidi/network_events.rbs +55 -0
  33. data/sig/bidi2pdf/bidi/print_parameters_validator.rbs +44 -0
  34. data/sig/bidi2pdf/bidi/session.rbs +52 -0
  35. data/sig/bidi2pdf/bidi/user_context.rbs +50 -0
  36. data/sig/bidi2pdf/bidi/web_socket_dispatcher.rbs +53 -0
  37. data/sig/bidi2pdf/chromedriver_manager.rbs +42 -0
  38. data/sig/bidi2pdf/cli.rbs +21 -0
  39. data/sig/bidi2pdf/launcher.rbs +38 -0
  40. data/sig/bidi2pdf/process_tree.rbs +27 -0
  41. data/sig/bidi2pdf/session_runner.rbs +51 -0
  42. data/sig/bidi2pdf/utils.rbs +5 -0
  43. data/sig/vendor/thor.rbs +13 -0
  44. data/tasks/changelog.rake +29 -0
  45. data/tasks/generate_rbs.rake +64 -0
  46. 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 :port, :websocket_url
28
+ attr_reader :session_uri, :started
29
29
 
30
- def initialize(port, headless: true)
31
- @port = port
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
- uri = URI("http://localhost:#{port}/session")
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
- response = Net::HTTP.post(uri, session_request.to_json, "Content-Type" => "application/json")
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
- Bidi::Client.new(@websocket_url).tap(&:start)
146
+ @websocket_url
130
147
  end
131
148
 
132
- # rubocop: enable Metrics/AbcSize
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, :session
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.close
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 @pid
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, 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"
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: #{options[:url]} -> #{options[:output]}"
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
- # rubocop:disable Metrics/AbcSize
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(options[:auth]) if options[:auth]
182
+ username, password = parse_auth(merged_options[:auth]) if merged_options[:auth]
61
183
 
62
184
  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]),
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: options[:port],
69
- headless: options[:headless],
70
- wait_window_loaded: options[:wait_window_loaded],
71
- wait_network_idle: options[:wait_network_idle]
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 = options[: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 options[:log_level]
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
@@ -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
- def launch
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: @manager.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.stop
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(user_context)
44
+ def add_cookies(tab)
45
45
  @cookies.each do |name, value|
46
- user_context.set_cookie(
46
+ tab.set_cookie(
47
47
  name: name,
48
48
  value: value,
49
49
  domain: domain,
50
- source_origin: source_origin
50
+ secure: uri.scheme == "https"
51
51
  )
52
52
  end
53
53
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bidi2pdf
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/bidi2pdf.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "bidi2pdf/utils"
4
+ require_relative "bidi2pdf/process_tree"
4
5
  require_relative "bidi2pdf/launcher"
5
6
  require_relative "bidi2pdf/bidi/session"
6
7