crusher 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +4 -0
- data/bin/crush +22 -0
- data/lib/crusher/configurator.rb +65 -0
- data/lib/crusher/cookie.rb +562 -0
- data/lib/crusher/crush_session.rb +106 -0
- data/lib/crusher/http_load_generator.rb +54 -0
- data/lib/crusher/load_generator.rb +25 -0
- data/lib/crusher/util.rb +14 -0
- data/lib/crusher.rb +12 -0
- metadata +103 -0
data/Rakefile
ADDED
data/bin/crush
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.join( File.dirname(__FILE__) + "/../lib" )
|
3
|
+
require 'crusher'
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'active_support/core_ext'
|
6
|
+
|
7
|
+
config = Crusher::Configurator::Configuration.new('Crushfile')
|
8
|
+
|
9
|
+
target_name = ARGV[0].to_sym
|
10
|
+
scenario_name = ARGV[1].to_sym
|
11
|
+
|
12
|
+
target = config.targets[target_name]
|
13
|
+
scenario = config.scenarios[scenario_name]
|
14
|
+
|
15
|
+
puts "Fatal Error: Unknown target '#{target_name}'" unless target
|
16
|
+
puts "Fatal Error: Unknown scenario '#{scenario_name}'" unless scenario
|
17
|
+
exit(1) unless target && scenario
|
18
|
+
|
19
|
+
options = {}
|
20
|
+
options[:log_file] = ARGV[2]
|
21
|
+
|
22
|
+
Crusher::CrushSession.new(target, scenario, options)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Crusher
|
2
|
+
module Configurator
|
3
|
+
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
attr_reader :targets, :scenarios
|
7
|
+
|
8
|
+
def initialize(config_path)
|
9
|
+
@targets = {}
|
10
|
+
@scenarios = {}
|
11
|
+
eval File.open(config_path).read
|
12
|
+
end
|
13
|
+
|
14
|
+
def target target_name, &block
|
15
|
+
@targets[target_name] = Crusher::Configurator::Target.new(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def scenario scenario_name, &block
|
19
|
+
@scenarios[scenario_name] = Crusher::Configurator::Scenario.new(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
class Target
|
25
|
+
|
26
|
+
def initialize(*args, &block)
|
27
|
+
block.call self, *args
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri(new_uri = nil)
|
31
|
+
return @uri unless new_uri
|
32
|
+
@uri = new_uri
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class Scenario
|
38
|
+
|
39
|
+
attr_reader :launch_jobs, :phases
|
40
|
+
|
41
|
+
def initialize(*args, &block)
|
42
|
+
@launch_jobs = []
|
43
|
+
@phases = []
|
44
|
+
@duration = nil
|
45
|
+
block.call self, *args
|
46
|
+
end
|
47
|
+
|
48
|
+
def launch(count, type, &block)
|
49
|
+
@launch_jobs.push :type => type, :count => count, :proc => block
|
50
|
+
end
|
51
|
+
|
52
|
+
def phase(name, options = {}, &block)
|
53
|
+
@phases << options.merge( :name => name.to_s, :proc => block )
|
54
|
+
end
|
55
|
+
|
56
|
+
def duration(new_duration = nil)
|
57
|
+
return @duration unless new_duration
|
58
|
+
@duration = new_duration
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,562 @@
|
|
1
|
+
# cookie.rb is redistributed file which is originally included in Webagent
|
2
|
+
# version 0.6.2 by TAKAHASHI `Maki' Masayoshi. And it contains some bug fixes.
|
3
|
+
# You can download the entire package of Webagent from
|
4
|
+
# http://www.rubycolor.org/arc/.
|
5
|
+
|
6
|
+
|
7
|
+
# Cookie class
|
8
|
+
#
|
9
|
+
# I refered to w3m's source to make these classes. Some comments
|
10
|
+
# are quoted from it. I'm thanksful for author(s) of it.
|
11
|
+
#
|
12
|
+
# w3m homepage: http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/
|
13
|
+
|
14
|
+
require 'uri'
|
15
|
+
require 'time'
|
16
|
+
require 'monitor'
|
17
|
+
|
18
|
+
class WebAgent
|
19
|
+
|
20
|
+
module CookieUtils
|
21
|
+
|
22
|
+
def head_match?(str1, str2)
|
23
|
+
str1 == str2[0, str1.length]
|
24
|
+
end
|
25
|
+
|
26
|
+
def tail_match?(str1, str2)
|
27
|
+
if str1.length > 0
|
28
|
+
str1 == str2[-str1.length..-1].to_s
|
29
|
+
else
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def domain_match(host, domain)
|
35
|
+
domainname = domain.sub(/\.\z/, '').downcase
|
36
|
+
hostname = host.sub(/\.\z/, '').downcase
|
37
|
+
case domain
|
38
|
+
when /\d+\.\d+\.\d+\.\d+/
|
39
|
+
return (hostname == domainname)
|
40
|
+
when '.'
|
41
|
+
return true
|
42
|
+
when /^\./
|
43
|
+
# allows; host == rubyforge.org, domain == .rubyforge.org
|
44
|
+
return tail_match?(domainname, '.' + hostname)
|
45
|
+
else
|
46
|
+
return (hostname == domainname)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def total_dot_num(string)
|
51
|
+
string.scan(/\./).length()
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
class Cookie
|
57
|
+
include CookieUtils
|
58
|
+
|
59
|
+
attr_accessor :name, :value
|
60
|
+
attr_accessor :domain, :path
|
61
|
+
attr_accessor :expires ## for Netscape Cookie
|
62
|
+
attr_accessor :url
|
63
|
+
attr_writer :use, :secure, :discard, :domain_orig, :path_orig, :override
|
64
|
+
|
65
|
+
USE = 1
|
66
|
+
SECURE = 2
|
67
|
+
DOMAIN = 4
|
68
|
+
PATH = 8
|
69
|
+
DISCARD = 16
|
70
|
+
OVERRIDE = 32
|
71
|
+
OVERRIDE_OK = 32
|
72
|
+
|
73
|
+
def initialize()
|
74
|
+
@name = @value = @domain = @path = nil
|
75
|
+
@expires = nil
|
76
|
+
@url = nil
|
77
|
+
@use = @secure = @discard = @domain_orig = @path_orig = @override = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def discard?
|
81
|
+
@discard
|
82
|
+
end
|
83
|
+
|
84
|
+
def use?
|
85
|
+
@use
|
86
|
+
end
|
87
|
+
|
88
|
+
def secure?
|
89
|
+
@secure
|
90
|
+
end
|
91
|
+
|
92
|
+
def domain_orig?
|
93
|
+
@domain_orig
|
94
|
+
end
|
95
|
+
|
96
|
+
def path_orig?
|
97
|
+
@path_orig
|
98
|
+
end
|
99
|
+
|
100
|
+
def override?
|
101
|
+
@override
|
102
|
+
end
|
103
|
+
|
104
|
+
def flag
|
105
|
+
flg = 0
|
106
|
+
flg += USE if @use
|
107
|
+
flg += SECURE if @secure
|
108
|
+
flg += DOMAIN if @domain_orig
|
109
|
+
flg += PATH if @path_orig
|
110
|
+
flg += DISCARD if @discard
|
111
|
+
flg += OVERRIDE if @override
|
112
|
+
flg
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_flag(flag)
|
116
|
+
flag = flag.to_i
|
117
|
+
@use = true if flag & USE > 0
|
118
|
+
@secure = true if flag & SECURE > 0
|
119
|
+
@domain_orig = true if flag & DOMAIN > 0
|
120
|
+
@path_orig = true if flag & PATH > 0
|
121
|
+
@discard = true if flag & DISCARD > 0
|
122
|
+
@override = true if flag & OVERRIDE > 0
|
123
|
+
end
|
124
|
+
|
125
|
+
def match?(url)
|
126
|
+
domainname = url.host
|
127
|
+
if (!domainname ||
|
128
|
+
!domain_match(domainname, @domain) ||
|
129
|
+
(@path && !head_match?(@path, url.path)) ||
|
130
|
+
(@secure && (url.scheme != 'https')) )
|
131
|
+
return false
|
132
|
+
else
|
133
|
+
return true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def join_quotedstr(array, sep)
|
138
|
+
ret = Array.new()
|
139
|
+
old_elem = nil
|
140
|
+
array.each{|elem|
|
141
|
+
if (elem.scan(/"/).length % 2) == 0
|
142
|
+
if old_elem
|
143
|
+
old_elem << sep << elem
|
144
|
+
else
|
145
|
+
ret << elem
|
146
|
+
old_elem = nil
|
147
|
+
end
|
148
|
+
else
|
149
|
+
if old_elem
|
150
|
+
old_elem << sep << elem
|
151
|
+
ret << old_elem
|
152
|
+
old_elem = nil
|
153
|
+
else
|
154
|
+
old_elem = elem.dup
|
155
|
+
end
|
156
|
+
end
|
157
|
+
}
|
158
|
+
ret
|
159
|
+
end
|
160
|
+
|
161
|
+
def parse(str, url)
|
162
|
+
@url = url
|
163
|
+
# TODO: should not depend on join_quotedstr. scan with escape like CSV.
|
164
|
+
cookie_elem = str.split(/;/)
|
165
|
+
cookie_elem = join_quotedstr(cookie_elem, ';')
|
166
|
+
cookie_elem -= [""] # del empty elements, a cookie might included ";;"
|
167
|
+
first_elem = cookie_elem.shift
|
168
|
+
if first_elem !~ /([^=]*)(\=(.*))?/
|
169
|
+
return
|
170
|
+
## raise ArgumentError 'invalid cookie value'
|
171
|
+
end
|
172
|
+
@name = $1.strip
|
173
|
+
@value = normalize_cookie_value($3)
|
174
|
+
cookie_elem.each{|pair|
|
175
|
+
key, value = pair.split(/=/, 2) ## value may nil
|
176
|
+
key.strip!
|
177
|
+
value = normalize_cookie_value(value)
|
178
|
+
case key.downcase
|
179
|
+
when 'domain'
|
180
|
+
@domain = value
|
181
|
+
when 'expires'
|
182
|
+
@expires = nil
|
183
|
+
begin
|
184
|
+
@expires = Time.parse(value).gmtime() if value
|
185
|
+
rescue ArgumentError
|
186
|
+
end
|
187
|
+
when 'path'
|
188
|
+
@path = value
|
189
|
+
when 'secure'
|
190
|
+
@secure = true ## value may nil, but must 'true'.
|
191
|
+
else
|
192
|
+
## ignore
|
193
|
+
end
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
def normalize_cookie_value(value)
|
198
|
+
if value
|
199
|
+
value = value.strip.sub(/\A"(.*)"\z/) { $1 }
|
200
|
+
value = nil if value.empty?
|
201
|
+
end
|
202
|
+
value
|
203
|
+
end
|
204
|
+
private :normalize_cookie_value
|
205
|
+
end
|
206
|
+
|
207
|
+
class CookieManager
|
208
|
+
include CookieUtils
|
209
|
+
|
210
|
+
### errors
|
211
|
+
class Error < StandardError; end
|
212
|
+
class ErrorOverrideOK < Error; end
|
213
|
+
class SpecialError < Error; end
|
214
|
+
|
215
|
+
attr_reader :cookies
|
216
|
+
attr_accessor :cookies_file
|
217
|
+
attr_accessor :accept_domains, :reject_domains
|
218
|
+
|
219
|
+
# for conformance to http://wp.netscape.com/newsref/std/cookie_spec.html
|
220
|
+
attr_accessor :netscape_rule
|
221
|
+
SPECIAL_DOMAIN = [".com",".edu",".gov",".mil",".net",".org",".int"]
|
222
|
+
|
223
|
+
def initialize(file=nil)
|
224
|
+
@cookies = Array.new()
|
225
|
+
@cookies.extend(MonitorMixin)
|
226
|
+
@cookies_file = file
|
227
|
+
@is_saved = true
|
228
|
+
@reject_domains = Array.new()
|
229
|
+
@accept_domains = Array.new()
|
230
|
+
@netscape_rule = false
|
231
|
+
end
|
232
|
+
|
233
|
+
def cookies=(cookies)
|
234
|
+
@cookies = cookies
|
235
|
+
@cookies.extend(MonitorMixin)
|
236
|
+
end
|
237
|
+
|
238
|
+
def save_all_cookies(force = nil, save_unused = true, save_discarded = true)
|
239
|
+
@cookies.synchronize do
|
240
|
+
check_expired_cookies()
|
241
|
+
if @is_saved and !force
|
242
|
+
return
|
243
|
+
end
|
244
|
+
File.open(@cookies_file, 'w') do |f|
|
245
|
+
@cookies.each do |cookie|
|
246
|
+
if (cookie.use? or save_unused) and
|
247
|
+
(!cookie.discard? or save_discarded)
|
248
|
+
f.print(cookie.url.to_s,"\t",
|
249
|
+
cookie.name,"\t",
|
250
|
+
cookie.value,"\t",
|
251
|
+
cookie.expires.to_i,"\t",
|
252
|
+
cookie.domain,"\t",
|
253
|
+
cookie.path,"\t",
|
254
|
+
cookie.flag,"\n")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
@is_saved = true
|
260
|
+
end
|
261
|
+
|
262
|
+
def save_cookies(force = nil)
|
263
|
+
save_all_cookies(force, false, false)
|
264
|
+
end
|
265
|
+
|
266
|
+
def check_expired_cookies()
|
267
|
+
@cookies.reject!{|cookie|
|
268
|
+
is_expired = (cookie.expires && (cookie.expires < Time.now.gmtime))
|
269
|
+
if is_expired && !cookie.discard?
|
270
|
+
@is_saved = false
|
271
|
+
end
|
272
|
+
is_expired
|
273
|
+
}
|
274
|
+
end
|
275
|
+
|
276
|
+
def parse(str, url)
|
277
|
+
cookie = WebAgent::Cookie.new()
|
278
|
+
cookie.parse(str, url)
|
279
|
+
add(cookie)
|
280
|
+
end
|
281
|
+
|
282
|
+
def make_cookie_str(cookie_list)
|
283
|
+
if cookie_list.empty?
|
284
|
+
return nil
|
285
|
+
end
|
286
|
+
|
287
|
+
ret = ''
|
288
|
+
c = cookie_list.shift
|
289
|
+
ret += "#{c.name}=#{c.value}"
|
290
|
+
cookie_list.each{|cookie|
|
291
|
+
ret += "; #{cookie.name}=#{cookie.value}"
|
292
|
+
}
|
293
|
+
return ret
|
294
|
+
end
|
295
|
+
private :make_cookie_str
|
296
|
+
|
297
|
+
|
298
|
+
def find(url)
|
299
|
+
return nil if @cookies.empty?
|
300
|
+
|
301
|
+
cookie_list = Array.new()
|
302
|
+
@cookies.each{|cookie|
|
303
|
+
is_expired = (cookie.expires && (cookie.expires < Time.now.gmtime))
|
304
|
+
if cookie.use? && !is_expired && cookie.match?(url)
|
305
|
+
if cookie_list.select{|c1| c1.name == cookie.name}.empty?
|
306
|
+
cookie_list << cookie
|
307
|
+
end
|
308
|
+
end
|
309
|
+
}
|
310
|
+
return make_cookie_str(cookie_list)
|
311
|
+
end
|
312
|
+
|
313
|
+
def find_cookie_info(domain, path, name)
|
314
|
+
@cookies.find{|c|
|
315
|
+
c.domain == domain && c.path == path && c.name == name
|
316
|
+
}
|
317
|
+
end
|
318
|
+
private :find_cookie_info
|
319
|
+
|
320
|
+
# not tested well; used only netscape_rule = true.
|
321
|
+
def cookie_error(err, override)
|
322
|
+
if !err.kind_of?(ErrorOverrideOK) || !override
|
323
|
+
raise err
|
324
|
+
end
|
325
|
+
end
|
326
|
+
private :cookie_error
|
327
|
+
|
328
|
+
def add(cookie)
|
329
|
+
url = cookie.url
|
330
|
+
name, value = cookie.name, cookie.value
|
331
|
+
expires, domain, path =
|
332
|
+
cookie.expires, cookie.domain, cookie.path
|
333
|
+
secure, domain_orig, path_orig =
|
334
|
+
cookie.secure?, cookie.domain_orig?, cookie.path_orig?
|
335
|
+
discard, override =
|
336
|
+
cookie.discard?, cookie.override?
|
337
|
+
|
338
|
+
domainname = url.host
|
339
|
+
domain_orig, path_orig = domain, path
|
340
|
+
use_security = override
|
341
|
+
|
342
|
+
if domain
|
343
|
+
|
344
|
+
# [DRAFT 12] s. 4.2.2 (does not apply in the case that
|
345
|
+
# host name is the same as domain attribute for version 0
|
346
|
+
# cookie)
|
347
|
+
# I think that this rule has almost the same effect as the
|
348
|
+
# tail match of [NETSCAPE].
|
349
|
+
if domain !~ /^\./ && domainname != domain
|
350
|
+
domain = '.'+domain
|
351
|
+
end
|
352
|
+
|
353
|
+
# [NETSCAPE] rule
|
354
|
+
if @netscape_rule
|
355
|
+
n = total_dot_num(domain)
|
356
|
+
if n < 2
|
357
|
+
cookie_error(SpecialError.new(), override)
|
358
|
+
elsif n == 2
|
359
|
+
## [NETSCAPE] rule
|
360
|
+
ok = SPECIAL_DOMAIN.select{|sdomain|
|
361
|
+
sdomain == domain[-(sdomain.length)..-1]
|
362
|
+
}
|
363
|
+
if ok.empty?
|
364
|
+
cookie_error(SpecialError.new(), override)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# this implementation does not check RFC2109 4.3.2 case 2;
|
370
|
+
# the portion of host not in domain does not contain a dot.
|
371
|
+
# according to nsCookieService.cpp in Firefox 3.0.4, Firefox 3.0.4
|
372
|
+
# and IE does not check, too.
|
373
|
+
end
|
374
|
+
|
375
|
+
path ||= url.path.sub(%r|/[^/]*|, '')
|
376
|
+
domain ||= domainname
|
377
|
+
@cookies.synchronize do
|
378
|
+
cookie = find_cookie_info(domain, path, name)
|
379
|
+
if !cookie
|
380
|
+
cookie = WebAgent::Cookie.new()
|
381
|
+
cookie.use = true
|
382
|
+
@cookies << cookie
|
383
|
+
end
|
384
|
+
check_expired_cookies()
|
385
|
+
end
|
386
|
+
|
387
|
+
cookie.url = url
|
388
|
+
cookie.name = name
|
389
|
+
cookie.value = value
|
390
|
+
cookie.expires = expires
|
391
|
+
cookie.domain = domain
|
392
|
+
cookie.path = path
|
393
|
+
|
394
|
+
## for flag
|
395
|
+
cookie.secure = secure
|
396
|
+
cookie.domain_orig = domain_orig
|
397
|
+
cookie.path_orig = path_orig
|
398
|
+
if discard || cookie.expires == nil
|
399
|
+
cookie.discard = true
|
400
|
+
else
|
401
|
+
cookie.discard = false
|
402
|
+
@is_saved = false
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def load_cookies()
|
407
|
+
return if !File.readable?(@cookies_file)
|
408
|
+
@cookies.synchronize do
|
409
|
+
@cookies.clear
|
410
|
+
File.open(@cookies_file,'r'){|f|
|
411
|
+
while line = f.gets
|
412
|
+
cookie = WebAgent::Cookie.new()
|
413
|
+
@cookies << cookie
|
414
|
+
col = line.chomp.split(/\t/)
|
415
|
+
cookie.url = URI.parse(col[0])
|
416
|
+
cookie.name = col[1]
|
417
|
+
cookie.value = col[2]
|
418
|
+
if col[3].empty? or col[3] == '0'
|
419
|
+
cookie.expires = nil
|
420
|
+
else
|
421
|
+
cookie.expires = Time.at(col[3].to_i).gmtime
|
422
|
+
end
|
423
|
+
cookie.domain = col[4]
|
424
|
+
cookie.path = col[5]
|
425
|
+
cookie.set_flag(col[6])
|
426
|
+
end
|
427
|
+
}
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def check_cookie_accept_domain(domain)
|
432
|
+
unless domain
|
433
|
+
return false
|
434
|
+
end
|
435
|
+
@accept_domains.each{|dom|
|
436
|
+
if domain_match(domain, dom)
|
437
|
+
return true
|
438
|
+
end
|
439
|
+
}
|
440
|
+
@reject_domains.each{|dom|
|
441
|
+
if domain_match(domain, dom)
|
442
|
+
return false
|
443
|
+
end
|
444
|
+
}
|
445
|
+
return true
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
__END__
|
451
|
+
|
452
|
+
=begin
|
453
|
+
|
454
|
+
== WebAgent::CookieManager Class
|
455
|
+
|
456
|
+
Load, save, parse and send cookies.
|
457
|
+
|
458
|
+
=== Usage
|
459
|
+
|
460
|
+
## initialize
|
461
|
+
cm = WebAgent::CookieManager.new("/home/foo/bar/cookie")
|
462
|
+
|
463
|
+
## load cookie data
|
464
|
+
cm.load_cookies()
|
465
|
+
|
466
|
+
## parse cookie from string (maybe "Set-Cookie:" header)
|
467
|
+
cm.parse(str)
|
468
|
+
|
469
|
+
## send cookie data to url
|
470
|
+
f.write(cm.find(url))
|
471
|
+
|
472
|
+
## save cookie to cookiefile
|
473
|
+
cm.save_cookies()
|
474
|
+
|
475
|
+
|
476
|
+
=== Class Methods
|
477
|
+
|
478
|
+
-- CookieManager::new(file=nil)
|
479
|
+
|
480
|
+
create new CookieManager. If a file is provided,
|
481
|
+
use it as cookies' file.
|
482
|
+
|
483
|
+
=== Methods
|
484
|
+
|
485
|
+
-- CookieManager#save_cookies(force = nil)
|
486
|
+
|
487
|
+
save cookies' data into file. if argument is true,
|
488
|
+
save data although data is not modified.
|
489
|
+
|
490
|
+
-- CookieManager#parse(str, url)
|
491
|
+
|
492
|
+
parse string and store cookie (to parse HTTP response header).
|
493
|
+
|
494
|
+
-- CookieManager#find(url)
|
495
|
+
|
496
|
+
get cookies and make into string (to send as HTTP request header).
|
497
|
+
|
498
|
+
-- CookieManager#add(cookie)
|
499
|
+
|
500
|
+
add new cookie.
|
501
|
+
|
502
|
+
-- CookieManager#load_cookies()
|
503
|
+
|
504
|
+
load cookies' data from file.
|
505
|
+
|
506
|
+
|
507
|
+
== WebAgent::CookieUtils Module
|
508
|
+
|
509
|
+
-- CookieUtils::head_match?(str1, str2)
|
510
|
+
-- CookieUtils::tail_match?(str1, str2)
|
511
|
+
-- CookieUtils::domain_match(host, domain)
|
512
|
+
-- CookieUtils::total_dot_num(str)
|
513
|
+
|
514
|
+
|
515
|
+
== WebAgent::Cookie Class
|
516
|
+
|
517
|
+
=== Class Methods
|
518
|
+
|
519
|
+
-- Cookie::new()
|
520
|
+
|
521
|
+
create new cookie.
|
522
|
+
|
523
|
+
=== Methods
|
524
|
+
|
525
|
+
-- Cookie#match?(url)
|
526
|
+
|
527
|
+
match cookie by url. if match, return true. otherwise,
|
528
|
+
return false.
|
529
|
+
|
530
|
+
-- Cookie#name
|
531
|
+
-- Cookie#name=(name)
|
532
|
+
-- Cookie#value
|
533
|
+
-- Cookie#value=(value)
|
534
|
+
-- Cookie#domain
|
535
|
+
-- Cookie#domain=(domain)
|
536
|
+
-- Cookie#path
|
537
|
+
-- Cookie#path=(path)
|
538
|
+
-- Cookie#expires
|
539
|
+
-- Cookie#expires=(expires)
|
540
|
+
-- Cookie#url
|
541
|
+
-- Cookie#url=(url)
|
542
|
+
|
543
|
+
accessor methods for cookie's items.
|
544
|
+
|
545
|
+
-- Cookie#discard?
|
546
|
+
-- Cookie#discard=(discard)
|
547
|
+
-- Cookie#use?
|
548
|
+
-- Cookie#use=(use)
|
549
|
+
-- Cookie#secure?
|
550
|
+
-- Cookie#secure=(secure)
|
551
|
+
-- Cookie#domain_orig?
|
552
|
+
-- Cookie#domain_orig=(domain_orig)
|
553
|
+
-- Cookie#path_orig?
|
554
|
+
-- Cookie#path_orig=(path_orig)
|
555
|
+
-- Cookie#override?
|
556
|
+
-- Cookie#override=(override)
|
557
|
+
-- Cookie#flag
|
558
|
+
-- Cookie#set_flag(flag_num)
|
559
|
+
|
560
|
+
accessor methods for flags.
|
561
|
+
|
562
|
+
=end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Crusher
|
2
|
+
class CrushSession
|
3
|
+
|
4
|
+
attr_reader :target, :scenario, :log_file
|
5
|
+
|
6
|
+
def initialize(target, scenario, options = {})
|
7
|
+
@target = target
|
8
|
+
@scenario = scenario
|
9
|
+
@queued_jobs = []
|
10
|
+
@running_jobs = []
|
11
|
+
@options = options
|
12
|
+
@current_phase = "booting"
|
13
|
+
|
14
|
+
@scenario.launch_jobs.each do |job|
|
15
|
+
job_id = 0
|
16
|
+
puts job.keys.inspect
|
17
|
+
puts "Launching #{job[:count]} #{job[:type]}(s)..."
|
18
|
+
job[:count].times do
|
19
|
+
queue_job(job, job_id)
|
20
|
+
job_id += 1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
launch_queued_jobs
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def log(message)
|
29
|
+
log_message = Time.now.strftime('%Y-%m-%d %H:%M:%S') + ' ' + @current_phase + ' ' + message + "\n"
|
30
|
+
if @log_file
|
31
|
+
@log_file.syswrite(log_message)
|
32
|
+
else
|
33
|
+
puts log_message
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def queue_job(job, id)
|
41
|
+
@queued_jobs << [job, id]
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_next_phase
|
45
|
+
if @scenario.phases.length == 0
|
46
|
+
shutdown
|
47
|
+
else
|
48
|
+
next_phase = @scenario.phases.shift
|
49
|
+
@current_phase = next_phase[:name].to_s
|
50
|
+
|
51
|
+
if next_phase[:wait]
|
52
|
+
EM::Timer.new(next_phase[:wait].to_i) do
|
53
|
+
run_next_phase
|
54
|
+
end
|
55
|
+
elsif next_phase[:over]
|
56
|
+
seconds_per_job = next_phase[:over].to_f / @running_jobs.count.to_f
|
57
|
+
next_job = 0
|
58
|
+
|
59
|
+
slow_iterator = EM::PeriodicTimer.new(seconds_per_job) do
|
60
|
+
next_phase[:proc].call(@running_jobs[next_job])
|
61
|
+
next_job += 1
|
62
|
+
|
63
|
+
if next_job >= @running_jobs.length
|
64
|
+
slow_iterator.cancel
|
65
|
+
run_next_phase
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def launch_queued_jobs
|
74
|
+
return nil if @queued_jobs.length == 0
|
75
|
+
|
76
|
+
fork do
|
77
|
+
|
78
|
+
@log_file = File.open(@options.delete(:log_file), 'a') if @options[:log_file]
|
79
|
+
|
80
|
+
# Reseed the pseudorandom number generator with the process's PID file, otherwise
|
81
|
+
# we'll inherit our sequence of random numbers from the parent process
|
82
|
+
Kernel.srand(Process.pid)
|
83
|
+
|
84
|
+
EM.run do
|
85
|
+
@queued_jobs.each do |settings|
|
86
|
+
job, job_id = settings
|
87
|
+
options = @options.merge(:process_name => "#{job[:type]} #{job_id}")
|
88
|
+
@running_jobs << job[:proc].call(self, options, job_id)
|
89
|
+
end
|
90
|
+
run_next_phase
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
@queued_jobs = []
|
96
|
+
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def shutdown
|
101
|
+
EM::stop_event_loop
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Crusher
|
2
|
+
|
3
|
+
class HttpLoadGenerator < LoadGenerator
|
4
|
+
|
5
|
+
|
6
|
+
def initialize(crush_session, options)
|
7
|
+
super(crush_session, options)
|
8
|
+
@cookies = ::WebAgent::CookieManager.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(method, path, content = {}, &block)
|
12
|
+
|
13
|
+
uri = URI.parse(path)
|
14
|
+
|
15
|
+
http_args = content.keys.map do |key|
|
16
|
+
content[key] ? "#{URI.escape(key.to_s)}=#{URI.escape(content[key].to_s)}" : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
qs, content = if method == :get
|
20
|
+
[((uri.query || '').split('&') + http_args.compact).join("&"), nil]
|
21
|
+
else
|
22
|
+
[uri.query, http_args.join("&")]
|
23
|
+
end
|
24
|
+
|
25
|
+
options = {
|
26
|
+
:verb => method,
|
27
|
+
:host => uri.host,
|
28
|
+
:port => uri.port,
|
29
|
+
:request => uri.path,
|
30
|
+
:query_string => qs,
|
31
|
+
:content => content,
|
32
|
+
:contenttype => 'application/x-www-form-urlencoded',
|
33
|
+
:cookie => @cookies.find(uri),
|
34
|
+
:ssl => (uri.scheme == "https" ? true : false)
|
35
|
+
}
|
36
|
+
|
37
|
+
start_time = Time.now
|
38
|
+
EM::Protocols::HttpClient.request(options).callback do |response|
|
39
|
+
duration = Time.now - start_time
|
40
|
+
|
41
|
+
response[:headers].each do |header|
|
42
|
+
matcher = /Set-Cookie:\s*(.*)/i.match(header)
|
43
|
+
@cookies.parse(matcher[1], uri) if matcher
|
44
|
+
end
|
45
|
+
|
46
|
+
block.call(response, duration) if block
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Crusher
|
2
|
+
|
3
|
+
class LoadGenerator
|
4
|
+
|
5
|
+
def initialize(crush_session, options)
|
6
|
+
@crush_session = crush_session
|
7
|
+
@options = options
|
8
|
+
begin
|
9
|
+
@log_file = File.open(options[:log_file], 'a') if options[:log_file]
|
10
|
+
rescue Errno::ENOENT
|
11
|
+
log("Log file #{options[:log_file]} couldn't be opened; logging to STDOUT")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def act!; end
|
16
|
+
|
17
|
+
def prepare; end
|
18
|
+
|
19
|
+
def log(message)
|
20
|
+
@crush_session.log(message)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/lib/crusher/util.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module Crusher::Util
|
2
|
+
DUMMY_WORDS = %w( Vivamus condimentum mi vel massa blandit ut pretium risus cursus Pellentesque eu mollis turpis Cum sociis natoque penatibus et magnis dis parturient montes nascetur ridiculus mus Phasellus lobortis bibendum lectus Suspendisse suscipit sem in arcu tempor at malesuada lectus facilisis Sed euismod pretium gravida Ut feugiat tortor a erat mattis a eleifend est rutrum Ut est elit accumsan ut lacinia ac dignissim tincidunt elit Duis nisi mauris ornare eget laoreet vel euismod at sem Donec in neque nec odio ornare vestibulum Pellentesque ut turpis erat nec sollicitudin nulla Nulla varius fringilla porta Fusce sit amet orci eget sem iaculis porttitor Proin pulvinar sodales odio in dapibus erat viverra in Donec fringilla lacus et lacus tincidunt ut laoreet dolor tristique Nullam vulputate iaculis condimentum Mauris et ligula id erat auctor adipiscing Nulla luctus interdum consequatProin vehicula egestas leo a feugiat velit fermentum quis Morbi mattis dignissim tincidunt Nunc sed dignissim leo Mauris vitae mi sollicitudin eros elementum interdum non quis velit Nam eleifend aliquam lobortis Maecenas in metus non libero porttitor tincidunt vitae molestie augue Lorem ipsum dolor sit amet consectetur adipiscing elit Vestibulum blandit erat ac urna gravida ac aliquam nulla scelerisque Fusce ante purus rhoncus auctor molestie vel tincidunt sit amet orci Sed viverra augue ut quam condimentum dictum Duis ullamcorper elit in erat ullamcorper eget rhoncus velit blandit Morbi ultrices lacus id suscipit tempor turpis dolor placerat ante tempor mollis turpis leo et augue Phasellus augue dolor ultrices luctus rhoncus nec sollicitudin sit amet nequeVestibulum interdum nibh sed mauris vulputate sed tincidunt magna rhoncus Etiam sapien metus ultricies in eleifend a aliquet eget nibh Duis in est nibh eu tincidunt enim Vivamus suscipit felis a velit dignissim et fringilla mi laoreet Duis metus odio porta vitae dictum in rutrum ut quam Donec tristique adipiscing nisl non iaculis Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas Nulla eget pulvinar nulla Cum sociis natoque penatibus et magnis dis parturient montes nascetur ridiculus mus Donec venenatis justo eu vulputate venenatis leo ante varius enim ac laoreet lacus elit id ipsum Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas Etiam euismod lorem quis faucibus vehicula nisi lorem mattis felis in imperdiet urna magna non dolor Aenean sed eros eu odio pretium blandit In hac habitasse platea dictumst Vestibulum nisi orci malesuada eu adipiscing eget posuere nec orci Curabitur bibendum orci non felis faucibus lobortisSuspendisse imperdiet pellentesque magna sit amet commodo odio hendrerit vitae Duis dapibus tempor nunc interdum vulputate Curabitur fringilla erat ut magna viverra pulvinar sed eu nunc Phasellus sit amet aliquam nulla Donec aliquam nibh in ullamcorper dictum nibh risus pellentesque nunc eu laoreet nulla elit sed justo Donec blandit justo eu mi ultricies eu tempus turpis fringilla Proin ligula elit varius vel egestas et hendrerit sed metus Phasellus nec tortor nunc eu auctor quam Nulla quis ultricies felis Proin iaculis felis non ultrices )
|
3
|
+
|
4
|
+
srand Time.now.seconds # Seeds rng on boot
|
5
|
+
|
6
|
+
def self.nrand(min, max)
|
7
|
+
(1..6).map{ rand(min,max) }.sum / 6
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.lipsum(words)
|
11
|
+
(1..words).map{ DUMMY_WORDS[ rand(DUMMY_WORDS.length) ] }.join(' ')
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/lib/crusher.rb
ADDED
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: crusher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- phene
|
14
|
+
- thatothermitch
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-11-08 00:00:00 -08:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: httpclient
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: crusher-eventmachine
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description: A scriptable, multiprocessing load generator designed to bring web applications to their knees
|
51
|
+
email:
|
52
|
+
executables:
|
53
|
+
- crush
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- lib/crusher/configurator.rb
|
60
|
+
- lib/crusher/cookie.rb
|
61
|
+
- lib/crusher/crush_session.rb
|
62
|
+
- lib/crusher/http_load_generator.rb
|
63
|
+
- lib/crusher/load_generator.rb
|
64
|
+
- lib/crusher/util.rb
|
65
|
+
- lib/crusher.rb
|
66
|
+
- bin/crush
|
67
|
+
- Rakefile
|
68
|
+
has_rdoc: false
|
69
|
+
homepage: http://github.com/socialcast/crusher
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
requirements: []
|
96
|
+
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.3.7
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: A scriptable load generator
|
102
|
+
test_files: []
|
103
|
+
|