chromate-rb 0.0.2.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 +1 -0
- data/CHANGELOG.md +20 -2
- data/Rakefile +2 -2
- data/docker_root/app.rb +6 -11
- data/docs/BOT_BROWSER.md +74 -0
- data/docs/browser.md +20 -55
- data/docs/client.md +126 -0
- data/docs/element.md +77 -1
- data/docs/elements/checkbox.md +69 -0
- data/docs/elements/radio.md +57 -0
- data/lib/bot_browser/downloader.rb +38 -26
- data/lib/bot_browser/installer.rb +27 -9
- data/lib/bot_browser.rb +5 -1
- data/lib/chromate/actions/dom.rb +24 -35
- data/lib/chromate/actions/navigate.rb +3 -0
- data/lib/chromate/actions/screenshot.rb +52 -14
- data/lib/chromate/actions/stealth.rb +38 -23
- data/lib/chromate/binary.rb +83 -0
- data/lib/chromate/browser.rb +70 -26
- data/lib/chromate/c_logger.rb +1 -0
- data/lib/chromate/client.rb +25 -8
- data/lib/chromate/configuration.rb +2 -2
- data/lib/chromate/element.rb +62 -9
- 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 +10 -18
- 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 +11 -0
- data/lib/chromate/hardwares/mouse_controller.rb +8 -0
- data/lib/chromate/hardwares.rb +4 -4
- data/lib/chromate/user_agent.rb +14 -12
- data/lib/chromate/version.rb +1 -1
- data/results/bot.png +0 -0
- data/results/brotector.png +0 -0
- data/results/cloudflare.png +0 -0
- data/results/pixelscan.png +0 -0
- metadata +27 -2
@@ -1,49 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Special thanks to the BotBrowser project (https://github.com/MiddleSchoolStudent/BotBrowser)
|
4
|
+
# for providing an amazing foundation for browser automation and making this work possible.
|
5
|
+
|
6
|
+
require 'chromate/binary'
|
7
|
+
|
3
8
|
module BotBrowser
|
4
9
|
class Downloader
|
5
10
|
class << self
|
6
|
-
def download(version = nil)
|
11
|
+
def download(version = nil, profile = nil, platform = :mac)
|
7
12
|
version ||= versions.keys.first
|
13
|
+
profile ||= profiles[version].keys.first
|
8
14
|
version = version.to_sym
|
9
|
-
|
10
|
-
|
11
|
-
binary_path = download_file(versions[version][platform][arch][:binary], "/tmp/botbrowser_#{version}_#{platform}_#{arch}.dmg")
|
12
|
-
profile_path = download_file(versions[version][platform][arch][:profile], "/tmp/botbrowser_#{version}_#{platform}_#{arch}.json")
|
15
|
+
binary_path = download_file(versions[version][platform], "/tmp/botbrowser_#{version}_#{platform}.dmg")
|
16
|
+
profile_path = download_file(profiles[version][profile], "/tmp/botbrowser_#{version}_#{platform}.json")
|
13
17
|
|
14
18
|
[binary_path, profile_path]
|
15
19
|
end
|
16
20
|
|
17
21
|
def download_file(url, path)
|
18
22
|
Chromate::CLogger.log("Downloading #{url} to #{path}")
|
19
|
-
|
23
|
+
Chromate::Binary.run('curl', ['-L', url, '-o', path])
|
20
24
|
|
21
25
|
path
|
22
26
|
end
|
23
27
|
|
24
28
|
def versions
|
25
29
|
{
|
30
|
+
v132: {
|
31
|
+
mac: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/20250204/botbrowser_132.0.6834.84_mac_arm64.dmg',
|
32
|
+
linux: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/20250204/botbrowser_132.0.6834.84_amd64.deb',
|
33
|
+
windows: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/20250204/botbrowser_132.0.6834.84_win_x86_64.7z'
|
34
|
+
},
|
35
|
+
v130: {
|
36
|
+
mac: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/v130/botbrowser_130.0.6723.92_mac_arm64.dmg',
|
37
|
+
linux: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/v130/botbrowser_130.0.6723.117_amd64.deb',
|
38
|
+
windows: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/v130/botbrowser_130.0.6723.117_win_x86_64.7z'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def profiles
|
44
|
+
{
|
45
|
+
v128: {
|
46
|
+
mac: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v128/chrome128_mac_arm64.enc',
|
47
|
+
win: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v128/chrome128_win10_x86_64.enc'
|
48
|
+
},
|
49
|
+
v129: {
|
50
|
+
mac: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v129/chrome129_mac_arm64.enc'
|
51
|
+
},
|
26
52
|
v130: {
|
27
|
-
mac:
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
linux: {
|
34
|
-
x64: {
|
35
|
-
binary: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/v130/botbrowser_130.0.6723.117_amd64.deb',
|
36
|
-
# no specifi linux profile for moment
|
37
|
-
profile: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v130/chrome130-macarm.enc'
|
38
|
-
}
|
39
|
-
},
|
40
|
-
windows: {
|
41
|
-
x64: {
|
42
|
-
binary: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/v130/botbrowser_130.0.6723.117_win_x86_64.7z',
|
43
|
-
# no specific windows profile for moment
|
44
|
-
profile: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v130/chrome130-macarm.enc'
|
45
|
-
}
|
46
|
-
}
|
53
|
+
mac: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v130/chrome130_mac_arm64.enc',
|
54
|
+
iphone: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v130/chrome130_iphone.enc'
|
55
|
+
},
|
56
|
+
v132: {
|
57
|
+
mac: 'https://github.com/MiddleSchoolStudent/BotBrowser/blob/main/profiles/v132/chrome132_mac_arm64.enc',
|
58
|
+
win: 'https://github.com/MiddleSchoolStudent/BotBrowser/blob/main/profiles/v132/chrome132_win10_x86_64.enc'
|
47
59
|
}
|
48
60
|
}
|
49
61
|
end
|
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'yaml'
|
3
4
|
require 'chromate/helpers'
|
4
5
|
require 'chromate/c_logger'
|
5
6
|
require 'bot_browser/downloader'
|
6
7
|
|
7
8
|
module BotBrowser
|
8
9
|
class Installer
|
10
|
+
class NotInstalledError < StandardError; end
|
9
11
|
class << self
|
10
12
|
include Chromate::Helpers
|
11
13
|
|
@@ -22,6 +24,22 @@ module BotBrowser
|
|
22
24
|
"#{Dir.home}/.botbrowser"
|
23
25
|
end
|
24
26
|
|
27
|
+
def installed?
|
28
|
+
File.exist?("#{config_dir}/config.yml")
|
29
|
+
end
|
30
|
+
|
31
|
+
def uninstall
|
32
|
+
raise NotInstalledError, 'BotBrowser is not installed' unless installed?
|
33
|
+
|
34
|
+
config = YAML.load_file("#{config_dir}/config.yml")
|
35
|
+
Chromate::CLogger.log("Uninstalling binary at #{config["bot_browser_path"]}")
|
36
|
+
FileUtils.rm_rf(config['bot_browser_path'])
|
37
|
+
Chromate::CLogger.log("Uninstalling profile at #{config["profile"]}")
|
38
|
+
FileUtils.rm_rf(config['profile'])
|
39
|
+
FileUtils.rm_rf(config_dir)
|
40
|
+
Chromate::CLogger.log('Uninstalled')
|
41
|
+
end
|
42
|
+
|
25
43
|
private
|
26
44
|
|
27
45
|
def install_binary(binary_path)
|
@@ -35,7 +53,7 @@ module BotBrowser
|
|
35
53
|
|
36
54
|
def create_config_dir
|
37
55
|
Chromate::CLogger.log("Creating config directory at #{config_dir}")
|
38
|
-
|
56
|
+
FileUtils.mkdir_p(config_dir)
|
39
57
|
end
|
40
58
|
|
41
59
|
def install_profile(profile_path)
|
@@ -46,24 +64,24 @@ module BotBrowser
|
|
46
64
|
end
|
47
65
|
|
48
66
|
def install_binary_mac(binary_path)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
67
|
+
Chromate::Binary.run('hdiutil', ['attach', binary_path])
|
68
|
+
Chromate::Binary.run('cp', ['-r', '/Volumes/Chromium/Chromium.app', '/Applications/'])
|
69
|
+
Chromate::Binary.run('hdiutil', ['detach', '/Volumes/Chromium'])
|
70
|
+
Chromate::Binary.run('xattr', ['-rd', 'com.apple.quarantine', '/Applications/Chromium.app'])
|
71
|
+
Chromate::Binary.run('codesign', ['--force', '--deep', '--sign', '-', '/Applications/Chromium.app'], need_success: false)
|
54
72
|
|
55
73
|
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
56
74
|
end
|
57
75
|
|
58
76
|
def install_binary_linux(binary_path)
|
59
|
-
|
60
|
-
|
77
|
+
Chromate::Binary.run('sudo', ['dpkg', '-i', binary_path])
|
78
|
+
Chromate::Binary.run('sudo', ['apt-get', 'install', '-f'])
|
61
79
|
|
62
80
|
'/usr/bin/chromium'
|
63
81
|
end
|
64
82
|
|
65
83
|
def install_binary_windows(binary_path)
|
66
|
-
|
84
|
+
Chromate::Binary.run('7z', ['x', binary_path])
|
67
85
|
|
68
86
|
'chromium.exe'
|
69
87
|
end
|
data/lib/bot_browser.rb
CHANGED
data/lib/chromate/actions/dom.rb
CHANGED
@@ -11,47 +11,36 @@ module Chromate
|
|
11
11
|
# @param selector [String] CSS selector
|
12
12
|
# @return [Chromate::Element]
|
13
13
|
def find_element(selector)
|
14
|
-
Chromate::Element.new(selector, @client)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
find_element(selector).type(text)
|
34
|
-
end
|
35
|
-
|
36
|
-
# @param selector [String] CSS selector
|
37
|
-
# @return [String]
|
38
|
-
def select_option(selector, option)
|
39
|
-
Chromate::Elements::Select.new(selector, @client).select_option(option)
|
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
|
40
33
|
end
|
41
34
|
|
42
35
|
# @param selector [String] CSS selector
|
43
36
|
# @return [String]
|
44
37
|
def evaluate_script(script)
|
45
|
-
result = @client.send_message('Runtime.evaluate', expression: script)
|
38
|
+
result = @client.send_message('Runtime.evaluate', expression: script, returnByValue: true)
|
46
39
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
result['result']['objectId']
|
52
|
-
else
|
53
|
-
result['result']
|
54
|
-
end
|
40
|
+
result['result']['value']
|
41
|
+
rescue StandardError => e
|
42
|
+
Chromate::CLogger.log("Error evaluating script: #{e.message}", :error)
|
43
|
+
nil
|
55
44
|
end
|
56
45
|
end
|
57
46
|
end
|
@@ -27,12 +27,15 @@ module Chromate
|
|
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
|
@@ -5,17 +5,30 @@ module Chromate
|
|
5
5
|
module Screenshot
|
6
6
|
# @param file_path [String] The path to save the screenshot to
|
7
7
|
# @param options [Hash] Options for the screenshot
|
8
|
-
# @option options [String] :format The format of the screenshot
|
8
|
+
# @option options [String] :format The format of the screenshot (default: 'png')
|
9
9
|
# @option options [Boolean] :full_page Whether to take a screenshot of the full page
|
10
10
|
# @option options [Boolean] :fromSurface Whether to take a screenshot from the surface
|
11
|
-
# @return [
|
11
|
+
# @return [Hash] A hash containing the path and base64-encoded image data of the screenshot
|
12
12
|
def screenshot(file_path = "#{Time.now.to_i}.png", options = {})
|
13
|
+
file_path ||= "#{Time.now.to_i}.png"
|
13
14
|
return xvfb_screenshot(file_path) if @xfvb
|
14
|
-
|
15
|
+
|
16
|
+
if options[:full_page]
|
17
|
+
original_viewport = fetch_viewport_size
|
18
|
+
update_screen_size_to_full_page!
|
19
|
+
end
|
15
20
|
|
16
21
|
image_data = make_screenshot(options)
|
22
|
+
reset_screen_size! if options[:full_page]
|
23
|
+
|
17
24
|
File.binwrite(file_path, image_data)
|
18
|
-
|
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]
|
19
32
|
end
|
20
33
|
|
21
34
|
private
|
@@ -27,13 +40,10 @@ module Chromate
|
|
27
40
|
system("xwd -root -display #{display} | convert xwd:- #{file_path}")
|
28
41
|
end
|
29
42
|
|
30
|
-
#
|
31
|
-
# @
|
32
|
-
|
33
|
-
# @return [Boolean] Whether the screenshot was successful
|
34
|
-
def screenshot_full_page(file_path, options = {})
|
43
|
+
# Updates the screen size to match the full page dimensions
|
44
|
+
# @return [void]
|
45
|
+
def update_screen_size_to_full_page!
|
35
46
|
metrics = @client.send_message('Page.getLayoutMetrics')
|
36
|
-
|
37
47
|
content_size = metrics['contentSize']
|
38
48
|
width = content_size['width'].ceil
|
39
49
|
height = content_size['height'].ceil
|
@@ -44,11 +54,36 @@ module Chromate
|
|
44
54
|
height: height,
|
45
55
|
deviceScaleFactor: 1
|
46
56
|
})
|
57
|
+
end
|
47
58
|
|
48
|
-
|
49
|
-
|
59
|
+
# Resets the device metrics override
|
60
|
+
# @return [void]
|
61
|
+
def reset_screen_size!
|
50
62
|
@client.send_message('Emulation.clearDeviceMetricsOverride')
|
51
|
-
|
63
|
+
end
|
64
|
+
|
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
|
+
}
|
73
|
+
end
|
74
|
+
|
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
|
+
})
|
52
87
|
end
|
53
88
|
|
54
89
|
# @param options [Hash] Options for the screenshot
|
@@ -58,12 +93,15 @@ module Chromate
|
|
58
93
|
def make_screenshot(options = {})
|
59
94
|
default_options = {
|
60
95
|
format: 'png',
|
61
|
-
fromSurface: true
|
96
|
+
fromSurface: true,
|
97
|
+
captureBeyondViewport: true
|
62
98
|
}
|
63
99
|
|
64
100
|
params = default_options.merge(options)
|
65
101
|
|
66
102
|
@client.send_message('Page.enable')
|
103
|
+
@client.send_message('DOM.enable')
|
104
|
+
@client.send_message('DOM.getDocument', depth: -1, pierce: true)
|
67
105
|
|
68
106
|
result = @client.send_message('Page.captureScreenshot', params)
|
69
107
|
|
@@ -1,45 +1,60 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'user_agent_parser'
|
4
|
+
|
3
5
|
module Chromate
|
4
6
|
module Actions
|
5
7
|
module Stealth
|
6
8
|
# @return [void]
|
7
|
-
def patch
|
9
|
+
def patch
|
8
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
|
+
]
|
9
34
|
|
10
|
-
# Define custom headers
|
11
35
|
custom_headers = {
|
12
|
-
'User-Agent' =>
|
13
|
-
'Accept-Language' => '
|
14
|
-
'Sec-CH-UA' =>
|
15
|
-
'Sec-CH-UA-Platform' =>
|
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}\"",
|
16
40
|
'Sec-CH-UA-Mobile' => '?0'
|
17
41
|
}
|
18
|
-
|
19
|
-
# Apply custom headers
|
20
42
|
@client.send_message('Network.setExtraHTTPHeaders', headers: custom_headers)
|
21
43
|
|
22
|
-
# Override User-Agent and high-entropy data to avoid fingerprinting
|
23
44
|
user_agent_override = {
|
24
|
-
userAgent:
|
25
|
-
platform:
|
26
|
-
acceptLanguage: '
|
45
|
+
userAgent: user_agent,
|
46
|
+
platform: platform,
|
47
|
+
acceptLanguage: 'en-US,en;q=0.9',
|
27
48
|
userAgentMetadata: {
|
28
|
-
brands:
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
fullVersion: '131.0.0.0',
|
34
|
-
platform: UserAgent.os,
|
35
|
-
platformVersion: UserAgent.os_version,
|
36
|
-
architecture: 'x86_64',
|
49
|
+
brands: brands,
|
50
|
+
fullVersion: version.to_s,
|
51
|
+
platform: platform,
|
52
|
+
platformVersion: u_agent.os.version.to_s,
|
53
|
+
architecture: Chromate::UserAgent.arch,
|
37
54
|
model: '',
|
38
55
|
mobile: false
|
39
56
|
}
|
40
57
|
}
|
41
|
-
|
42
|
-
# Apply User-Agent override and high-entropy data
|
43
58
|
@client.send_message('Network.setUserAgentOverride', user_agent_override)
|
44
59
|
end
|
45
60
|
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
|