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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +72 -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 +87 -0
- data/dockerfiles/Dockerfile +21 -7
- data/dockerfiles/README.md +49 -0
- data/docs/BOT_BROWSER.md +74 -0
- data/docs/README.md +74 -0
- data/docs/browser.md +124 -102
- data/docs/client.md +126 -0
- data/docs/element.md +365 -0
- data/docs/elements/checkbox.md +69 -0
- data/docs/elements/radio.md +57 -0
- data/lib/bot_browser/downloader.rb +64 -0
- data/lib/bot_browser/installer.rb +99 -0
- data/lib/bot_browser.rb +43 -0
- data/lib/chromate/actions/dom.rb +35 -27
- data/lib/chromate/actions/navigate.rb +7 -5
- data/lib/chromate/actions/screenshot.rb +71 -14
- data/lib/chromate/actions/stealth.rb +62 -0
- data/lib/chromate/binary.rb +83 -0
- data/lib/chromate/browser.rb +120 -24
- data/lib/chromate/c_logger.rb +8 -0
- data/lib/chromate/client.rb +65 -26
- data/lib/chromate/configuration.rb +31 -14
- data/lib/chromate/element.rb +119 -16
- data/lib/chromate/elements/checkbox.rb +40 -0
- data/lib/chromate/elements/option.rb +43 -0
- data/lib/chromate/elements/radio.rb +37 -0
- data/lib/chromate/elements/select.rb +50 -6
- data/lib/chromate/elements/tags.rb +29 -0
- data/lib/chromate/exceptions.rb +2 -0
- data/lib/chromate/files/agents.json +11 -0
- data/lib/chromate/files/stealth.js +199 -0
- data/lib/chromate/hardwares/keyboard_controller.rb +45 -0
- data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
- data/lib/chromate/hardwares/mouse_controller.rb +55 -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 +19 -3
- data/lib/chromate/helpers.rb +22 -15
- data/lib/chromate/user_agent.rb +41 -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 +45 -2
data/lib/chromate/actions/dom.rb
CHANGED
@@ -3,36 +3,44 @@
|
|
3
3
|
module Chromate
|
4
4
|
module Actions
|
5
5
|
module Dom
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
#
|
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']
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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 (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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/chromate/browser.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
67
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
88
|
-
|
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
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
data/lib/chromate/c_logger.rb
CHANGED
@@ -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
|