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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +20 -2
  4. data/Rakefile +2 -2
  5. data/docker_root/app.rb +6 -11
  6. data/docs/BOT_BROWSER.md +74 -0
  7. data/docs/browser.md +20 -55
  8. data/docs/client.md +126 -0
  9. data/docs/element.md +77 -1
  10. data/docs/elements/checkbox.md +69 -0
  11. data/docs/elements/radio.md +57 -0
  12. data/lib/bot_browser/downloader.rb +38 -26
  13. data/lib/bot_browser/installer.rb +27 -9
  14. data/lib/bot_browser.rb +5 -1
  15. data/lib/chromate/actions/dom.rb +24 -35
  16. data/lib/chromate/actions/navigate.rb +3 -0
  17. data/lib/chromate/actions/screenshot.rb +52 -14
  18. data/lib/chromate/actions/stealth.rb +38 -23
  19. data/lib/chromate/binary.rb +83 -0
  20. data/lib/chromate/browser.rb +70 -26
  21. data/lib/chromate/c_logger.rb +1 -0
  22. data/lib/chromate/client.rb +25 -8
  23. data/lib/chromate/configuration.rb +2 -2
  24. data/lib/chromate/element.rb +62 -9
  25. data/lib/chromate/elements/checkbox.rb +40 -0
  26. data/lib/chromate/elements/option.rb +43 -0
  27. data/lib/chromate/elements/radio.rb +37 -0
  28. data/lib/chromate/elements/select.rb +10 -18
  29. data/lib/chromate/elements/tags.rb +29 -0
  30. data/lib/chromate/exceptions.rb +2 -0
  31. data/lib/chromate/files/agents.json +11 -0
  32. data/lib/chromate/files/stealth.js +199 -0
  33. data/lib/chromate/hardwares/keyboard_controller.rb +11 -0
  34. data/lib/chromate/hardwares/mouse_controller.rb +8 -0
  35. data/lib/chromate/hardwares.rb +4 -4
  36. data/lib/chromate/user_agent.rb +14 -12
  37. data/lib/chromate/version.rb +1 -1
  38. data/results/bot.png +0 -0
  39. data/results/brotector.png +0 -0
  40. data/results/cloudflare.png +0 -0
  41. data/results/pixelscan.png +0 -0
  42. 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
- platform = :mac
10
- arch = :arm64
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
- `curl -L #{url} >> #{path}`
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
- arm64: {
29
- binary: 'https://github.com/MiddleSchoolStudent/BotBrowser/releases/download/v130/botbrowser_130.0.6723.92_mac_arm64.dmg',
30
- profile: 'https://raw.githubusercontent.com/MiddleSchoolStudent/BotBrowser/refs/heads/main/profiles/v130/chrome130-macarm.enc'
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
- `mkdir -p #{config_dir}`
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
- `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`
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
- `sudo dpkg -i #{binary_path}`
60
- `sudo apt-get install -f`
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
- `7z x #{binary_path}`
84
+ Chromate::Binary.run('7z', ['x', binary_path])
67
85
 
68
86
  'chromium.exe'
69
87
  end
data/lib/bot_browser.rb CHANGED
@@ -10,8 +10,12 @@ module BotBrowser
10
10
  Installer.install(version)
11
11
  end
12
12
 
13
+ def uninstall
14
+ Installer.uninstall
15
+ end
16
+
13
17
  def installed?
14
- File.exist?("#{Dir.home}/.botbrowser/config.yml")
18
+ Installer.installed?
15
19
  end
16
20
 
17
21
  def load
@@ -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
- end
16
-
17
- # @param selector [String] CSS selector
18
- # @return [Chromate::Element]
19
- def click_element(selector)
20
- find_element(selector).click
21
- end
22
-
23
- # @param selector [String] CSS selector
24
- # @return [Chromate::Element]
25
- def hover_element(selector)
26
- find_element(selector).hover
27
- end
28
-
29
- # @param selector [String] CSS selector
30
- # @param text [String] Text to type
31
- # @return [Chromate::Element]
32
- def type_text(selector, text)
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
- 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
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 [Boolean] Whether the screenshot was successful
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
- return screenshot_full_page(file_path, options) if options.delete(:full_page)
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
- 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]
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
- # @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
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
- screenshot(file_path, options)
49
-
59
+ # Resets the device metrics override
60
+ # @return [void]
61
+ def reset_screen_size!
50
62
  @client.send_message('Emulation.clearDeviceMetricsOverride')
51
- true
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 # rubocop:disable Metrics/MethodLength
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' => 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
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: UserAgent.call,
25
- platform: UserAgent.os,
26
- acceptLanguage: 'fr-FR,fr;q=0.9,en-US;q=0.8',
45
+ userAgent: user_agent,
46
+ platform: platform,
47
+ acceptLanguage: 'en-US,en;q=0.9',
27
48
  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',
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