module Browsed class Client attr_accessor :configuration attr_accessor :driver, :browser, :browser_id, :environment attr_accessor :session, :proxy attr_accessor :device, :user_agent, :resolution attr_accessor :manager, :maximum_processes include Capybara::DSL def initialize(configuration: ::Browsed.configuration, driver: :selenium_chrome, browser: :chrome, device: :desktop, proxy: nil, user_agent: nil, resolution: nil, environment: :production, options: {}, maximum_processes: nil) self.configuration = configuration self.driver = driver || self.configuration.driver self.browser = browser || self.configuration.browser self.environment = environment || self.configuration.environment self.browser_id = generate_browser_id self.device = device self.proxy = proxy self.manager = ::Browsed::Manager.new(browser: self.browser) self.maximum_processes = maximum_processes || self.configuration.maximum_processes set_user_agent(user_agent) set_resolution(resolution) options.merge!(browser_id: self.browser_id) setup_capybara(options: options) end include ::Browsed::Poltergeist include ::Browsed::Firefox include ::Browsed::Chrome def setup_capybara(options: {}, retries: 3) self.manager.kill_stale_processes! if can_start_new_process? register_driver!(options) Capybara.default_driver = self.driver Capybara.javascript_driver = self.driver Capybara.default_max_wait_time = options.fetch(:wait_time, 30) #seconds self.session = Capybara::Session.new(self.driver) else raise Browsed::TooManyProcessesError, "Too many #{self.browser} processes running, reached maximum allowed number of #{self.maximum_processes} processes." end end def can_start_new_process? self.maximum_processes.nil? || self.manager.can_start_more_processes?(max_count: self.maximum_processes) end def display_screenshot!(path) Launchy.open path if development? end # Resize the window separately and not based on initialization def resize!(res = nil) res ||= self.resolution if res && res.size.eql?(2) && !self.driver.eql?(:chrome) && !self.driver.eql?(:poltergeist) # Resolutions for Chrome & Poltergeist are set in the driver self.session.current_window.resize_to(res.first, res.last) # [width, height] end end def reset_session! self.session.reset_session! end def generate_browser_id SecureRandom.hex[0..15] end def quit!(retries: 3) begin self.session.driver.quit rescue Exception retries -= 1 retry if retries > 0 end # If Selenium/Phantom somehow isn't able to shut down the browser, force a shutdown using kill -9 self.manager.set_command(browser_id: self.browser_id) self.manager.kill_processes! self.session = nil end private def register_driver!(options = {}) if poltergeist? register_poltergeist_driver(options: options) elsif selenium? if firefox_browser? register_firefox_driver(options: options) elsif firefox_headless_browser? register_firefox_driver(options: options.merge(headless: true)) elsif chrome_browser? self.driver = :selenium_chrome register_chrome_driver(options: options) elsif chrome_headless_browser? self.driver = :selenium_chrome_headless register_chrome_driver(options: options.merge(headless: true)) end end end def poltergeist? self.driver.to_sym.eql?(:poltergeist) end def selenium? self.driver.to_sym.eql?(:selenium) end def firefox_browser? self.browser.to_sym.eql?(:firefox) end def firefox_headless_browser? self.browser.to_sym.eql?(:firefox_headless) end def chrome_browser? self.browser.to_sym.eql?(:chrome) end def chrome_headless_browser? self.browser.to_sym.eql?(:chrome_headless) end def development? in_environment?(:development) end def in_environment?(env) self.environment.eql?(env) end # User Agents def set_user_agent(user_agent) if !user_agent.to_s.empty? && !user_agent.to_sym.eql?(:randomize) self.user_agent = user_agent elsif (!user_agent.to_s.empty? && user_agent.to_sym.eql?(:randomize)) || poltergeist? case self.device when :iphone self.user_agent = Agents.random_user_agent(:phones, :iphone) when :android_phone self.user_agent = Agents.random_user_agent(:phones, :android) when :ipad self.user_agent = Agents.random_user_agent(:tablets, :ipad) when :android_tablet self.user_agent = Agents.random_user_agent(:tablets, :android) else self.user_agent = Agents.random_user_agent(self.device) end end end def runs_ios? Agents.runs_ios?(self.user_agent) end def is_iphone? Agents.is_iphone?(self.user_agent) end def is_ipad? Agents.is_ipad?(self.user_agent) end # Resolution def set_resolution(res) if res && res.is_a?(Array) self.resolution = res elsif res && res.is_a?(Symbol) && res.eql?(:randomize) self.resolution = randomize_resolution end end def randomize_resolution runs_ios? ? randomize_ios_resolution : Browsed::Constants::RESOLUTIONS.fetch(self.device, :desktop).sample end def randomize_ios_resolution resolution_device = case self.device when :iphone, :android_phone :phone when :ipad, :android_tablet :tablet else self.device end random_key = Browsed::Constants::RESOLUTIONS.fetch(resolution_device, :desktop).keys.sample resolution = Browsed::Constants::RESOLUTIONS.fetch(resolution_device, :desktop)[random_key] end def wait_for_ajax Timeout.timeout(Capybara.default_max_wait_time) do loop until finished_all_ajax_requests? end end def finished_all_ajax_requests? evald = self.session.evaluate_script('jQuery.active') evald.nil? || evald.zero? end def log(message) puts "[Browsed::Client] - #{Time.now}: #{message}" if self.configuration.verbose? end end end