diff --git a/Rakefile b/Rakefile index c4be0fc98fc9..3acfdb97bfba 100755 --- a/Rakefile +++ b/Rakefile @@ -10,4 +10,5 @@ require 'metasploit/framework/spec/untested_payloads' Metasploit::Framework::Require.optionally_active_record_railtie Metasploit::Framework::Application.load_tasks +Metasploit::Framework::Spec::Threads::Suite.define_task Metasploit::Framework::Spec::UntestedPayloads.define_task diff --git a/lib/metasploit/framework/spec/threads/logger.rb b/lib/metasploit/framework/spec/threads/logger.rb new file mode 100644 index 000000000000..ec67364a5543 --- /dev/null +++ b/lib/metasploit/framework/spec/threads/logger.rb @@ -0,0 +1,23 @@ +require 'metasploit/framework/spec/threads/suite' + +original_thread_new = Thread.method(:new) + +# Patches `Thread.new` so that if logs `caller` so thread leaks can be traced +Thread.define_singleton_method(:new) { |*args, &block| + lines = ['BEGIN Thread.new caller'] + + caller.each do |frame| + lines << " #{frame}" + end + + lines << 'END Thread.new caller' + + Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.parent.mkpath + + Metasploit::Framework::Spec::Threads::Suite::LOG_PATHNAME.open('a') { |f| + # single puts so threads can't write in between each other. + f.puts lines.join("\n") + } + + original_thread_new.call(*args, &block) +} \ No newline at end of file diff --git a/lib/metasploit/framework/spec/threads/suite.rb b/lib/metasploit/framework/spec/threads/suite.rb index acd17d7ed39c..d123e1554cf5 100644 --- a/lib/metasploit/framework/spec/threads/suite.rb +++ b/lib/metasploit/framework/spec/threads/suite.rb @@ -1,35 +1,65 @@ -module Metasploit::Framework::Spec::Threads::Suite - # - # CONSTANTS - # - - EXPECTED_THREAD_COUNT_BEFORE_SUITE = 1 - - # - # Module Methods - # - - # Configures `before(:suite)` and `after(:suite)` callback to detect thread leaks. - # - # @return [void] - def self.configure! - unless @configured - RSpec.configure do |config| - config.before(:suite) do - thread_count = Thread.list.count - - expect(thread_count).to( - (be <= EXPECTED_THREAD_COUNT_BEFORE_SUITE), - "#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when " \ - "only #{EXPECTED_THREAD_COUNT_BEFORE_SUITE} #{'thread'.pluralize(EXPECTED_THREAD_COUNT_BEFORE_SUITE)} " \ - "expected before suite runs" - ) +require 'pathname' + +# @note needs to use explicit nesting. so this file can be loaded directly without loading 'metasploit/framework' which +# allows for faster loading of rake tasks. +module Metasploit + module Framework + module Spec + module Threads + module Suite + # + # CONSTANTS + # + + # Number of allowed threads when threads are counted in `before(:suite)` + EXPECTED_THREAD_COUNT_BEFORE_SUITE = 1 + # `caller` for all Thread.new calls + LOG_PATHNAME = Pathname.new('log/metasploit/framework/spec/threads/suite.log') + + # + # Module Methods + # + + # Configures `before(:suite)` and `after(:suite)` callback to detect thread leaks. + # + # @return [void] + def self.configure! + unless @configured + RSpec.configure do |config| + config.before(:suite) do + thread_count = Thread.list.count + + # check with if first so that error message can be constructed lazily + if thread_count > EXPECTED_THREAD_COUNT_BEFORE_SUITE + log = LOG_PATHNAME.read() + + raise RuntimeError, + "#{thread_count} #{'thread'.pluralize(thread_count)} exist(s) when " \ + "only #{EXPECTED_THREAD_COUNT_BEFORE_SUITE} " \ + "#{'thread'.pluralize(EXPECTED_THREAD_COUNT_BEFORE_SUITE)} expected before suite runs:\n#{log}" + end + end + end + + @configured = true + end + + @configured + end + + def self.define_task + Rake::Task.define_task('metasploit:framework:spec:threads:suite') do + parent_pathname = Pathname.new(__FILE__).parent + threads_logger_pathname = parent_pathname.join('logger') + load_pathname = parent_pathname.parent.parent.parent.parent.expand_path + + ENV['RUBYOPT'] = "-I#{load_pathname} -r#{threads_logger_pathname} #{ENV['RUBYOPT']}" + end + + Rake::Task.define_task(spec: 'metasploit:framework:spec:threads:suite') + end end end - - @configured = true end - - @configured end end \ No newline at end of file