# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'objspace'
require 'contrast/components/interface'

module Contrast
  module Utils
    # Implementation of a heap dump util to automate generation
    class HeapDumpUtil
      include Contrast::Components::Interface
      access_component :heap_dump, :logging

      LOG_ERROR_DUMPS = 'Unable to generate heap dumps'
      FILE_WRITE_FLAGS = 'w'

      class << self
        def run
          return unless heap_dump_enabled?

          log_enabled_warning
          dir = heap_dump_control[:path]
          Dir.mkdir(dir) unless Dir.exist?(dir)
          return unless File.writable?(dir)

          delay = heap_dump_control[:delay]
          Contrast::Agent::Thread.new do
            logger.info("HEAP DUMP THREAD INITIALIZED. WAITING #{ delay } SECONDS TO BEGIN.")
            sleep(delay)
            capture_heap_dump
          end
        rescue StandardError => e
          logger.info(LOG_ERROR_DUMPS, e)
          nil
        end

        def log_enabled_warning
          dir    = heap_dump_control[:path]
          window = heap_dump_control[:window]
          count  = heap_dump_control[:count]
          delay  = heap_dump_control[:delay]
          clean  = heap_dump_control[:clean]

          logger.info <<~WARNING
            *****************************************************
            ********      HEAP DUMP HAS BEEN ENABLED     ********
            *** APPLICATION PROCESS WILL EXIT UPON COMPLETION ***
            *****************************************************

            Heap dump is a debugging tool that snapshots the entire
            state of the Ruby VM. It is an exceptionally expensive
            process, and should only be used to debug especially
            pernicious errors.

            It will write multiple memory snaphots, which are liable
            to be multiple gigabytes in size.
            They will be named "[unix timestamp]-heap.dump",
            e.g.: 1020304050-heap.dump

            It will then call Ruby `exit()`.

            If this is not your specific intent, you can (and should)
            disable this option in your Contrast config file.

            HEAP DUMP PARAMETERS:
            \t[write files to this directory]             dir:     #{ dir    }
            \t[wait this many seconds in between dumps]   window:  #{ window }
            \t[heap dump this many times]                 count:   #{ count  }
            \t[wait this many seconds into app lifetime]  delay:   #{ delay  }
            \t[perform gc pass before dump]               clean:   #{ clean  }

            *****************************************************
            ********        YOU HAVE BEEN WARNED         ********
            *****************************************************
          WARNING
        end

        def capture_heap_dump
          dir    = heap_dump_control[:path]
          window = heap_dump_control[:window]
          count  = heap_dump_control[:count]
          clean  = heap_dump_control[:clean]
          logger.info('HEAP DUMP MAIN LOOP')
          ObjectSpace.trace_object_allocations_start
          count.times do |i|
            logger.info('STARTING HEAP DUMP PASS', current_pass: i + 1, max: count)
            output = "#{ Time.now.to_f }-heap.dump"
            output = File.join(dir, output)
            begin
              logger.info('OPENING HEADUMP FILE', dir: dir, file: output)
              file = File.new(output, FILE_WRITE_FLAGS)
              if clean
                logger.info('PERFORMING GARBAGE COLLECTION BEFORE HEAP DUMP')
                GC.start
              end
              ObjectSpace.dump_all(output: file)
              logger.info('FINISHING HEAP DUMP PASS', current_pass: i + 1, max: count)
            ensure
              file.close
            end
            sleep(window)
          end
        ensure
          ObjectSpace.trace_object_allocations_stop
          logger.info('*****************************************************')
          logger.info('********        HEAP DUMP HAS CONCLUDED      ********')
          logger.info('***     APPLICATION PROCESS WILL EXIT SHORTLY     ***')
          logger.info('*****************************************************')
          exit # rubocop:disable Rails/Exit We weren't kidding!
        end
      end
    end
  end
end