1
+ require 'pathname'
2
+
3
+ # @note needs to use explicit nesting. so this file can be loaded directly without loading 'metasploit/framework' which
4
+ # allows for faster loading of rake tasks.
5
+ module Metasploit
6
+ module Framework
7
+ module Spec
8
+ module Threads
9
+ module Suite
10
+ #
11
+ # CONSTANTS
12
+ #
13
+
14
+ # Number of allowed threads when threads are counted in `after(:suite)` or `before(:suite)`
15
+ EXPECTED_THREAD_COUNT_AROUND_SUITE = 1
16
+ # `caller` for all Thread.new calls
17
+ LOG_PATHNAME = Pathname . new ( 'log/metasploit/framework/spec/threads/suite.log' )
18
+ # Regular expression for extracting the UUID out of {LOG_PATHNAME} for each Thread.new caller block
19
+ UUID_REGEXP = /BEGIN Thread.new caller \( (?<uuid>.*)\) /
20
+ # Name of thread local variable that Thread UUID is stored
21
+ UUID_THREAD_LOCAL_VARIABLE = "metasploit/framework/spec/threads/logger/uuid"
22
+
23
+ #
24
+ # Module Methods
25
+ #
26
+
27
+ # Configures `before(:suite)` and `after(:suite)` callback to detect thread leaks.
28
+ #
29
+ # @return [void]
30
+ def self . configure!
31
+ unless @configured
32
+ RSpec . configure do |config |
33
+ config . before ( :suite ) do
34
+ thread_count = Metasploit ::Framework ::Spec ::Threads ::Suite . non_debugger_thread_list . count
35
+
36
+ # check with if first so that error message can be constructed lazily
37
+ if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE
38
+ # LOG_PATHNAME may not exist if suite run without `rake spec`
39
+ if LOG_PATHNAME . exist?
40
+ log = LOG_PATHNAME . read ( )
41
+ else
42
+ log "Run `rake spec` to log where Thread.new is called."
43
+ end
44
+
45
+ raise RuntimeError ,
46
+ "#{ thread_count } #{ 'thread' . pluralize ( thread_count ) } exist(s) when " \
47
+ "only #{ EXPECTED_THREAD_COUNT_AROUND_SUITE } " \
48
+ "#{ 'thread' . pluralize ( EXPECTED_THREAD_COUNT_AROUND_SUITE ) } expected before suite runs:\n " \
49
+ "#{ log } "
50
+ end
51
+
52
+ LOG_PATHNAME . parent . mkpath
53
+
54
+ LOG_PATHNAME . open ( 'a' ) do |f |
55
+ # separator so after(:suite) can differentiate between threads created before(:suite) and during the
56
+ # suites
57
+ f . puts 'before(:suite)'
58
+ end
59
+ end
60
+
61
+ config . after ( :suite ) do
62
+ LOG_PATHNAME . parent . mkpath
63
+
64
+ LOG_PATHNAME . open ( 'a' ) do |f |
65
+ # separator so that a flip flop can be used when reading the file below. Also useful if it turns
66
+ # out any threads are being created after this callback, which could be the case if another
67
+ # after(:suite) accidentally created threads by creating an Msf::Simple::Framework instance.
68
+ f . puts 'after(:suite)'
69
+ end
70
+
71
+ thread_list = Metasploit ::Framework ::Spec ::Threads ::Suite . non_debugger_thread_list
72
+ thread_count = thread_list . count
73
+
74
+ if thread_count > EXPECTED_THREAD_COUNT_AROUND_SUITE
75
+ error_lines = [ ]
76
+
77
+ if LOG_PATHNAME . exist?
78
+ caller_by_thread_uuid = Metasploit ::Framework ::Spec ::Threads ::Suite . caller_by_thread_uuid
79
+
80
+ thread_list . each do |thread |
81
+ thread_uuid = thread [ Metasploit ::Framework ::Spec ::Threads ::Suite ::UUID_THREAD_LOCAL_VARIABLE ]
82
+
83
+ # unmanaged thread, such as the main VM thread
84
+ unless thread_uuid
85
+ next
86
+ end
87
+
88
+ caller = caller_by_thread_uuid [ thread_uuid ]
89
+
90
+ error_lines << "Thread #{ thread_uuid } 's status is #{ thread . status . inspect } " \
91
+ "and was started here:\n "
92
+
93
+ error_lines . concat ( caller )
94
+ end
95
+ else
96
+ error_lines << "Run `rake spec` to log where Thread.new is called."
97
+ end
98
+
99
+ raise RuntimeError ,
100
+ "#{ thread_count } #{ 'thread' . pluralize ( thread_count ) } exist(s) when only " \
101
+ "#{ EXPECTED_THREAD_COUNT_AROUND_SUITE } " \
102
+ "#{ 'thread' . pluralize ( EXPECTED_THREAD_COUNT_AROUND_SUITE ) } expected after suite runs:\n " \
103
+ "#{ error_lines . join } "
104
+ end
105
+ end
106
+ end
107
+
108
+ @configured = true
109
+ end
110
+
111
+ @configured
112
+ end
113
+
114
+ def self . define_task
115
+ Rake ::Task . define_task ( 'metasploit:framework:spec:threads:suite' ) do
116
+ if Metasploit ::Framework ::Spec ::Threads ::Suite ::LOG_PATHNAME . exist?
117
+ Metasploit ::Framework ::Spec ::Threads ::Suite ::LOG_PATHNAME . delete
118
+ end
119
+
120
+ parent_pathname = Pathname . new ( __FILE__ ) . parent
121
+ threads_logger_pathname = parent_pathname . join ( 'logger' )
122
+ load_pathname = parent_pathname . parent . parent . parent . parent . expand_path
123
+
124
+ # Must append to RUBYOPT or Rubymine debugger will not work
125
+ ENV [ 'RUBYOPT' ] = "#{ ENV [ 'RUBYOPT' ] } -I#{ load_pathname } -r#{ threads_logger_pathname } "
126
+ end
127
+
128
+ Rake ::Task . define_task ( spec : 'metasploit:framework:spec:threads:suite' )
129
+ end
130
+
131
+ # @note Ensure {LOG_PATHNAME} exists before calling.
132
+ #
133
+ # Yields each line of {LOG_PATHNAME} that happened during the suite run.
134
+ #
135
+ # @yield [line]
136
+ # @yieldparam line [String] a line in the {LOG_PATHNAME} between `before(:suite)` and `after(:suite)`
137
+ # @yieldreturn [void]
138
+ def self . each_suite_line
139
+ in_suite = false
140
+
141
+ LOG_PATHNAME . each_line do |line |
142
+ if in_suite
143
+ if line . start_with? ( 'after(:suite)' )
144
+ break
145
+ else
146
+ yield line
147
+ end
148
+ else
149
+ if line . start_with? ( 'before(:suite)' )
150
+ in_suite = true
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ # @note Ensure {LOG_PATHNAME} exists before calling.
157
+ #
158
+ # Yield each line for each Thread UUID gathered during the suite run.
159
+ #
160
+ # @yield [uuid, line]
161
+ # @yieldparam uuid [String] the UUID of thread thread
162
+ # @yieldparam line [String] a line in the `caller` for the given `uuid`
163
+ # @yieldreturn [void]
164
+ def self . each_thread_line
165
+ in_thread_caller = false
166
+ uuid = nil
167
+
168
+ each_suite_line do |line |
169
+ if in_thread_caller
170
+ if line . start_with? ( 'END Thread.new caller' )
171
+ in_thread_caller = false
172
+ next
173
+ else
174
+ yield uuid , line
175
+ end
176
+ else
177
+ match = line . match ( UUID_REGEXP )
178
+
179
+ if match
180
+ in_thread_caller = true
181
+ uuid = match [ :uuid ]
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # The `caller` for each Thread UUID.
188
+ #
189
+ # @return [Hash{String => Array<String>}]
190
+ def self . caller_by_thread_uuid
191
+ lines_by_thread_uuid = Hash . new { |hash , uuid |
192
+ hash [ uuid ] = [ ]
193
+ }
194
+
195
+ each_thread_line do |uuid , line |
196
+ lines_by_thread_uuid [ uuid ] << line
197
+ end
198
+
199
+ lines_by_thread_uuid
200
+ end
201
+
202
+ # @return
203
+ def self . non_debugger_thread_list
204
+ Thread . list . reject { |thread |
205
+ # don't do `is_a? Debugger::DebugThread` because it requires Debugger::DebugThread to be loaded, which it
206
+ # won't when not debugging.
207
+ thread . class . name == 'Debugger::DebugThread'
208
+ }
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
0 commit comments