chromate-rb 0.0.1.pre → 0.0.3.pre

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +72 -3
  4. data/README.md +33 -6
  5. data/Rakefile +48 -16
  6. data/docker_root/Gemfile +4 -0
  7. data/docker_root/Gemfile.lock +28 -0
  8. data/docker_root/TestInDocker.gif +0 -0
  9. data/docker_root/app.rb +87 -0
  10. data/dockerfiles/Dockerfile +21 -7
  11. data/dockerfiles/README.md +49 -0
  12. data/docs/BOT_BROWSER.md +74 -0
  13. data/docs/README.md +74 -0
  14. data/docs/browser.md +124 -102
  15. data/docs/client.md +126 -0
  16. data/docs/element.md +365 -0
  17. data/docs/elements/checkbox.md +69 -0
  18. data/docs/elements/radio.md +57 -0
  19. data/lib/bot_browser/downloader.rb +64 -0
  20. data/lib/bot_browser/installer.rb +99 -0
  21. data/lib/bot_browser.rb +43 -0
  22. data/lib/chromate/actions/dom.rb +35 -27
  23. data/lib/chromate/actions/navigate.rb +7 -5
  24. data/lib/chromate/actions/screenshot.rb +71 -14
  25. data/lib/chromate/actions/stealth.rb +62 -0
  26. data/lib/chromate/binary.rb +83 -0
  27. data/lib/chromate/browser.rb +120 -24
  28. data/lib/chromate/c_logger.rb +8 -0
  29. data/lib/chromate/client.rb +65 -26
  30. data/lib/chromate/configuration.rb +31 -14
  31. data/lib/chromate/element.rb +119 -16
  32. data/lib/chromate/elements/checkbox.rb +40 -0
  33. data/lib/chromate/elements/option.rb +43 -0
  34. data/lib/chromate/elements/radio.rb +37 -0
  35. data/lib/chromate/elements/select.rb +50 -6
  36. data/lib/chromate/elements/tags.rb +29 -0
  37. data/lib/chromate/exceptions.rb +2 -0
  38. data/lib/chromate/files/agents.json +11 -0
  39. data/lib/chromate/files/stealth.js +199 -0
  40. data/lib/chromate/hardwares/keyboard_controller.rb +45 -0
  41. data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
  42. data/lib/chromate/hardwares/mouse_controller.rb +55 -11
  43. data/lib/chromate/hardwares/mouses/linux_controller.rb +124 -21
  44. data/lib/chromate/hardwares/mouses/mac_os_controller.rb +6 -6
  45. data/lib/chromate/hardwares/mouses/virtual_controller.rb +95 -7
  46. data/lib/chromate/hardwares/mouses/x11.rb +36 -0
  47. data/lib/chromate/hardwares.rb +19 -3
  48. data/lib/chromate/helpers.rb +22 -15
  49. data/lib/chromate/user_agent.rb +41 -15
  50. data/lib/chromate/version.rb +1 -1
  51. data/lib/chromate.rb +2 -0
  52. data/logo.png +0 -0
  53. data/results/bot.png +0 -0
  54. data/results/brotector.png +0 -0
  55. data/results/cloudflare.png +0 -0
  56. data/results/headers.png +0 -0
  57. data/results/pixelscan.png +0 -0
  58. metadata +45 -2
@@ -3,36 +3,44 @@
3
3
  module Chromate
4
4
  module Actions
5
5
  module Dom
6
- def find_element(selector)
7
- Chromate::Element.new(selector, @client)
8
- end
9
-
10
- def click_element(selector)
11
- find_element(selector).click
12
- end
13
-
14
- def hover_element(selector)
15
- find_element(selector).hover
16
- end
17
-
18
- def type_text(selector, text)
19
- find_element(selector).type(text)
20
- end
21
-
22
- def get_text(selector)
23
- find_element(selector).text
24
- end
25
-
26
- def get_property(selector, property)
27
- find_element(selector).attributes[property]
28
- end
29
-
30
- def select_option(selector, option)
31
- Chromate::Elements::Select.new(selector, @client).select_option(option)
6
+ # @return [String]
7
+ def source
8
+ evaluate_script('document.documentElement.outerHTML')
32
9
  end
33
10
 
