File: //usr/lib/ruby/vendor_ruby/selenium/webdriver/common/service.rb
# frozen_string_literal: true
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
module Selenium
  module WebDriver
    #
    # Base class implementing default behavior of service object,
    # responsible for starting and stopping driver implementations.
    #
    class Service
      START_TIMEOUT = 20
      SOCKET_LOCK_TIMEOUT = 45
      STOP_TIMEOUT = 20
      @default_port = nil
      @driver_path = nil
      @executable = nil
      @missing_text = nil
      class << self
        attr_reader :default_port, :driver_path, :executable, :missing_text, :shutdown_supported
        def chrome(**opts)
          Chrome::Service.new(**opts)
        end
        def firefox(**opts)
          binary_path = Firefox::Binary.path
          args = opts.delete(:args)
          case args
          when Hash
            args[:binary] ||= binary_path
            opts[:args] = args
          when Array
            opts[:args] = ["--binary=#{binary_path}"]
            opts[:args] += args
          else
            opts[:args] = ["--binary=#{binary_path}"]
          end
          Firefox::Service.new(**opts)
        end
        def ie(**opts)
          IE::Service.new(**opts)
        end
        alias_method :internet_explorer, :ie
        def edge(**opts)
          Edge::Service.new(**opts)
        end
        def safari(**opts)
          Safari::Service.new(**opts)
        end
        def driver_path=(path)
          Platform.assert_executable path if path.is_a?(String)
          @driver_path = path
        end
      end
      attr_accessor :host
      attr_reader :executable_path
      #
      # End users should use a class method for the desired driver, rather than using this directly.
      #
      # @api private
      #
      def initialize(path: nil, port: nil, args: nil)
        path ||= self.class.driver_path
        port ||= self.class.default_port
        args ||= []
        @executable_path = binary_path(path)
        @host = Platform.localhost
        @port = Integer(port)
        @extra_args = args.is_a?(Hash) ? extract_service_args(args) : args
        raise Error::WebDriverError, "invalid port: #{@port}" if @port < 1
      end
      def start
        raise "already started: #{uri.inspect} #{@executable_path.inspect}" if process_running?
        Platform.exit_hook(&method(:stop)) # make sure we don't leave the server running
        socket_lock.locked do
          find_free_port
          start_process
          connect_until_stable
        end
      end
      def stop
        return unless self.class.shutdown_supported
        stop_server
        @process.poll_for_exit STOP_TIMEOUT
      rescue ChildProcess::TimeoutError
        nil # noop
      ensure
        stop_process
      end
      def uri
        @uri ||= URI.parse("http://#{@host}:#{@port}")
      end
      private
      def binary_path(path = nil)
        path = path.call if path.is_a?(Proc)
        path ||= Platform.find_binary(self.class.executable)
        raise Error::WebDriverError, self.class.missing_text unless path
        Platform.assert_executable path
        path
      end
      def build_process(*command)
        WebDriver.logger.debug("Executing Process #{command}")
        @process = ChildProcess.build(*command)
        if WebDriver.logger.debug?
          @process.io.stdout = @process.io.stderr = WebDriver.logger.io
        elsif Platform.jruby?
          # Apparently we need to read the output of drivers on JRuby.
          @process.io.stdout = @process.io.stderr = File.new(Platform.null_device, 'w')
        end
        @process
      end
      def connect_to_server
        Net::HTTP.start(@host, @port) do |http|
          http.open_timeout = STOP_TIMEOUT / 2
          http.read_timeout = STOP_TIMEOUT / 2
          yield http
        end
      end
      def find_free_port
        @port = PortProber.above(@port)
      end
      def start_process
        @process = build_process(@executable_path, "--port=#{@port}", *@extra_args)
        # Note: this is a bug only in Windows 7
        @process.leader = true unless Platform.windows?
        @process.start
      end
      def stop_process
        return if process_exited?
        @process.stop STOP_TIMEOUT
        @process.io.stdout.close if Platform.jruby? && !WebDriver.logger.debug?
      end
      def stop_server
        return if process_exited?
        connect_to_server { |http| http.get('/shutdown') }
      end
      def process_running?
        defined?(@process) && @process&.alive?
      end
      def process_exited?
        @process.nil? || @process.exited?
      end
      def connect_until_stable
        socket_poller = SocketPoller.new @host, @port, START_TIMEOUT
        return if socket_poller.connected?
        raise Error::WebDriverError, cannot_connect_error_text
      end
      def cannot_connect_error_text
        "unable to connect to #{self.class.executable} #{@host}:#{@port}"
      end
      def socket_lock
        @socket_lock ||= SocketLock.new(@port - 1, SOCKET_LOCK_TIMEOUT)
      end
      protected
      def extract_service_args(driver_opts)
        driver_opts.key?(:args) ? driver_opts.delete(:args) : []
      end
    end # Service
  end # WebDriver
end # Selenium