<commit_before>require 'timeout'
require 'bosh/dev'
require 'securerandom'

module Bosh::Dev::Sandbox
  class Service
    attr_reader :description

    def initialize(cmd_array, cmd_options, logger)
      @cmd_array = cmd_array
      @cmd_options = cmd_options
      @logger = logger
      @pid = nil

      # Add unique identifier to avoid confusing log information
      @description = "#{@cmd_array.first} (#{SecureRandom.hex(4)})"
    end

    def start
      env = ENV.to_hash.merge(@cmd_options.fetch(:env, {}))

      if running?
        @logger.info("Already started #{@description} with PID #{@pid}")
      else
        unless system("which #{@cmd_array.first} > /dev/null")
          raise "Cannot find #{@description} in the $PATH"
        end

        @log_id = SecureRandom.hex(4)

        @pid = Process.spawn(env, *@cmd_array, {
          out: stdout || :close,
          err: stderr || :close,
          in: :close,
        })
        @logger.info("Started #{@description} with PID #{@pid}, log-id: #{@log_id}")

        Process.detach(@pid)

        tries = 0
        until running?
          tries += 1
          raise RuntimeError, "Cannot run #{@cmd_array} with #{env.inspect}" if tries > 20
          sleep(0.1)
        end
      end
    end

    def stop(signal = 'TERM')
      if running?
        kill_process(signal, @pid)

        # Block until process exits to avoid race conditions in the caller
        # (e.g. director process is killed but we don't wait and then we
        # try to delete db which is in use by director)
        wait_for_process_to_exit_or_be_killed
      end

      # Reset pid so that we do not think that service is still running
      # when pid is given to some other unrelated process
      @pid = nil
    end

    def running?
      @pid && Process.kill(0, @pid)
    rescue Errno::ESRCH # No such process
      false
    rescue Errno::EPERM # Owned by some other user/process
      @logger.info("Process other than #{@description} is running with PID=#{@pid} so this service is not running.")
      @logger.debug(`ps #{@pid}`)
      false
    end

    def stdout_contents
      stdout ? File.read(stdout) : ''
    end

    def stderr_contents
      stderr ? File.read(stderr) : ''
    end

    private

    def wait_for_process_to_exit_or_be_killed(remaining_attempts = 60)
      while running?
        remaining_attempts -= 1
        if remaining_attempts == 35
          @logger.info("Killing #{@description} with PID=#{@pid}")
          kill_process('KILL', @pid)
        elsif remaining_attempts == 0
          raise "KILL signal ignored by #{@description} with PID=#{@pid}"<commit_msg>Make integration test service more thread safe<commit_after>require 'timeout'
require 'bosh/dev'
require 'securerandom'
require 'thread'

module Bosh::Dev::Sandbox
  class Service
    attr_reader :description

    def initialize(cmd_array, cmd_options, logger)
      @cmd_array = cmd_array
      @cmd_options = cmd_options
      @logger = logger
      @pid = nil

      # Add unique identifier to avoid confusing log information
      @description = "#{@cmd_array.first} (#{SecureRandom.hex(4)})"
      @pid_mutex = Mutex.new
    end

    def start
      env = ENV.to_hash.merge(@cmd_options.fetch(:env, {}))

      if running?(@pid)
        @logger.info("Already started #{@description} with PID #{@pid}")
      else
        unless system("which #{@cmd_array.first} > /dev/null")
          raise "Cannot find #{@description} in the $PATH"
        end

        @log_id = SecureRandom.hex(4)

        @pid_mutex.synchronize do
          @pid = Process.spawn(env, *@cmd_array, {
            out: stdout || :close,
            err: stderr || :close,
            in: :close,
          })
        end

        @logger.info("Started #{@description} with PID #{@pid}, log-id: #{@log_id}")

        Process.detach(@pid)

        tries = 0
        until running?(@pid)
          tries += 1
          raise RuntimeError, "Cannot run #{@cmd_array} with #{env.inspect}" if tries > 20
          sleep(0.1)
        end
      end
    end

    def stop(signal = 'TERM')
      pid_to_stop = @pid
      @pid = nil

      if running?(pid_to_stop)
        kill_process(signal, pid_to_stop)

        # Block until process exits to avoid race conditions in the caller
        # (e.g. director process is killed but we don't wait and then we
        # try to delete db which is in use by director)
        wait_for_process_to_exit_or_be_killed(pid_to_stop)
      end
    end

    def stdout_contents
      stdout ? File.read(stdout) : ''
    end

    def stderr_contents
      stderr ? File.read(stderr) : ''
    end

    private

    def running?(pid)
      pid && Process.kill(0, pid)
    rescue Errno::ESRCH # No such process
      false
    rescue Errno::EPERM # Owned by some other user/process
      @logger.info("Process other than #{@description} is running with PID=#{pid} so this service is not running.")
      @logger.debug(`ps #{pid}`)
      false
    end

    def wait_for_process_to_exit_or_be_killed(pid, remaining_attempts = 60)
      while running?(pid)
        remaining_attempts -= 1
        if remaining_attempts == 35
          @logger.info("Killing #{@description} with PID=#{pid}")
          kill_process('KILL', pid)
        elsif remaining_attempts == 0
          raise "KILL signal ignored by #{@description} with PID=#{pid}"
{'fraction_non_alphanumeric': 0.1086713807219948, 'fraction_numerical': 0.005582433941198363, 'mean_word_length': 2.372020075282309, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4139251', 'n_tokens_mistral': 1687, 'n_tokens_neox': 1625, 'n_words': 483}