11
+ # @param selector [String] CSS selector
12
+ # @return [Chromate::Element]
13
+ def find_element(selector)
14
+ base_element = Chromate::Element.new(selector, @client)
15
+
16
+ options = {
17
+ object_id: base_element.object_id,
18
+ node_id: base_element.node_id,
19
+ root_id: base_element.root_id
20
+ }
21
+
22
+ if base_element.select?
23
+ Chromate::Elements::Select.new(selector, @client, **options)
24
+ elsif base_element.option?
25
+ Chromate::Elements::Option.new(selector, @client, **options)
26
+ elsif base_element.radio?
27
+ Chromate::Elements::Radio.new(selector, @client, **options)
28
+ elsif base_element.checkbox?
29
+ Chromate::Elements::Checkbox.new(selector, @client, **options)
30
+ else
31
+ base_element
32
+ end
33
+ end
34
+
35
+ # @param selector [String] CSS selector
36
+ # @return [String]
34
37
  def evaluate_script(script)
35
- @client.send_message('Runtime.evaluate', expression: script)
38
+ result = @client.send_message('Runtime.evaluate', expression: script, returnByValue: true)
39
+
40
+ result['result']['value']
41
+ rescue StandardError => e
42
+ Chromate::CLogger.log("Error evaluating script: #{e.message}", :error)
43
+ nil
36
44
  end
37
45
  end
38
46
  end
@@ -17,22 +17,25 @@ module Chromate
17
17
  dom_content_loaded = false
18
18
  frame_stopped_loading = false
19
19
 
20
- # Utiliser un Mutex pour la synchronisation
20
+ # Use Mutex for synchronization
21
21
  mutex = Mutex.new
22
22
  condition = ConditionVariable.new
23
23
 
24
- # S'abonner aux messages WebSocket
24
+ # Subscribe to websocket messages
25
25
  listener = proc do |message|
26
26
  mutex.synchronize do
27
27
  case message['method']
28
28
  when 'Page.domContentEventFired'
29
29
  dom_content_loaded = true
30
+ Chromate::CLogger.log('DOMContentEventFired')
30
31
  condition.signal if dom_content_loaded && page_loaded && frame_stopped_loading
31
32
  when 'Page.loadEventFired'
32
33
  page_loaded = true
34
+ Chromate::CLogger.log('LoadEventFired')
33
35
  condition.signal if dom_content_loaded && page_loaded && frame_stopped_loading
34
36
  when 'Page.frameStoppedLoading'
35
37
  frame_stopped_loading = true
38
+ Chromate::CLogger.log('FrameStoppedLoading')
36
39
  condition.signal if dom_content_loaded && page_loaded && frame_stopped_loading
37
40
  end
38
41
  end
@@ -40,15 +43,14 @@ module Chromate
40
43
 
41
44
  @client.on_message(&listener)
42
45
 
43
- # Attendre les trois événements (DOMContent, Load et FrameStoppedLoading) avec un timeout
46
+ # Wait for all three events (DOMContent, Load and FrameStoppedLoading) with a timeout
44
47
  Timeout.timeout(15) do
45
48
  mutex.synchronize do
46
49
  condition.wait(mutex) until dom_content_loaded && page_loaded && frame_stopped_loading
47
50
  end
48
51
  end
49
52
 
50
- # Nettoyer l'écouteur WebSocket
51
- @client.on_message { |msg| } # Supprime tous les anciens écouteurs en ajoutant un listener vide
53
+ @client.on_message { |msg| } # Remove listener
52
54
 
53
55
  self
54
56
  end
@@ -3,15 +3,47 @@
3
3
  module Chromate
4
4
  module Actions
5
5
  module Screenshot
6
- def screenshot_to_file(file_path, options = {})
7
- image_data = screenshot(options)
6
+ # @param file_path [String] The path to save the screenshot to
7
+ # @param options [Hash] Options for the screenshot
8
+ # @option options [String] :format The format of the screenshot (default: 'png')
9
+ # @option options [Boolean] :full_page Whether to take a screenshot of the full page
10
+ # @option options [Boolean] :fromSurface Whether to take a screenshot from the surface
11
+ # @return [Hash] A hash containing the path and base64-encoded image data of the screenshot
12
+ def screenshot(file_path = "#{Time.now.to_i}.png", options = {})
13
+ file_path ||= "#{Time.now.to_i}.png"
14
+ return xvfb_screenshot(file_path) if @xfvb
15
+
16
+ if options[:full_page]
17
+ original_viewport = fetch_viewport_size
18
+ update_screen_size_to_full_page!
19
+ end
20
+
21
+ image_data = make_screenshot(options)
22
+ reset_screen_size! if options[:full_page]
23
+
8
24
  File.binwrite(file_path, image_data)
