chromate-rb 0.0.1.pre → 0.0.2.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +54 -3
- data/README.md +33 -6
- data/Rakefile +48 -16
- data/docker_root/Gemfile +4 -0
- data/docker_root/Gemfile.lock +28 -0
- data/docker_root/TestInDocker.gif +0 -0
- data/docker_root/app.rb +92 -0
- data/dockerfiles/Dockerfile +21 -7
- data/dockerfiles/README.md +49 -0
- data/docs/README.md +74 -0
- data/docs/browser.md +149 -92
- data/docs/element.md +289 -0
- data/lib/bot_browser/downloader.rb +52 -0
- data/lib/bot_browser/installer.rb +81 -0
- data/lib/bot_browser.rb +39 -0
- data/lib/chromate/actions/dom.rb +28 -9
- data/lib/chromate/actions/navigate.rb +4 -5
- data/lib/chromate/actions/screenshot.rb +30 -11
- data/lib/chromate/actions/stealth.rb +47 -0
- data/lib/chromate/browser.rb +64 -12
- data/lib/chromate/c_logger.rb +7 -0
- data/lib/chromate/client.rb +40 -18
- data/lib/chromate/configuration.rb +31 -14
- data/lib/chromate/element.rb +65 -15
- data/lib/chromate/elements/select.rb +59 -7
- data/lib/chromate/hardwares/keyboard_controller.rb +34 -0
- data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
- data/lib/chromate/hardwares/mouse_controller.rb +47 -11
- data/lib/chromate/hardwares/mouses/linux_controller.rb +124 -21
- data/lib/chromate/hardwares/mouses/mac_os_controller.rb +6 -6
- data/lib/chromate/hardwares/mouses/virtual_controller.rb +95 -7
- data/lib/chromate/hardwares/mouses/x11.rb +36 -0
- data/lib/chromate/hardwares.rb +16 -0
- data/lib/chromate/helpers.rb +22 -15
- data/lib/chromate/user_agent.rb +39 -15
- data/lib/chromate/version.rb +1 -1
- data/lib/chromate.rb +2 -0
- data/logo.png +0 -0
- data/results/bot.png +0 -0
- data/results/brotector.png +0 -0
- data/results/cloudflare.png +0 -0
- data/results/headers.png +0 -0
- data/results/pixelscan.png +0 -0
- metadata +20 -2
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chromate/helpers'
|
4
|
+
require 'chromate/c_logger'
|
5
|
+
require 'bot_browser/downloader'
|
6
|
+
|
7
|
+
module BotBrowser
|
8
|
+
class Installer
|
9
|
+
class << self
|
10
|
+
include Chromate::Helpers
|
11
|
+
|
12
|
+
def install(version = nil)
|
13
|
+
create_config_dir
|
14
|
+
binary_path, profile_path = Downloader.download(version)
|
15
|
+
bot_browser_path = install_binary(binary_path)
|
16
|
+
bot_browser_profile_path = install_profile(profile_path)
|
17
|
+
|
18
|
+
write_config(bot_browser_path, bot_browser_profile_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def config_dir
|
22
|
+
"#{Dir.home}/.botbrowser"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def install_binary(binary_path)
|
28
|
+
Chromate::CLogger.log("Installing binary from #{binary_path}")
|
29
|
+
return install_binary_mac(binary_path) if mac?
|
30
|
+
return install_binary_linux(binary_path) if linux?
|
31
|
+
return install_binary_windows(binary_path) if windows?
|
32
|
+
|
33
|
+
raise 'Unsupported platform'
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_config_dir
|
37
|
+
Chromate::CLogger.log("Creating config directory at #{config_dir}")
|
38
|
+
`mkdir -p #{config_dir}`
|
39
|
+
end
|
40
|
+
|
41
|
+
def install_profile(profile_path)
|
42
|
+
Chromate::CLogger.log("Installing profile from #{profile_path}")
|
43
|
+
`cp #{profile_path} #{config_dir}/`
|
44
|
+
|
45
|
+
"#{config_dir}/#{File.basename(profile_path)}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def install_binary_mac(binary_path)
|
49
|
+
`hdiutil attach #{binary_path}`
|
50
|
+
`cp -r /Volumes/Chromium/Chromium.app /Applications/`
|
51
|
+
`hdiutil detach /Volumes/Chromium`
|
52
|
+
`xattr -rd com.apple.quarantine /Applications/Chromium.app`
|
53
|
+
`codesign --force --deep --sign - /Applications/Chromium.app`
|
54
|
+
|
55
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
56
|
+
end
|
57
|
+
|
58
|
+
def install_binary_linux(binary_path)
|
59
|
+
`sudo dpkg -i #{binary_path}`
|
60
|
+
`sudo apt-get install -f`
|
61
|
+
|
62
|
+
'/usr/bin/chromium'
|
63
|
+
end
|
64
|
+
|
65
|
+
def install_binary_windows(binary_path)
|
66
|
+
`7z x #{binary_path}`
|
67
|
+
|
68
|
+
'chromium.exe'
|
69
|
+
end
|
70
|
+
|
71
|
+
def write_config(bot_browser_path, bot_browser_profile_path)
|
72
|
+
Chromate::CLogger.log("Writing config to #{config_dir}/config.yml")
|
73
|
+
File.write(File.expand_path("#{config_dir}/config.yml"), <<~YAML)
|
74
|
+
---
|
75
|
+
bot_browser_path: #{bot_browser_path}
|
76
|
+
profile: #{bot_browser_profile_path}
|
77
|
+
YAML
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/bot_browser.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'chromate/c_logger'
|
5
|
+
require 'bot_browser/installer'
|
6
|
+
|
7
|
+
module BotBrowser
|
8
|
+
class << self
|
9
|
+
def install(version = nil)
|
10
|
+
Installer.install(version)
|
11
|
+
end
|
12
|
+
|
13
|
+
def installed?
|
14
|
+
File.exist?("#{Dir.home}/.botbrowser/config.yml")
|
15
|
+
end
|
16
|
+
|
17
|
+
def load
|
18
|
+
yaml = YAML.load_file("#{Dir.home}/.botbrowser/config.yml")
|
19
|
+
|
20
|
+
Chromate.configure do |config|
|
21
|
+
ENV['CHROME_BIN'] = yaml['bot_browser_path']
|
22
|
+
config.args = [
|
23
|
+
"--bot-profile=#{yaml["profile"]}",
|
24
|
+
'--no-sandbox'
|
25
|
+
]
|
26
|
+
config.startup_patch = false
|
27
|
+
end
|
28
|
+
|
29
|
+
Chromate::CLogger.log('BotBrowser loaded', level: :debug)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Usage
|
35
|
+
# require 'bot_browser'
|
36
|
+
|
37
|
+
# BotBrowser.install
|
38
|
+
# BotBrowser.load
|
39
|
+
# browser = Chromate::Browser.new
|
data/lib/chromate/actions/dom.rb
CHANGED
@@ -3,36 +3,55 @@
|
|
3
3
|
module Chromate
|
4
4
|
module Actions
|
5
5
|
module Dom
|
6
|
+
# @return [String]
|
7
|
+
def source
|
8
|
+
evaluate_script('document.documentElement.outerHTML')
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param selector [String] CSS selector
|
12
|
+
# @return [Chromate::Element]
|
6
13
|
def find_element(selector)
|
7
14
|
Chromate::Element.new(selector, @client)
|
8
15
|
end
|
9
16
|
|
17
|
+
# @param selector [String] CSS selector
|
18
|
+
# @return [Chromate::Element]
|
10
19
|
def click_element(selector)
|
11
20
|
find_element(selector).click
|
12
21
|
end
|
13
22
|
|
23
|
+
# @param selector [String] CSS selector
|
24
|
+
# @return [Chromate::Element]
|
14
25
|
def hover_element(selector)
|
15
26
|
find_element(selector).hover
|
16
27
|
end
|
17
28
|
|
29
|
+
# @param selector [String] CSS selector
|
30
|
+
# @param text [String] Text to type
|
31
|
+
# @return [Chromate::Element]
|
18
32
|
def type_text(selector, text)
|
19
33
|
find_element(selector).type(text)
|
20
34
|
end
|
21
35
|
|
22
|
-
|
23
|
-
|
24
|
-
end
|
25
|
-
|
26
|
-
def get_property(selector, property)
|
27
|
-
find_element(selector).attributes[property]
|
28
|
-
end
|
29
|
-
|
36
|
+
# @param selector [String] CSS selector
|
37
|
+
# @return [String]
|
30
38
|
def select_option(selector, option)
|
31
39
|
Chromate::Elements::Select.new(selector, @client).select_option(option)
|
32
40
|
end
|
33
41
|
|
42
|
+
# @param selector [String] CSS selector
|
43
|
+
# @return [String]
|
34
44
|
def evaluate_script(script)
|
35
|
-
@client.send_message('Runtime.evaluate', expression: script)
|
45
|
+
result = @client.send_message('Runtime.evaluate', expression: script)
|
46
|
+
|
47
|
+
case result['result']['type']
|
48
|
+
when 'string', 'number', 'boolean'
|
49
|
+
result['result']['value']
|
50
|
+
when 'object'
|
51
|
+
result['result']['objectId']
|
52
|
+
else
|
53
|
+
result['result']
|
54
|
+
end
|
36
55
|
end
|
37
56
|
end
|
38
57
|
end
|
@@ -17,11 +17,11 @@ module Chromate
|
|
17
17
|
dom_content_loaded = false
|
18
18
|
frame_stopped_loading = false
|
19
19
|
|
20
|
-
#
|
20
|
+
# Use Mutex for synchronization
|
21
21
|
mutex = Mutex.new
|
22
22
|
condition = ConditionVariable.new
|
23
23
|
|
24
|
-
#
|
24
|
+
# Subscribe to websocket messages
|
25
25
|
listener = proc do |message|
|
26
26
|
mutex.synchronize do
|
27
27
|
case message['method']
|
@@ -40,15 +40,14 @@ module Chromate
|
|
40
40
|
|
41
41
|
@client.on_message(&listener)
|
42
42
|
|
43
|
-
#
|
43
|
+
# Wait for all three events (DOMContent, Load and FrameStoppedLoading) with a timeout
|
44
44
|
Timeout.timeout(15) do
|
45
45
|
mutex.synchronize do
|
46
46
|
condition.wait(mutex) until dom_content_loaded && page_loaded && frame_stopped_loading
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
#
|
51
|
-
@client.on_message { |msg| } # Supprime tous les anciens écouteurs en ajoutant un listener vide
|
50
|
+
@client.on_message { |msg| } # Remove listener
|
52
51
|
|
53
52
|
self
|
54
53
|
end
|
@@ -3,12 +3,34 @@
|
|
3
3
|
module Chromate
|
4
4
|
module Actions
|
5
5
|
module Screenshot
|
6
|
-
|
7
|
-
|
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
|
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 [Boolean] Whether the screenshot was successful
|
12
|
+
def screenshot(file_path = "#{Time.now.to_i}.png", options = {})
|
13
|
+
return xvfb_screenshot(file_path) if @xfvb
|
14
|
+
return screenshot_full_page(file_path, options) if options.delete(:full_page)
|
15
|
+
|
16
|
+
image_data = make_screenshot(options)
|
8
17
|
File.binwrite(file_path, image_data)
|
9
18
|
true
|
10
19
|
end
|
11
20
|
|
21
|
+
private
|
22
|
+
|
23
|
+
# @param file_path [String] The path to save the screenshot to
|
24
|
+
# @return [Boolean] Whether the screenshot was successful
|
25
|
+
def xvfb_screenshot(file_path)
|
26
|
+
display = ENV['DISPLAY'] || ':99'
|
27
|
+
system("xwd -root -display #{display} | convert xwd:- #{file_path}")
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param file_path [String] The path to save the screenshot to
|
31
|
+
# @param options [Hash] Options for the screenshot
|
32
|
+
# @option options [String] :format The format of the screenshot
|
33
|
+
# @return [Boolean] Whether the screenshot was successful
|
12
34
|
def screenshot_full_page(file_path, options = {})
|
13
35
|
metrics = @client.send_message('Page.getLayoutMetrics')
|
14
36
|
|
@@ -23,20 +45,17 @@ module Chromate
|
|
23
45
|
deviceScaleFactor: 1
|
24
46
|
})
|
25
47
|
|
26
|
-
|
48
|
+
screenshot(file_path, options)
|
27
49
|
|
28
50
|
@client.send_message('Emulation.clearDeviceMetricsOverride')
|
29
51
|
true
|
30
52
|
end
|
31
53
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def screenshot(options = {})
|
54
|
+
# @param options [Hash] Options for the screenshot
|
55
|
+
# @option options [String] :format The format of the screenshot
|
56
|
+
# @option options [Boolean] :fromSurface Whether to take a screenshot from the surface
|
57
|
+
# @return [String] The image data
|
58
|
+
def make_screenshot(options = {})
|
40
59
|
default_options = {
|
41
60
|
format: 'png',
|
42
61
|
fromSurface: true
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chromate
|
4
|
+
module Actions
|
5
|
+
module Stealth
|
6
|
+
# @return [void]
|
7
|
+
def patch # rubocop:disable Metrics/MethodLength
|
8
|
+
@client.send_message('Network.enable')
|
9
|
+
|
10
|
+
# Define custom headers
|
11
|
+
custom_headers = {
|
12
|
+
'User-Agent' => UserAgent.call,
|
13
|
+
'Accept-Language' => 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,und;q=0.6,es;q=0.5,pt;q=0.4',
|
14
|
+
'Sec-CH-UA' => '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
15
|
+
'Sec-CH-UA-Platform' => '"' + UserAgent.os + '"', # rubocop:disable Style/StringConcatenation
|
16
|
+
'Sec-CH-UA-Mobile' => '?0'
|
17
|
+
}
|
18
|
+
|
19
|
+
# Apply custom headers
|
20
|
+
@client.send_message('Network.setExtraHTTPHeaders', headers: custom_headers)
|
21
|
+
|
22
|
+
# Override User-Agent and high-entropy data to avoid fingerprinting
|
23
|
+
user_agent_override = {
|
24
|
+
userAgent: UserAgent.call,
|
25
|
+
platform: UserAgent.os,
|
26
|
+
acceptLanguage: 'fr-FR,fr;q=0.9,en-US;q=0.8',
|
27
|
+
userAgentMetadata: {
|
28
|
+
brands: [
|
29
|
+
{ brand: 'Google Chrome', version: '131' },
|
30
|
+
{ brand: 'Chromium', version: '131' },
|
31
|
+
{ brand: 'Not_A Brand', version: '24' }
|
32
|
+
],
|
33
|
+
fullVersion: '131.0.0.0',
|
34
|
+
platform: UserAgent.os,
|
35
|
+
platformVersion: UserAgent.os_version,
|
36
|
+
architecture: 'x86_64',
|
37
|
+
model: '',
|
38
|
+
mobile: false
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
# Apply User-Agent override and high-entropy data
|
43
|
+
@client.send_message('Network.setUserAgentOverride', user_agent_override)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/chromate/browser.rb
CHANGED
@@ -14,6 +14,7 @@ require_relative 'user_agent'
|
|
14
14
|
require_relative 'actions/navigate'
|
15
15
|
require_relative 'actions/screenshot'
|
16
16
|
require_relative 'actions/dom'
|
17
|
+
require_relative 'actions/stealth'
|
17
18
|
|
18
19
|
module Chromate
|
19
20
|
class Browser
|
@@ -23,7 +24,15 @@ module Chromate
|
|
23
24
|
include Actions::Navigate
|
24
25
|
include Actions::Screenshot
|
25
26
|
include Actions::Dom
|
26
|
-
|
27
|
+
include Actions::Stealth
|
28
|
+
|
29
|
+
# @param options [Hash] Options for the browser
|
30
|
+
# @option options [String] :chrome_path The path to the Chrome executable
|
31
|
+
# @option options [String] :user_data_dir The path to the user data directory
|
32
|
+
# @option options [Boolean] :headless Whether to run Chrome in headless mode
|
33
|
+
# @option options [Boolean] :xfvb Whether to run Chrome in Xvfb
|
34
|
+
# @option options [Boolean] :native_control Whether to use native controls
|
35
|
+
# @option options [Boolean] :record Whether to record the screen
|
27
36
|
def initialize(options = {})
|
28
37
|
@options = config.options.merge(options)
|
29
38
|
@chrome_path = @options.fetch(:chrome_path)
|
@@ -38,8 +47,7 @@ module Chromate
|
|
38
47
|
@client = nil
|
39
48
|
@args = [
|
40
49
|
@chrome_path,
|
41
|
-
"--user-data-dir=#{@user_data_dir}"
|
42
|
-
"--lang=#{@options[:lang] || "fr-FR"}"
|
50
|
+
"--user-data-dir=#{@user_data_dir}"
|
43
51
|
]
|
44
52
|
|
45
53
|
trap('INT') { stop_and_exit }
|
@@ -48,13 +56,12 @@ module Chromate
|
|
48
56
|
at_exit { stop }
|
49
57
|
end
|
50
58
|
|
59
|
+
# @return [self]
|
51
60
|
def start
|
52
61
|
build_args
|
53
62
|
@client = Client.new(self)
|
54
63
|
@args << "--remote-debugging-port=#{@client.port}"
|
55
64
|
|
56
|
-
start_video_recording if @record
|
57
|
-
|
58
65
|
if @xfvb
|
59
66
|
if ENV['DISPLAY'].nil?
|
60
67
|
ENV['DISPLAY'] = ':0' if mac? # XQuartz generally uses :0 on Mac
|
@@ -63,49 +70,94 @@ module Chromate
|
|
63
70
|
@args << "--display=#{ENV.fetch("DISPLAY", nil)}"
|
64
71
|
end
|
65
72
|
|
73
|
+
Hardwares::MouseController.reset_mouse_position
|
74
|
+
Chromate::CLogger.log("Starting browser with args: #{@args}", level: :debug)
|
66
75
|
@process = spawn(*@args, err: 'chrome_errors.log', out: 'chrome_output.log')
|
67
76
|
sleep 2
|
68
77
|
|
69
78
|
@client.start
|
79
|
+
|
80
|
+
start_video_recording if @record
|
81
|
+
|
82
|
+
patch if config.patch?
|
83
|
+
|
70
84
|
self
|
71
85
|
end
|
72
86
|
|
87
|
+
# @return [self]
|
73
88
|
def stop
|
74
|
-
|
75
|
-
|
76
|
-
|
89
|
+
stop_process(@process) if @process
|
90
|
+
stop_process(@record_process) if @record_process
|
91
|
+
stop_process(@xfvb_process) if @xfvb_process
|
77
92
|
@client&.stop
|
93
|
+
|
94
|
+
self
|
78
95
|
end
|
79
96
|
|
97
|
+
# @return [Boolean]
|
80
98
|
def native_control?
|
81
99
|
@native_control
|
82
100
|
end
|
83
101
|
|
84
102
|
private
|
85
103
|
|
104
|
+
# @return [Integer]
|
86
105
|
def start_video_recording
|
87
|
-
|
88
|
-
|
106
|
+
outname = @record.is_a?(String) ? @record : "output_video_#{Time.now.to_i}.mp4"
|
107
|
+
outfile = File.join(Dir.pwd, outname)
|
108
|
+
# TODO: get screen resolution dynamically
|
109
|
+
@record_process = spawn(
|
110
|
+
"ffmpeg -f x11grab -draw_mouse 1 -r 30 -s 1920x1080 -i #{ENV.fetch("DISPLAY")} -c:v libx264 -preset ultrafast -pix_fmt yuv420p -y #{outfile}"
|
111
|
+
)
|
89
112
|
end
|
90
113
|
|
114
|
+
# @return [Array<String>]
|
91
115
|
def build_args
|
92
116
|
exclude_switches = config.exclude_switches || []
|
93
117
|
exclude_switches += @options[:exclude_switches] if @options[:exclude_switches]
|
94
118
|
|
119
|
+
if @options.dig(:options, :args)
|
120
|
+
@args += @options[:options][:args]
|
121
|
+
@args << "--exclude-switches=#{exclude_switches.join(",")}" if exclude_switches.any?
|
122
|
+
return @args
|
123
|
+
end
|
95
124
|
@args += config.generate_arguments(**@options)
|
96
|
-
@args += @options[:options][:args] if @options.dig(:options, :args)
|
97
125
|
@args << "--user-agent=#{@options[:user_agent] || UserAgent.call}"
|
98
126
|
@args << "--exclude-switches=#{exclude_switches.join(",")}" if exclude_switches.any?
|
99
127
|
|
100
128
|
@args
|
101
129
|
end
|
102
130
|
|
131
|
+
# @param pid [Integer] PID of the process to stop
|
132
|
+
# @param timeout [Integer] Timeout in seconds to wait for the process to stop
|
133
|
+
# @return [void]
|
134
|
+
def stop_process(pid, timeout: 5)
|
135
|
+
return unless pid
|
136
|
+
|
137
|
+
# Send SIGINT to the process to stop it gracefully
|
138
|
+
Process.kill('INT', pid)
|
139
|
+
begin
|
140
|
+
Timeout.timeout(timeout) do
|
141
|
+
Process.wait(pid)
|
142
|
+
end
|
143
|
+
rescue Timeout::Error
|
144
|
+
# If the process does not stop gracefully, send SIGKILL
|
145
|
+
CLogger.log("Process #{pid} did not stop gracefully. Sending SIGKILL...", level: :debug)
|
146
|
+
Process.kill('KILL', pid)
|
147
|
+
Process.wait(pid)
|
148
|
+
end
|
149
|
+
rescue Errno::ESRCH
|
150
|
+
# The process has already stopped
|
151
|
+
end
|
152
|
+
|
153
|
+
# @return [void]
|
103
154
|
def stop_and_exit
|
104
|
-
|
155
|
+
CLogger.log('Stopping browser...', level: :debug)
|
105
156
|
stop
|
106
157
|
exit
|
107
158
|
end
|
108
159
|
|
160
|
+
# @return [Chromate::Configuration]
|
109
161
|
def config
|
110
162
|
Chromate.configuration
|
111
163
|
end
|
data/lib/chromate/c_logger.rb
CHANGED
@@ -4,6 +4,9 @@ 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|
|
@@ -11,10 +14,14 @@ module Chromate
|
|
11
14
|
end
|
12
15
|
end
|
13
16
|
|
17
|
+
# @return [Chromate::CLogger]
|
14
18
|
def self.logger
|
15
19
|
@logger ||= new($stdout)
|
16
20
|
end
|
17
21
|
|
22
|
+
# @param [String] message
|
23
|
+
# @param [Symbol] level
|
24
|
+
# @return [void]
|
18
25
|
def self.log(message, level: :info)
|
19
26
|
logger.send(level, message)
|
20
27
|
end
|
data/lib/chromate/client.rb
CHANGED
@@ -7,18 +7,21 @@ module Chromate
|
|
7
7
|
class Client
|
8
8
|
include Helpers
|
9
9
|
|
10
|
+
# @return [Array<Proc>]
|
10
11
|
def self.listeners
|
11
12
|
@@listeners ||= [] # rubocop:disable Style/ClassVars
|
12
13
|
end
|
13
14
|
|
14
15
|
attr_reader :port, :ws, :browser
|
15
16
|
|
17
|
+
# @param [Chromate::Browser] browser
|
16
18
|
def initialize(browser)
|
17
|
-
@browser
|
18
|
-
options
|
19
|
-
@port
|
19
|
+
@browser = browser
|
20
|
+
options = browser.options
|
21
|
+
@port = options[:port] || find_available_port
|
20
22
|
end
|
21
23
|
|
24
|
+
# @return [self]
|
22
25
|
def start
|
23
26
|
@ws_url = fetch_websocket_debug_url
|
24
27
|
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
@@ -29,7 +32,7 @@ module Chromate
|
|
29
32
|
|
30
33
|
@ws.on :message do |msg|
|
31
34
|
message = JSON.parse(msg.data)
|
32
|
-
client_self.handle_message
|
35
|
+
client_self.send(:handle_message, message)
|
33
36
|
|
34
37
|
Client.listeners.each do |listener|
|
35
38
|
listener.call(message)
|
@@ -37,63 +40,82 @@ module Chromate
|
|
37
40
|
end
|
38
41
|
|
39
42
|
@ws.on :open do
|
40
|
-
|
43
|
+
Chromate::CLogger.log('Successfully connected to WebSocket', level: :debug)
|
41
44
|
end
|
42
45
|
|
43
46
|
@ws.on :error do |e|
|
44
|
-
|
47
|
+
Chromate::CLogger.log("WebSocket error: #{e.message}", level: :error)
|
45
48
|
end
|
46
49
|
|
47
50
|
@ws.on :close do |_e|
|
48
|
-
|
51
|
+
Chromate::CLogger.log('WebSocket connection closed', level: :debug)
|
49
52
|
end
|
50
53
|
|
51
|
-
sleep 0.2
|
54
|
+
sleep 0.2 # Wait for the connection to be established
|
52
55
|
client_self.send_message('Target.setDiscoverTargets', { discover: true })
|
56
|
+
|
53
57
|
client_self
|
54
58
|
end
|
55
59
|
|
60
|
+
# @return [self]
|
56
61
|
def stop
|
57
62
|
@ws&.close
|
63
|
+
|
64
|
+
self
|
58
65
|
end
|
59
66
|
|
67
|
+
# @param [String] method
|
68
|
+
# @param [Hash] params
|
69
|
+
# @return [Hash]
|
60
70
|
def send_message(method, params = {})
|
61
71
|
@id += 1
|
62
72
|
message = { id: @id, method: method, params: params }
|
63
|
-
|
73
|
+
Chromate::CLogger.log("Sending WebSocket message: #{message}", level: :debug)
|
64
74
|
|
65
75
|
begin
|
66
76
|
@ws.send(message.to_json)
|
67
77
|
@callbacks[@id] = Queue.new
|
68
78
|
result = @callbacks[@id].pop
|
69
|
-
|
79
|
+
Chromate::CLogger.log("Response received for message #{message[:id]}: #{result}", level: :debug)
|
70
80
|
result
|
71
81
|
rescue StandardError => e
|
72
|
-
|
82
|
+
Chromate::CLogger.log("Error sending WebSocket message: #{e.message}", level: :error)
|
73
83
|
reconnect
|
74
84
|
retry
|
75
85
|
end
|
76
86
|
end
|
77
87
|
|
88
|
+
# @return [self]
|
78
89
|
def reconnect
|
79
90
|
@ws_url = fetch_websocket_debug_url
|
80
91
|
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
81
|
-
|
92
|
+
Chromate::CLogger.log('Successfully reconnected to WebSocket')
|
93
|
+
|
94
|
+
self
|
82
95
|
end
|
83
96
|
|
97
|
+
# Allowing different parts to subscribe to WebSocket messages
|
98
|
+
# @yieldparam [Hash] message
|
99
|
+
# @return [void]
|
100
|
+
def on_message(&block)
|
101
|
+
Client.listeners << block
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# @param [Hash] message
|
107
|
+
# @return [self]
|
84
108
|
def handle_message(message)
|
85
|
-
|
109
|
+
Chromate::CLogger.log("Message received: #{message}", level: :debug)
|
86
110
|
return unless message['id'] && @callbacks[message['id']]
|
87
111
|
|
88
112
|
@callbacks[message['id']].push(message['result'])
|
89
113
|
@callbacks.delete(message['id'])
|
90
|
-
end
|
91
114
|
|
92
|
-
|
93
|
-
def on_message(&block)
|
94
|
-
Client.listeners << block
|
115
|
+
self
|
95
116
|
end
|
96
117
|
|
118
|
+
# @return [String]
|
97
119
|
def fetch_websocket_debug_url
|
98
120
|
uri = URI("http://localhost:#{@port}/json/list")
|
99
121
|
response = Net::HTTP.get(uri)
|
@@ -108,8 +130,8 @@ module Chromate
|
|
108
130
|
end
|
109
131
|
end
|
110
132
|
|
133
|
+
# @return [String]
|
111
134
|
def create_new_page_target
|
112
|
-
# Créer une nouvelle page
|
113
135
|
uri = URI("http://localhost:#{@port}/json/new")
|
114
136
|
response = Net::HTTP.get(uri)
|
115
137
|
new_target = JSON.parse(response)
|