#---------------------------------------------------------------# # # # (C) Copyright Rubysophic Inc. 2007-2008 # # All rights reserved. # # # # Use, duplication or disclosure of the code is not permitted # # unless licensed. # # # # Last Updated: 7/09/08 # #---------------------------------------------------------------# # # # RubyRunInitializer__ sets up the environment for RubyRun. # # The major task is to identify the application classes and # # modules that are potential candidates for instrumentation. # # # # Also as at this release methods with super keyword is not # # supported. These methods will need to be identified upfront. # # # #---------------------------------------------------------------# module RubyRunInitializer__ require 'logger' require 'yaml' require 'digest/md5' require 'rubyrun_globals' require 'rubyrun_utils__' include RubyRunGlobals include RubyRunUtils__ # 1. Get all directories, logs and properties set up correctly # 2. Scan files in APP_PATHS to create an initial INCLUDE_HASH # and an EXCLUDE_HASH def init_rubyrun ready_rubyrun_env discover_targets end # 1. Extract the working directory from ENV VAR and recursively create all the # subdirectories if they dont exist # 2. Create the log and trace folers # 3. Initialize the loggers # 4. Load the properties from either the current working directory or # rubyrun working directory # 5. Spawn a separate thread to for monitoring and commands def ready_rubyrun_env begin raise RuntimeError, "environment variable #{RUBYRUN_WORKING_DIR} not set", caller \ unless env_var_exists?(RUBYRUN_WORKING_DIR) rubyrun_working_dir = ENV[RUBYRUN_WORKING_DIR] raise RuntimeError, "Missing #{RUBYRUN_OPTS_FILE} in #{rubyrun_working_dir}", caller \ unless File.exists?(rubyrun_working_dir + RUBYRUN_OPTS_FILE) rescue Exception => e fatal_exit(e) end *rubyrun_folders = rubyrun_working_dir + RUBYRUN_LOG, rubyrun_working_dir + RUBYRUN_REPORT, rubyrun_working_dir + RUBYRUN_SIGNATURE make_folder(rubyrun_folders) @rubyrun_log_folder, @rubyrun_report_folder, @rubyrun_signature_folder = *rubyrun_folders logname = File.basename($0, ".*") ready_logfile(logname) system_wide_opts = rubyrun_working_dir + RUBYRUN_OPTS_FILE app_wide_opts = Dir.getwd + '/' + RUBYRUN_OPTS_FILE File.exists?(app_wide_opts) ? load_config_props(app_wide_opts) : load_config_props(system_wide_opts) $rubyrun_tracer = RubyRunHTMLWriter.new(@rubyrun_report_folder + '/' + logname + '_' + $$.to_s + '_trace.html', METHOD_TRACE_HEADER, shift_age = 10, shift_size = 4096000) unless $rubyrun_trace_hash.empty? end # Make sub-directories (folders) def make_folder(rubyrun_folders) rubyrun_folders.each {|rubyrun_folder| begin Dir.mkdir(rubyrun_folder) unless File.exist?(rubyrun_folder) rescue Exception => e fatal_exit(e) end } end # Initialize rubyrun logger and create its own log format def ready_logfile(logname) $rubyrun_logger = Logger.new(@rubyrun_log_folder + '/' + logname + '_' + $$.to_s + '.log', shift_age = 10, shift_size = 4096000) $rubyrun_logger.level = Logger::INFO class << $rubyrun_logger include RubyRunUtils__ def format_message (severity, timestamp, progname, msg) "[#{timestamp.strftime("%Y-%m-%d %H:%M:%S")}.#{("%.3f" % timestamp.to_f).split('.')[1]}] #{get_thread_id} #{msg}\n" end end end # Property file is a yml file. # Load the properties into $config hash def load_config_props(config_file) begin $rubyrun_config = YAML.load_file(config_file) $rubyrun_logger.info "Properties found in #{config_file}:" RUBYRUN_PROP_DEFAULTS.each {|prop, def_value| $rubyrun_config[prop] = def_value unless config_prop_exists?(prop) $rubyrun_logger.info "#{prop} = #{$rubyrun_config[prop].inspect}" } $rubyrun_logger.info "***** APP_PATHS is nil. Applications will not be instrumented. *****" \ if $rubyrun_config['APP_PATHS'].empty? rescue Exception => e fatal_exit(e) end $rubyrun_debug_args = $rubyrun_config['DEBUG_ARGS'] $rubyrun_debug_obj = $rubyrun_config['DEBUG_OBJ'] $rubyrun_dad = $rubyrun_config['DAD'] $rubyrun_report_timer = $rubyrun_config['REPORT_TIMER'] $rubyrun_report_shift_age = $rubyrun_config['REPORT_SHIFT_AGE'] $rubyrun_trace_hash = $rubyrun_config['TRACE_HASH'] $rubyrun_adapter_hash = $rubyrun_config['DB_ADAPTER_HASH'] validate_opts end # Validate the range of REPORT_TIMER and REPORT_SHIFT_AGE # Use default values if out of acceptable range def validate_opts if $rubyrun_report_timer > 3600 || $rubyrun_report_timer < 60 $rubyrun_report_timer = RUBYRUN_PROP_DEFAULTS['REPORT_TIMER'] $rubyrun_logger.warn "REPORT_TIMER value must be between 60 and 3600. #{$rubyrun_report_timer}is used." end if $rubyrun_report_shift_age > 120 || $rubyrun_report_shift_age < 1 $rubyrun_report_shift_age = RUBYRUN_PROP_DEFAULTS['REPORT_SHIFT_AGE'] $rubyrun_logger.warn "REPORT_SHIFT_AGE value must be between 1 and 120. #{$rubyrun_report_shift_age}is used." end end # If the property is not defined or defined but with nil value, it # is deemed to be non-existent def config_prop_exists?(prop) $rubyrun_config.has_key?(prop) && !$rubyrun_config[prop].nil? ? true : false end # Set up global variables from the property file rubyrun_config.yml # For APP_PATHS property, expand any subdirectories and look for # .rb files recursively def discover_targets identify_candidates $rubyrun_include_hash.merge!($rubyrun_config['INCLUDE_HASH']) ['DB_ADAPTER_HASH','OUTER_DISPATCH_HASH','INNER_DISPATCH_HASH'].each { |hash_key| $rubyrun_include_hash.merge!($rubyrun_config[hash_key]) { |k,o,n| o.concat(n) } if config_prop_exists?(hash_key) } [RUBYRUN_OUTER_DISPATCH_HASH,RUBYRUN_INNER_DISPATCH_HASH,RUBYRUN_THREAD_END_HASH,RUBYRUN_VIEW_HASH].each {|hash| $rubyrun_include_hash.merge!(hash) { |k,o,n| o.concat(n) } } $rubyrun_exclude_hash.merge!($rubyrun_config['EXCLUDE_HASH']) $rubyrun_outer_dispatch_hash = config_prop_exists?('OUTER_DISPATCH_HASH') ? RUBYRUN_OUTER_DISPATCH_HASH.merge($rubyrun_config['OUTER_DISPATCH_HASH']) : RUBYRUN_OUTER_DISPATCH_HASH $rubyrun_inner_dispatch_hash = config_prop_exists?('INNER_DISPATCH_HASH') ? RUBYRUN_INNER_DISPATCH_HASH.merge($rubyrun_config['INNER_DISPATCH_HASH']) : RUBYRUN_INNER_DISPATCH_HASH $rubyrun_logger.info "Final INCLUDE_HASH = #{$rubyrun_include_hash.inspect}" $rubyrun_logger.info "Final EXCLUDE_HASH = #{$rubyrun_exclude_hash.inspect}" end # If the directory content has changed (new files, modified files, # new APP_PATHS) a new scan will be required, otherwise the last scan # results will simply be reused. When a new scan is finished, # the results are serialized. # # Digest technique is used to detect changes in directory content. def identify_candidates $rubyrun_config['APP_PATHS'].each {|element| element = Dir.getwd if element == '.' expand_folder(element) } unless $rubyrun_file_date_hash.empty? dir_signature = generate_hash($rubyrun_file_date_hash) exclude_array = [] if directory_changed?(dir_signature) $rubyrun_file_date_hash.each {|f, date| scan_module_class(f) scan_super(f).each {|name| exclude_array << name} } $rubyrun_exclude_hash['*'] = exclude_array.uniq if exclude_array.length > 0 serialize_scan_history(dir_signature) end end deserialize_scan_history $rubyrun_file_date_hash.clear if $rubyrun_file_date_hash end # For each directory, expand into a list of filenames and its modifed time. def expand_folder(folder) ($rubyrun_logger.warn("WARN: APP_PATHS not found: #{folder}") ; return) unless File.exists?(folder) if File.file?(folder) return if File.extname(folder) != '.rb' path = File.expand_path(folder) $rubyrun_file_date_hash[path] = File.mtime(path).to_i else Dir.entries(folder).each {|entry| next if entry =~ /^\.+/ path = File.expand_path(entry, folder) case when File.directory?(path) expand_folder(path) else next if File.extname(entry) != '.rb' $rubyrun_file_date_hash[path] = File.mtime(path).to_i end } end end # Compare the digest calculated from the current APP_PATHS directory # contents to the last serialized one. Return true(changed) or false # (unchanged) def directory_changed?(dir_signature) f = get_dir_hash_file dir_sig = File.exists?(f) ? File.open(f) {|f| Marshal.load(f)} : nil dir_signature != dir_sig end # Calcualte the digest from the directory contents of APP_PATHS def generate_hash(hash) Digest::MD5.hexdigest(hash.to_s) end # Use Marshal to serialize scan results (the include and exclude hashes) def serialize_scan_history(dir_signature) File.open(get_include_hash_file, 'w') {|f| Marshal.dump($rubyrun_include_hash, f)} File.open(get_exclude_hash_file, 'w') {|f| Marshal.dump($rubyrun_exclude_hash, f)} File.open(get_dir_hash_file, 'w') {|f| Marshal.dump(dir_signature, f)} end # Use Marshal to de-serialize the include and exclude hashes def deserialize_scan_history f = get_include_hash_file $rubyrun_include_hash = File.exists?(f) ? File.open(f) {|f| Marshal.load(f)} : {} f = get_exclude_hash_file $rubyrun_exclude_hash = File.exists?(f) ? File.open(f) {|f| Marshal.load(f)} : {} end # Regular expression that detects the class or module names of a given file def scan_module_class(path) $rubyrun_logger.info "Candidate modules/classes for instrumentation in #{path}:" open(path).grep(/^\s*(class|module)\s+([[:upper:]][A-Za-z0-9_:]*)/m) { $rubyrun_logger.info "\t #{$2}" $rubyrun_include_hash[$2] = [] } end # Line up all the method names in a file by line # # Line up all the line numbers that contain the keyworkd super # Collate the two arrays to determine which method has super in it # Put these method names in EXCLUDE_HASH and skip instrumentation def scan_super(path) $rubyrun_logger.info "Method(s) to be excluded from instrumentation in #{path}:" method_name_array, method_lineno_array, super_array, exclude_array = [],[],[],[] open(path) {|f| f.readlines.each_with_index {|code, lineno| code.scan(/^\s*def\s+(.*)\(+.*\)+/m) (method_name_array << $1; method_lineno_array << lineno) if $1 code.scan(/^\s*(super)/m) super_array << lineno if $1 } } method_name_array.reverse! method_lineno_array.reverse! super_array.each {|lineno| method_lineno_array.each_with_index {|linenum, i| unless lineno < linenum $rubyrun_logger.info "\t #{method_name_array[i]}" m = method_name_array[i].split('.') exclude_array << (m.length > 1 ? m[1] : m[0]) break end } } exclude_array end # Return the target file name that stores the serialized INCLUDE_HASH def get_include_hash_file @rubyrun_signature_folder + '/' + RUBYRUN_INCLUDE_HASH_FILE end # return the target file name that stores the serialized EXCLUDE_HASH def get_exclude_hash_file @rubyrun_signature_folder + '/' + RUBYRUN_EXCLUDE_HASH_FILE end # Return the target file name that stores the serialized directory contents digest # This file has to be application dependent, and we use the current directory of the # running process to represent the application directory. def get_dir_hash_file @rubyrun_signature_folder + '/' + RUBYRUN_DIR_HASH_FILE + '_' + Digest::MD5.hexdigest(Dir.getwd) end end