9
- true
25
+
26
+ {
27
+ path: file_path,
28
+ base64: Base64.encode64(image_data)
29
+ }
30
+ ensure
31
+ restore_viewport_size(original_viewport) if options[:full_page]
10
32
  end
11
33
 
12
- def screenshot_full_page(file_path, options = {})
13
- metrics = @client.send_message('Page.getLayoutMetrics')
34
+ private
14
35
 
36
+ # @param file_path [String] The path to save the screenshot to
37
+ # @return [Boolean] Whether the screenshot was successful
38
+ def xvfb_screenshot(file_path)
39
+ display = ENV['DISPLAY'] || ':99'
40
+ system("xwd -root -display #{display} | convert xwd:- #{file_path}")
41
+ end
42
+
43
+ # Updates the screen size to match the full page dimensions
44
+ # @return [void]
45
+ def update_screen_size_to_full_page!
46
+ metrics = @client.send_message('Page.getLayoutMetrics')
15
47
  content_size = metrics['contentSize']
16
48
  width = content_size['width'].ceil
17
49
  height = content_size['height'].ceil
@@ -22,29 +54,54 @@ module Chromate
22
54
  height: height,
23
55
  deviceScaleFactor: 1
24
56
  })
57
+ end
25
58
 
26
- screenshot_to_file(file_path, options)
27
-
59
+ # Resets the device metrics override
60
+ # @return [void]
61
+ def reset_screen_size!
28
62
  @client.send_message('Emulation.clearDeviceMetricsOverride')
29
- true
30
63
  end
31
64
 
32
- def xvfb_screenshot(file_path)
33
- display = ENV['DISPLAY'] || ':99'
34
- system("xwd -root -display #{display} | convert xwd:- #{file_path}")
65
+ # Fetches the current viewport size
66
+ # @return [Hash] The current viewport dimensions
67
+ def fetch_viewport_size
68
+ metrics = @client.send_message('Page.getLayoutMetrics')
69
+ {
70
+ width: metrics['layoutViewport']['clientWidth'],
71
+ height: metrics['layoutViewport']['clientHeight']
72
+ }
35
73
  end
36
74
 
37
- private
75
+ # Restores the viewport size to its original dimensions
76
+ # @param viewport [Hash] The original viewport dimensions
77
+ # @return [void]
78
+ def restore_viewport_size(viewport)
79
+ return unless viewport
80
+
81
+ @client.send_message('Emulation.setDeviceMetricsOverride', {
82
+ mobile: false,
83
+ width: viewport[:width],
84
+ height: viewport[:height],
85
+ deviceScaleFactor: 1
86
+ })
87
+ end
38
88
 
39
- def screenshot(options = {})
89
+ # @param options [Hash] Options for the screenshot
90
+ # @option options [String] :format The format of the screenshot
91
+ # @option options [Boolean] :fromSurface Whether to take a screenshot from the surface
92
+ # @return [String] The image data
93
+ def make_screenshot(options = {})
40
94
  default_options = {
41
95
  format: 'png',
42
- fromSurface: true
96
+ fromSurface: true,
97
+ captureBeyondViewport: true
43
98
  }
44
99
 
45
100
  params = default_options.merge(options)
46
101
 
47
102
  @client.send_message('Page.enable')
103
+ @client.send_message('DOM.enable')
104
+ @client.send_message('DOM.getDocument', depth: -1, pierce: true)
48
105
 
49
106
  result = @client.send_message('Page.captureScreenshot', params)
