chromate-rb 0.0.1.pre → 0.0.2.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.
- 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)
|