Module: Metasploit::Framework::Spec::Threads::Suite
- Defined in:
- lib/metasploit/framework/spec/threads/suite.rb
Constant Summary collapse
- EXPECTED_THREAD_COUNT_AROUND_SUITE =
Number of allowed threads when threads are counted in ‘after(:suite)` or `before(:suite)`
Known threads:
1. Main Ruby thread 2. Active Record connection pool thread 3. Framework thread manager, a monitor thread for removing dead threads https://github.com/rapid7/metasploit-framework/blame/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/lib/msf/core/thread_manager.rb#L66-L89 4. Ruby's Timeout library thread, an automatically created monitor thread when using `Thread.timeout(1) { }` https://github.com/ruby/timeout/blob/bd25f4b138b86ef076e6d9d7374b159fffe5e4e9/lib/timeout.rb#L129-L137 5. REMOTE_DB thread, if enabled
Intermittent threads that are non-deterministically left behind, which should be fixed in the future:
1. metadata cache hydration https://github.com/rapid7/metasploit-framework/blob/115946cd06faccac654e956e8ba9cf72ff328201/lib/msf/core/modules/metadata/cache.rb#L150-L153 2. session manager https://github.com/rapid7/metasploit-framework/blob/115946cd06faccac654e956e8ba9cf72ff328201/lib/msf/core/session_manager.rb#L153-L168
ENV['REMOTE_DB'] ? 7 : 6
- LOG_PATHNAME =
‘caller` for all Thread.new calls
Pathname.new('log/metasploit/framework/spec/threads/suite.log')
- UUID_REGEXP =
Regular expression for extracting the UUID out of LOG_PATHNAME for each Thread.new caller block
/BEGIN Thread.new caller \((?<uuid>.*)\)/
- UUID_THREAD_LOCAL_VARIABLE =
Name of thread local variable that Thread UUID is stored
"metasploit/framework/spec/threads/logger/uuid"
Class Method Summary collapse
-
.caller_by_thread_uuid ⇒ Hash{String => Array<String>}
The ‘caller` for each Thread UUID.
-
.configure! ⇒ void
Configures ‘before(:suite)` and `after(:suite)` callback to detect thread leaks.
- .define_task ⇒ Object
-
.each_suite_line {|line| ... } ⇒ Object
Yields each line of LOG_PATHNAME that happened during the suite run.
-
.each_thread_line {|uuid, line| ... } ⇒ Object
Yield each line for each Thread UUID gathered during the suite run.
- .non_debugger_thread_list ⇒ Object
Class Method Details
.caller_by_thread_uuid ⇒ Hash{String => Array<String>}
The ‘caller` for each Thread UUID.
208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 208 def self.caller_by_thread_uuid lines_by_thread_uuid = Hash.new { |hash, uuid| hash[uuid] = [] } each_thread_line do |uuid, line| lines_by_thread_uuid[uuid] << line end lines_by_thread_uuid end |
.configure! ⇒ void
This method returns an undefined value.
Configures ‘before(:suite)` and `after(:suite)` callback to detect thread leaks.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 47 def self.configure! unless @configured RSpec.configure do |config| config.before(:suite) do thread_count = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list.count # check with if first so that error message can be constructed lazily if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE # LOG_PATHNAME may not exist if suite run without `rake spec` if LOG_PATHNAME.exist? log = LOG_PATHNAME.read() else log "Run `rake spec` to log where Thread.new is called." end raise RuntimeError, "#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when " \ "only #{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \ "#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected before suite runs:\n" \ "#{log}" end LOG_PATHNAME.parent.mkpath LOG_PATHNAME.open('a') do |f| # separator so after(:suite) can differentiate between threads created before(:suite) and during the # suites f.puts 'before(:suite)' end end config.after(:suite) do LOG_PATHNAME.parent.mkpath LOG_PATHNAME.open('a') do |f| # separator so that a flip flop can be used when reading the file below. Also useful if it turns # out any threads are being created after this callback, which could be the case if another # after(:suite) accidentally created threads by creating an Msf::Simple::Framework instance. f.puts 'after(:suite)' end thread_list = Metasploit::Framework::Spec::Threads::Suite.non_debugger_thread_list thread_count = thread_list.count if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE error_lines = [] if LOG_PATHNAME.exist? caller_by_thread_uuid = Metasploit::Framework::Spec::Threads::Suite.caller_by_thread_uuid thread_list.each do |thread| thread_uuid = thread[Metasploit::Framework::Spec::Threads::Suite::UUID_THREAD_LOCAL_VARIABLE] thread_name = thread[:tm_name] # unmanaged thread, such as the main VM thread unless thread_uuid next end caller = caller_by_thread_uuid[thread_uuid] error_lines << "Thread #{thread_uuid}'s (name=#{thread_name} status is #{thread.status.inspect} " \ "and was started here:\n" error_lines.concat(caller) error_lines << "The thread backtrace was:\n#{thread.backtrace ? thread.backtrace.join("\n") : 'nil (no backtrace)'}\n" end else error_lines << "Run `rake spec` to log where Thread.new is called." end raise RuntimeError, "#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when only " \ "#{EXPECTED_THREAD_COUNT_AROUND_SUITE} " \ "#{'thread'.pluralize(EXPECTED_THREAD_COUNT_AROUND_SUITE)} expected after suite runs:\n" \ "#{error_lines.join}" end end end @configured = true end @configured end |
.define_task ⇒ Object
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 132 def self.define_task Rake::Task.define_task('metasploit:framework:spec:threads:suite') do if Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.exist? Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.delete end parent_pathname = Pathname.new(__FILE__).parent threads_logger_pathname = parent_pathname.join('logger') load_pathname = parent_pathname.parent.parent.parent.parent. # Must append to RUBYOPT or Rubymine debugger will not work ENV['RUBYOPT'] = "#{ENV['RUBYOPT']} -I#{load_pathname} -r#{threads_logger_pathname}" end Rake::Task.define_task(spec: 'metasploit:framework:spec:threads:suite') end |
.each_suite_line {|line| ... } ⇒ Object
Ensure LOG_PATHNAME exists before calling.
Yields each line of LOG_PATHNAME that happened during the suite run.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 156 def self.each_suite_line in_suite = false LOG_PATHNAME.each_line do |line| if in_suite if line.start_with?('after(:suite)') break else yield line end else if line.start_with?('before(:suite)') in_suite = true end end end end |
.each_thread_line {|uuid, line| ... } ⇒ Object
Ensure LOG_PATHNAME exists before calling.
Yield each line for each Thread UUID gathered during the suite run.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 182 def self.each_thread_line in_thread_caller = false uuid = nil each_suite_line do |line| if in_thread_caller if line.start_with?('END Thread.new caller') in_thread_caller = false next else yield uuid, line end else match = line.match(UUID_REGEXP) if match in_thread_caller = true uuid = match[:uuid] end end end end |
.non_debugger_thread_list ⇒ Object
221 222 223 224 225 226 227 228 |
# File 'lib/metasploit/framework/spec/threads/suite.rb', line 221 def self.non_debugger_thread_list Thread.list.reject { |thread| # don't do `is_a? Debugger::DebugThread` because it requires Debugger::DebugThread to be loaded, which it # won't when not debugging. thread.class.name == 'Debugger::DebugThread' || thread.class.name == 'Debase::DebugThread' } end |