50
107
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'user_agent_parser'
4
+
5
+ module Chromate
6
+ module Actions
7
+ module Stealth
8
+ # @return [void]
9
+ def patch
10
+ @client.send_message('Network.enable')
11
+ inject_stealth_script
12
+
13
+ # TODO: Improve dynamic user agent overriding
14
+ # It currently breaks fingerprint validation (pixcelscan.com)
15
+ # override_user_agent(@user_agent)
16
+ end
17
+
18
+ # @return [void]
19
+ def inject_stealth_script
20
+ stealth_script = File.read(File.join(__dir__, '../files/stealth.js'))
21
+ @client.send_message('Page.addScriptToEvaluateOnNewDocument', { source: stealth_script })
22
+ end
23
+
24
+ # @param user_agent [String]
25
+ # @return [void]
26
+ def override_user_agent(user_agent) # rubocop:disable Metrics/MethodLength
27
+ u_agent = UserAgentParser.parse(user_agent)
28
+ platform = Chromate::UserAgent.os
29
+ version = u_agent.version
30
+ brands = [
31
+ { brand: u_agent.family || 'Not_A_Brand', version: version.major },
32
+ { brand: u_agent.device.brand || 'Not_A_Brand', version: u_agent.os.version.to_s }
33
+ ]
34
+
35
+ custom_headers = {
36
+ 'User-Agent' => user_agent,
37
+ 'Accept-Language' => 'en-US,en;q=0.9',
38
+ 'Sec-CH-UA' => brands.map { |brand| "\"#{brand[:brand]}\";v=\"#{brand[:version]}\"" }.join(', '),
39
+ 'Sec-CH-UA-Platform' => "\"#{u_agent.device.family}\"",
40
+ 'Sec-CH-UA-Mobile' => '?0'
41
+ }
42
+ @client.send_message('Network.setExtraHTTPHeaders', headers: custom_headers)
43
+
44
+ user_agent_override = {
45
+ userAgent: user_agent,
46
+ platform: platform,
47
+ acceptLanguage: 'en-US,en;q=0.9',
48
+ userAgentMetadata: {
49
+ brands: brands,
50
+ fullVersion: version.to_s,
51
+ platform: platform,
52
+ platformVersion: u_agent.os.version.to_s,
53
+ architecture: Chromate::UserAgent.arch,
54
+ model: '',
55
+ mobile: false
56
+ }
57
+ }
58
+ @client.send_message('Network.setUserAgentOverride', user_agent_override)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Chromate
6
+ class Binary
7
+ def self.run(path, args, need_success: true)
8
+ command = [path] + args
9
+ stdout, stderr, status = Open3.capture3(*command)
10
+ raise stderr if need_success && !status.success?
11
+
12
+ stdout
13
+ end
14
+
15
+ attr_reader :pid
16
+
17
+ # @param [String] path
18
+ # @param [Array<String>] args
19
+ def initialize(path, args)
20
+ @path = path
21
+ @args = args || []
22
+ @pid = nil
23
+ end
24
+
25
+ # @return [self]
26
+ def start
27
+ command = [@path] + @args
28
+ _stdin, _stdout, _stderr, wait_thr = Open3.popen3(*command)
29
+ CLogger.log("Started process with pid #{wait_thr.pid}", level: :debug)
30
+ Process.detach(wait_thr.pid)
31
+ CLogger.log("Process detached with pid #{wait_thr.pid}", level: :debug)
32
+ @pid = wait_thr.pid
33
+
34
+ self
35
+ end
36
+
37
+ # @return [Boolean]
38
+ def started?
39
+ !@pid.nil?
40
+ end
41
+
42
+ def running?
43
+ return false unless started?
44
+
45
+ Process.getpgid(@pid).is_a?(Integer)
46
+ rescue Errno::ESRCH
47
+ false
48
+ end
49
+
50
+ # @return [self]
51
+ def stop
52
+ stop_process
53
+ end
54
+
55
+ # @return [Boolean]
56
+ def stopped?
57
+ @pid.nil?
58
+ end
59
+
60
+ private
61
+
62
+ def stop_process(timeout: 5)
63
+ return unless pid
64
+
65
+ # Send SIGINT to the process to stop it gracefully
66
+ Process.kill('INT', pid)
67
+ begin
68
+ Timeout.timeout(timeout) do
69
+ Process.wait(pid)
70
+ end
71
+ rescue Timeout::Error
72
+ # If the process does not stop gracefully, send SIGKILL
73
+ CLogger.log("Process #{pid} did not stop gracefully. Sending SIGKILL...", level: :debug)
74
+ Process.kill('KILL', pid)
75
+ Process.wait(pid)
76
+ end
77
+ rescue Errno::ESRCH
78
+ # The process has already stopped
79
+ ensure
80
+ @pid = nil
81
+ end
82
+ end
83
+ end
@@ -6,14 +6,20 @@ require 'securerandom'
6
6
  require 'net/http'
7
7
  require 'websocket-client-simple'
8
8
  require_relative 'helpers'
9
+ require_relative 'binary'
9
10
  require_relative 'client'
10
- require_relative 'element'
11
11
  require_relative 'hardwares'
12
+ require_relative 'element'
12
13
  require_relative 'elements/select'
14
+ require_relative 'elements/option'
15
+ require_relative 'elements/tags'
16
+ require_relative 'elements/radio'
17
+ require_relative 'elements/checkbox'
13
18
  require_relative 'user_agent'
