# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/components/logger' require 'contrast/agent/reporting/report' require 'contrast/agent/reporting/reporting_workers/reporting_workers' require 'contrast/agent/telemetry/base' require 'contrast/agent/protect/input_analyzer/worth_watching_analyzer' require 'contrast/config/diagnostics' module Contrast module Agent # This class used to ensure that our worker threads are running in multi-process environments class ThreadWatcher include Contrast::Components::Logger::InstanceMethods # @return [Contrast::Utils::HeapDumpUtil] attr_reader :heapdump_util # @return [Contrast::Agent::Reporter] attr_reader :reporter # @return [Contrast::Agent::ReportingWorkers::ReporterHeartbeat] attr_reader :reporter_heartbeat # @return [Contrast::Agent::Protect::WorthWatchingInputAnalyzer] attr_reader :worth_watching_analyzer # @return [Contrast::Agent::ReportingWorkers::ServerSettingsWorker] attr_reader :reporter_settings_worker # @return [Contrast::Agent::ReportingWorkers::ApplicationServerWorker] attr_reader :reporter_app_settings_worker def initialize @pids = {} @heapdump_util = Contrast::Utils::HeapDumpUtil.new @reporter = Contrast::Agent::Reporter.new @reporter_heartbeat = Contrast::Agent::ReportingWorkers::ReporterHeartbeat.new @reporter_settings_worker = Contrast::Agent::ReportingWorkers::ServerSettingsWorker.new @reporter_app_settings_worker = Contrast::Agent::ReportingWorkers::ApplicationServerWorker.new @telemetry = Contrast::Agent::Telemetry::Base.new if Contrast::Agent::Telemetry::Base.enabled? @worth_watching_analyzer = Contrast::Agent::Protect::WorthWatchingInputAnalyzer.new unless protect_disabled? end # @return [Hash, nil] map of process to thread startup status def startup! return unless ::Contrast::AGENT.enabled? logger.debug('ThreadWatcher started threads') @pids[Process.pid] = reporter_threads if Contrast::Agent::Telemetry::Base.enabled? telemetry_status = init_thread(telemetry_queue) @pids[Process.pid] = @pids[Process.pid] && telemetry_status end unless protect_disabled? worth_watching_analyzer_status = init_thread(worth_watching_analyzer) @pids[Process.pid] = @pids[Process.pid] && worth_watching_analyzer_status end @pids end def reporter_threads init_thread(reporter) && init_thread(reporter_heartbeat) && init_thread(reporter_settings_worker) && init_thread(reporter_app_settings_worker) end def ensure_running? return if @pids[Process.pid] == true logger.debug('ThreadWatcher - threads not running') startup! end # This method will check the config and if the config is invalid - it will kill the agent def self.check_before_start return if Contrast::CONFIG.send(:validate) @_diagnostics = Contrast::Agent::DiagnosticsConfig::Diagnostics.new(Contrast::AGENT.logger.path || Contrast::Components::Config::CONTRAST_LOG) @_diagnostics.config.populate_fail_connection @_diagnostics.write_to_file_logic(false, reset: false) # kill agent Contrast::AGENT.disable_agent! # TODO: RUBY-1822 # set the in_application_scope to 1 so we short circuit it end def shutdown! heapdump_util&.stop! telemetry_queue&.stop! reporter&.stop! reporter_heartbeat&.stop! worth_watching_analyzer&.stop! reporter_settings_worker&.stop! reporter_app_settings_worker&.stop! end # @return [Contrast::Agent::Telemetry::Base] def telemetry_queue @telemetry end private # Start the thread governed by the given watcher # # @param watcher [Contrast::Agent::ThreadWatcher] # @return [Boolean] if the watched thread started successfully def init_thread watcher return unless watcher&.attempt_to_start? unless watcher.running? logger.debug('Attempting to start thread', type: watcher.to_s) watcher.start_thread! end result = watcher.running? logger.debug('Thread status', type: watcher.to_s, alive: result) result end def protect_disabled? ::Contrast::PROTECT.forcibly_disabled? end end end end