14
19
  require_relative 'actions/navigate'
15
20
  require_relative 'actions/screenshot'
16
21
  require_relative 'actions/dom'
22
+ require_relative 'actions/stealth'
17
23
 
18
24
  module Chromate
19
25
  class Browser
@@ -23,7 +29,15 @@ module Chromate
23
29
  include Actions::Navigate
24
30
  include Actions::Screenshot
25
31
  include Actions::Dom
26
-
32
+ include Actions::Stealth
33
+
34
+ # @param options [Hash] Options for the browser
35
+ # @option options [String] :chrome_path The path to the Chrome executable
36
+ # @option options [String] :user_data_dir The path to the user data directory
37
+ # @option options [Boolean] :headless Whether to run Chrome in headless mode
38
+ # @option options [Boolean] :xfvb Whether to run Chrome in Xvfb
39
+ # @option options [Boolean] :native_control Whether to use native controls
40
+ # @option options [Boolean] :record Whether to record the screen
27
41
  def initialize(options = {})
28
42
  @options = config.options.merge(options)
29
43
  @chrome_path = @options.fetch(:chrome_path)
@@ -32,29 +46,21 @@ module Chromate
32
46
  @xfvb = @options.fetch(:xfvb)
33
47
  @native_control = @options.fetch(:native_control)
34
48
  @record = @options.fetch(:record, false)
35
- @process = nil
36
- @xfvb_process = nil
49
+ @binary = nil
37
50
  @record_process = nil
38
51
  @client = nil
39
- @args = [
40
- @chrome_path,
41
- "--user-data-dir=#{@user_data_dir}",
42
- "--lang=#{@options[:lang] || "fr-FR"}"
43
- ]
52
+ @args = []
44
53
 
45
54
  trap('INT') { stop_and_exit }
46
55
  trap('TERM') { stop_and_exit }
47
-
48
- at_exit { stop }
49
56
  end
50
57
 
58
+ # @return [self]
51
59
  def start
52
60
  build_args
53
61
  @client = Client.new(self)
54
62
  @args << "--remote-debugging-port=#{@client.port}"
55
63
 
56
- start_video_recording if @record
57
-
58
64
  if @xfvb
59
65
  if ENV['DISPLAY'].nil?
60
66
  ENV['DISPLAY'] = ':0' if mac? # XQuartz generally uses :0 on Mac
@@ -63,49 +69,139 @@ module Chromate
63
69
  @args << "--display=#{ENV.fetch("DISPLAY", nil)}"
64
70
  end
65
71
 
66
- @process = spawn(*@args, err: 'chrome_errors.log', out: 'chrome_output.log')
67
- sleep 2
72
+ Hardwares::MouseController.reset_mouse_position
73
+ Chromate::CLogger.log("Starting browser with args: #{@args}", level: :debug)
74
+ @binary = Binary.new(@chrome_path, @args)
68
75
 
76
+ @binary.start
69
77
  @client.start
78
+
79
+ start_video_recording if @record
80
+
81
+ patch if config.patch?
82
+
83
+ update_config!
84
+
70
85
  self
71
86
  end
72
87
 
88
+ # @return [Boolean]
89
+ def started?
90
+ @binary&.started? || false
91
+ end
92
+
93
+ # @return [self]
73
94
  def stop
74
- Process.kill('TERM', @process) if @process
75
- Process.kill('TERM', @record_process) if @record_process
76
- Process.kill('TERM', @xfvb_process) if @xfvb_process
95
+ stop_process(@record_process) if @record_process
96
+ @binary.stop if started?
77
97
  @client&.stop
98
+
99
+ @binary = nil
100
+ @record_process = nil
101
+
102
+ self
78
103
  end
79
104
 
105
+ # @return [Boolean]
80
106
  def native_control?
81
107
  @native_control
82
108
  end
83
109
 
84
110
  private
85
111
 
112
+ # @return [Integer]
86
113
  def start_video_recording
87
- outfile = File.join(Dir.pwd, "output_video_#{Time.now.to_i}.mp4")
88
- @record_process = spawn("ffmpeg -f x11grab -r 25 -s 1920x1080 -i #{ENV.fetch("DISPLAY", ":99")} -pix_fmt yuv420p -y #{outfile}")
114
+ outname = @record.is_a?(String) ? @record : "output_video_#{Time.now.to_i}.mp4"
115
+ outfile = File.join(Dir.pwd, outname).to_s
116
+ args = [
117
+ '-f',
118
+ 'x11grab',
119
+ '-draw_mouse',
120
+ '1',
121
+ '-r',
122
+ '30',
123
+ '-s',
124
+ '1920x1080',
125
+ '-i',
126
+ ENV.fetch('DISPLAY'),
127
+ '-c:v',
128
+ 'libx264',
129
+ '-preset',
130
+ 'ultrafast',
131
+ '-pix_fmt',
132
+ 'yuv420p',
133
+ '-y',
134
+ outfile
135
+ ]
136
+ binary = Binary.new('ffmpeg', args)
137
+ binary.start
138
+ @record_process = binary.pid
89
139
  end
90
140
 
141
+ # @return [Array<String>]
91
142
  def build_args
92
143
  exclude_switches = config.exclude_switches || []
93
144
  exclude_switches += @options[:exclude_switches] if @options[:exclude_switches]
145
+ @user_agent = @options[:user_agent] || UserAgent.call
94
146
 
95
- @args += config.generate_arguments(**@options)
96
- @args += @options[:options][:args] if @options.dig(:options, :args)
97
- @args << "--user-agent=#{@options[:user_agent] || UserAgent.call}"
147
+ @args = if @options.dig(:options, :args)
148
+ @options[:options][:args]
149
+ else
150
+ config.generate_arguments(**@options)
151
+ end
152
+
153
+ @args << "--user-agent=#{@user_agent}"
98
154
  @args << "--exclude-switches=#{exclude_switches.join(",")}" if exclude_switches.any?
155
+ @args << "--user-data-dir=#{@user_data_dir}"
99
156
 
100
157
  @args
101
158
  end
102
159
 
160
+ def set_hardwares
161
+ config.mouse_controller = Hardwares.mouse(client: @client, element: nil)
162
+ config.keyboard_controller = Hardwares.keyboard(client: @client, element: nil)
163
+ end
164
+
165
+ # @return [void]
166
+ def update_config!
167
+ config.args = @args
168
+ config.user_data_dir = @user_data_dir
169
+ config.headless = @headless
170
+ config.xfvb = @xfvb
171
+ config.native_control = @native_control
172
+
173
+ set_hardwares
174
+ end
175
+
176
+ # @param pid [Integer] PID of the process to stop
177
+ # @param timeout [Integer] Timeout in seconds to wait for the process to stop
178
+ # @return [void]
179
+ def stop_process(pid, timeout: 5)
180
+ return unless pid
181
+
182
+ # Send SIGINT to the process to stop it gracefully
183
+ Process.kill('INT', pid)
184
+ begin
185
+ Timeout.timeout(timeout) do
186
+ Process.wait(pid)
187
+ end
188
+ rescue Timeout::Error
189
+ # If the process does not stop gracefully, send SIGKILL
190
+ CLogger.log("Process #{pid} did not stop gracefully. Sending SIGKILL...", level: :debug)
191
+ Process.kill('KILL', pid)
192
+ end
193
+ rescue Errno::ESRCH
194
+ # The process has already stopped
195
+ end
196
+
197
+ # @return [void]
103
198
  def stop_and_exit
104
- puts 'Stopping browser...'
199
+ CLogger.log('Stopping browser...', level: :debug)
105
200
  stop
106
201
  exit
107
202
  end
108
203
 
204
+ # @return [Chromate::Configuration]
109
205
  def config
110
206
  Chromate.configuration
111
207
  end
@@ -4,17 +4,25 @@ require 'logger'
4
4
 
5
5
  module Chromate
6
6
  class CLogger < Logger
7
+ # @param [IO] logdev
8
+ # @param [Integer] shift_age
9
+ # @param [Integer] shift_size
7
10
  def initialize(logdev, shift_age: 0, shift_size: 1_048_576)
8
11
  super(logdev, shift_age, shift_size)
9
12
  self.formatter = proc do |severity, datetime, _progname, msg|
10
13
  "[Chromate] #{datetime.strftime("%Y-%m-%d %H:%M:%S")} #{severity}: #{msg}\n"
11
14
  end
15
+ self.level = ENV['CHROMATE_DEBUG'] ? :debug : :info
12
16
  end
13
17
 
18
+ # @return [Chromate::CLogger]
14
19
  def self.logger
15
20
  @logger ||= new($stdout)
16
21
  end
17
22
 
23
+ # @param [String] message
24
+ # @param [Symbol] level
25
+ # @return [void]
18
26
  def self.log(message, level: :info)
19
27
  logger.send(level, message)
20
28
  end