arachni 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +1 -9
- data/bin/arachni_script +1 -1
- data/components/checks/active/xss_dom_script_context.rb +1 -1
- data/components/checks/active/xss_event.rb +1 -1
- data/components/checks/active/xss_script_context.rb +1 -1
- data/components/plugins/autologin.rb +2 -2
- data/components/plugins/content_types.rb +4 -5
- data/components/plugins/cookie_collector.rb +6 -3
- data/components/plugins/uncommon_headers.rb +6 -2
- data/lib/arachni/browser.rb +26 -2
- data/lib/arachni/browser/element_locator.rb +9 -2
- data/lib/arachni/browser/javascript.rb +6 -0
- data/lib/arachni/browser/javascript/scripts/dom_monitor.js +39 -0
- data/lib/arachni/browser_cluster.rb +11 -25
- data/lib/arachni/element/capabilities/analyzable/differential.rb +4 -0
- data/lib/arachni/element/capabilities/analyzable/timeout.rb +4 -0
- data/lib/arachni/element/capabilities/auditable/dom.rb +1 -0
- data/lib/arachni/element/capabilities/mutable.rb +0 -9
- data/lib/arachni/element/capabilities/with_auditor/output.rb +2 -0
- data/lib/arachni/element/cookie.rb +9 -4
- data/lib/arachni/element/form.rb +6 -6
- data/lib/arachni/element/header.rb +1 -1
- data/lib/arachni/framework.rb +1 -0
- data/lib/arachni/http/client.rb +1 -0
- data/lib/arachni/option_groups.rb +3 -0
- data/lib/arachni/option_groups/paths.rb +63 -6
- data/lib/arachni/option_groups/snapshot.rb +4 -0
- data/lib/arachni/session.rb +73 -17
- data/lib/arachni/state/audit.rb +2 -0
- data/lib/version +1 -1
- data/spec/arachni/browser/javascript_spec.rb +20 -0
- data/spec/arachni/browser_spec.rb +51 -0
- data/spec/arachni/element/cookie_spec.rb +22 -1
- data/spec/arachni/element/form_spec.rb +19 -9
- data/spec/arachni/framework_spec.rb +17 -0
- data/spec/arachni/option_groups/paths_spec.rb +109 -8
- data/spec/arachni/option_groups/snapshot_spec.rb +17 -0
- data/spec/arachni/session_spec.rb +54 -26
- data/spec/components/plugins/autologin_spec.rb +59 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/support/factories/element/body.rb +3 -0
- data/spec/support/factories/element/generic_dom.rb +6 -0
- data/spec/support/factories/element/path.rb +3 -0
- data/spec/support/factories/element/server.rb +3 -0
- data/spec/support/factories/page/dom/transition.rb +21 -0
- data/spec/support/helpers/resets.rb +1 -0
- data/spec/support/servers/arachni/browser/javascript/dom_monitor.rb +15 -0
- data/ui/cli/framework.rb +5 -0
- data/ui/cli/framework/option_parser.rb +1 -1
- data/ui/cli/option_parser.rb +3 -0
- data/ui/cli/output.rb +45 -19
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe4bb5082b1faba59faf683145dd95841b0299a2
|
4
|
+
data.tar.gz: 83374892882e248fb3334b7651c7f166c99c3bc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: def0427f488fd60f1aed48b0a38d2cb3e3deff96c3270305f27a5a7e2e9a00d01074716b492169ef6e1f45984ac84f89ef39e81b1d155791b73aa560c40f5e33
|
7
|
+
data.tar.gz: 0edb331de9533d6f5b9a63d3fdcb3f4a7f64697ce6f0f3a98f610a53d41629037d50c01cee7037d28afa9b2489edaa82fd97dee4311c9c4188f4336e78815600
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,62 @@
|
|
1
1
|
# ChangeLog
|
2
2
|
|
3
|
+
## 1.0.3 _(October 3, 2014)_
|
4
|
+
|
5
|
+
- Added overrides for system write directories in `config/write_paths.yml`.
|
6
|
+
- `OptionGroups`
|
7
|
+
- `Paths`
|
8
|
+
- Added `.config` -- Parsing `config/write_paths.yml`.
|
9
|
+
- `#logs` -- Can now be set via `.config`.
|
10
|
+
- `#snapshots` -- Can now be set via `.config`.
|
11
|
+
- `Snapshot`
|
12
|
+
- `#save_path` -- Can now be set via `Paths.config`.
|
13
|
+
- `UI::Output`
|
14
|
+
- Moved default error log under `OptionGroups::Paths.logs`.
|
15
|
+
- Optimized file descriptor handling.
|
16
|
+
- `UI::CLI`
|
17
|
+
- `OptionParser`
|
18
|
+
- Set default report location save-dir from `OptionGroups::Paths.config`.
|
19
|
+
- `Framework`
|
20
|
+
- Print the error-log location at the end of the scan if there were errors.
|
21
|
+
- `Framework`
|
22
|
+
- Use `OptionGroups::Scope#extend_paths` to seed the crawl.
|
23
|
+
- `Browser`
|
24
|
+
- `ElementLocator.supported_element_attributes_for`
|
25
|
+
- Fixed `nil`-error when dealing with unknown attributes.
|
26
|
+
- Added `:ignore_scope` option, allowing the browser to roam completely
|
27
|
+
unrestricted.
|
28
|
+
- Capped `setTimeout` waiting period to `OptionGroups::HTTP#request_timeout`.
|
29
|
+
- Fixed issue resulting in multiple cookies with the same name being sent
|
30
|
+
to the web application.
|
31
|
+
- Assigned unique custom IDs to DOM elements without ID attributes.
|
32
|
+
- `BrowserCluster`
|
33
|
+
- Spawn browsers in series instead of in parallel to make it easier on
|
34
|
+
low resource systems.
|
35
|
+
- `Session`
|
36
|
+
- Fallback to `Framework` DOM Level 1 handlers when no `Browser` is available.
|
37
|
+
- When `OptionGroups::Scope#dom_depth_limit` is 0 don't use the `Browser`.
|
38
|
+
- Configured its `Browser` with `:ignore_scope` to allow for SSO support.
|
39
|
+
- `#logged_in?` -- Follow redirections for login check HTTP request.
|
40
|
+
- `Element`
|
41
|
+
- `Cookie`
|
42
|
+
- Added `#data` -- Providing access to raw cookie data.
|
43
|
+
- `Capabilities`
|
44
|
+
- `Analyzable`
|
45
|
+
- `Differential` -- Forcibly disable `OptionGroups::Audit#cookies_extensively`.
|
46
|
+
- `Timeout` -- Forcibly disable `OptionGroups::Audit#cookies_extensively`.
|
47
|
+
- `Mutable`
|
48
|
+
- `#each_mutation` -- Removed obsolete method-switch with default inputs.
|
49
|
+
- Plugins
|
50
|
+
- `uncommon_headers`
|
51
|
+
- Added `keep-alive` and `content-disposition` in the common list.
|
52
|
+
- Ignore out-of-scope responses.
|
53
|
+
- `content_types`
|
54
|
+
- Ignore out-of-scope responses.
|
55
|
+
- `cookie_collector`
|
56
|
+
- `Set-Cookie` header is now always an `Array`.
|
57
|
+
- `autologin`
|
58
|
+
- Don't modify `OptionGroups::Session` (login-check) options if already set.
|
59
|
+
|
3
60
|
## 1.0.2 _(September 13, 2014)_
|
4
61
|
|
5
62
|
- `UI::Output` -- Updated null output interface with placeholder debugging methods.
|
data/README.md
CHANGED
@@ -1,17 +1,9 @@
|
|
1
|
-
**NOTICE**:
|
2
|
-
|
3
|
-
* Arachni's license has changed, please see the _LICENSE_ file before working
|
4
|
-
with the project.
|
5
|
-
* v1.0 is not backwards compatible with v0.4.
|
6
|
-
|
7
|
-
<hr/>
|
8
|
-
|
9
1
|
# Arachni - Web Application Security Scanner Framework
|
10
2
|
|
11
3
|
<table>
|
12
4
|
<tr>
|
13
5
|
<th>Version</th>
|
14
|
-
<td>1.0.
|
6
|
+
<td>1.0.3</td>
|
15
7
|
</tr>
|
16
8
|
<tr>
|
17
9
|
<th>Homepage</th>
|
data/bin/arachni_script
CHANGED
@@ -68,7 +68,7 @@ Injects JS taint code and checks to see if it gets executed as proof of vulnerab
|
|
68
68
|
version: '0.1',
|
69
69
|
|
70
70
|
issue: {
|
71
|
-
name: %q{DOM-based Cross-Site Scripting (XSS)},
|
71
|
+
name: %q{DOM-based Cross-Site Scripting (XSS) in script context},
|
72
72
|
description: %q{
|
73
73
|
Client-side scripts are used extensively by modern web applications.
|
74
74
|
They perform from simple functions (such as the formatting of text) up to full
|
@@ -94,7 +94,7 @@ class Arachni::Checks::XssEvent < Arachni::Check::Base
|
|
94
94
|
version: '0.1.5',
|
95
95
|
|
96
96
|
issue: {
|
97
|
-
name: %q{Cross-Site Scripting in event tag of HTML element},
|
97
|
+
name: %q{Cross-Site Scripting (XSS) in event tag of HTML element},
|
98
98
|
description: %q{
|
99
99
|
Client-side scripts are used extensively by modern web applications.
|
100
100
|
They perform from simple functions (such as the formatting of text) up to full
|
@@ -139,7 +139,7 @@ Injects JS taint code and check to see if it gets executed as proof of vulnerabi
|
|
139
139
|
version: '0.2',
|
140
140
|
|
141
141
|
issue: {
|
142
|
-
name: %q{Cross-Site Scripting in
|
142
|
+
name: %q{Cross-Site Scripting (XSS) in script context},
|
143
143
|
description: %q{
|
144
144
|
Client-side scripts are used extensively by modern web applications.
|
145
145
|
They perform from simple functions (such as the formatting of text) up to full
|
@@ -48,8 +48,8 @@ class Arachni::Plugins::AutoLogin < Arachni::Plugin::Base
|
|
48
48
|
return
|
49
49
|
end
|
50
50
|
|
51
|
-
framework.options.session.check_url
|
52
|
-
framework.options.session.check_pattern
|
51
|
+
framework.options.session.check_url ||= response.url
|
52
|
+
framework.options.session.check_pattern ||= @verifier
|
53
53
|
|
54
54
|
if !session.logged_in?
|
55
55
|
register_results(
|
@@ -9,7 +9,6 @@
|
|
9
9
|
# Logs content-types of all server responses.
|
10
10
|
#
|
11
11
|
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
12
|
-
# @version 0.1.6
|
13
12
|
class Arachni::Plugins::ContentTypes < Arachni::Plugin::Base
|
14
13
|
|
15
14
|
is_distributable
|
@@ -29,7 +28,7 @@ class Arachni::Plugins::ContentTypes < Arachni::Plugin::Base
|
|
29
28
|
end
|
30
29
|
|
31
30
|
def run
|
32
|
-
|
31
|
+
http.on_complete do |response|
|
33
32
|
next if skip?( response )
|
34
33
|
|
35
34
|
type = response.headers.content_type
|
@@ -47,8 +46,8 @@ class Arachni::Plugins::ContentTypes < Arachni::Plugin::Base
|
|
47
46
|
end
|
48
47
|
|
49
48
|
def skip?( response )
|
50
|
-
logged?( response ) ||
|
51
|
-
!log?( response )
|
49
|
+
response.scope.out? || logged?( response ) ||
|
50
|
+
response.headers.content_type.to_s.empty? || !log?( response )
|
52
51
|
end
|
53
52
|
|
54
53
|
def log?( response )
|
@@ -97,7 +96,7 @@ It can help you categorize and identify publicly available file-types which in
|
|
97
96
|
turn can help you identify accidentally leaked files.
|
98
97
|
},
|
99
98
|
author: 'Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>',
|
100
|
-
version: '0.1.
|
99
|
+
version: '0.1.7',
|
101
100
|
options: [
|
102
101
|
Options::String.new( :exclude,
|
103
102
|
description: 'Exclude content-types that match this regular expression.',
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# Simple cookie collector
|
10
10
|
#
|
11
11
|
# @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
|
12
|
-
# @version 0.2
|
12
|
+
# @version 0.2.1
|
13
13
|
class Arachni::Plugins::CookieCollector < Arachni::Plugin::Base
|
14
14
|
|
15
15
|
is_distributable
|
@@ -42,9 +42,12 @@ class Arachni::Plugins::CookieCollector < Arachni::Plugin::Base
|
|
42
42
|
response_hash.delete( :body )
|
43
43
|
response_hash.delete( :headers_string )
|
44
44
|
|
45
|
+
r_h = response_hash.my_stringify_keys
|
46
|
+
r_h['headers']['Set-Cookie'] = [r_h['headers']['Set-Cookie']].flatten.compact
|
47
|
+
|
45
48
|
@cookies << {
|
46
49
|
'time' => Time.now.to_s,
|
47
|
-
'response' =>
|
50
|
+
'response' => r_h,
|
48
51
|
'cookies' => cookies
|
49
52
|
}
|
50
53
|
end
|
@@ -81,7 +84,7 @@ thousands of results leading to a huge report, highly increased memory
|
|
81
84
|
consumption and CPU usage.
|
82
85
|
},
|
83
86
|
author: 'Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>',
|
84
|
-
version: '0.2',
|
87
|
+
version: '0.2.1',
|
85
88
|
options: [
|
86
89
|
Options::String.new( :filter,
|
87
90
|
description: 'Pattern to use to determine which cookies to ' +
|
@@ -32,7 +32,9 @@ class Arachni::Plugins::UncommonHeaders < Arachni::Plugin::Base
|
|
32
32
|
'proxy-authenticate',
|
33
33
|
'set-cookie',
|
34
34
|
'trailer',
|
35
|
-
'transfer-encoding'
|
35
|
+
'transfer-encoding',
|
36
|
+
'keep-alive',
|
37
|
+
'content-disposition'
|
36
38
|
])
|
37
39
|
|
38
40
|
def prepare
|
@@ -52,6 +54,8 @@ class Arachni::Plugins::UncommonHeaders < Arachni::Plugin::Base
|
|
52
54
|
|
53
55
|
def run
|
54
56
|
http.on_complete do |response|
|
57
|
+
next if response.scope.out?
|
58
|
+
|
55
59
|
headers = response.headers.
|
56
60
|
select { |name, _| !COMMON.include?( name.to_s.downcase ) }
|
57
61
|
next if headers.empty?
|
@@ -85,7 +89,7 @@ class Arachni::Plugins::UncommonHeaders < Arachni::Plugin::Base
|
|
85
89
|
name: 'Uncommon headers',
|
86
90
|
description: %q{Intercepts HTTP responses and logs uncommon headers.},
|
87
91
|
author: 'Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>',
|
88
|
-
version: '0.1.
|
92
|
+
version: '0.1.3'
|
89
93
|
}
|
90
94
|
end
|
91
95
|
|
data/lib/arachni/browser.rb
CHANGED
@@ -126,6 +126,8 @@ class Browser
|
|
126
126
|
super()
|
127
127
|
@options = options.dup
|
128
128
|
|
129
|
+
@ignore_scope = options[:ignore_scope]
|
130
|
+
|
129
131
|
@width = options[:width] || 1600
|
130
132
|
@height = options[:height] || 1200
|
131
133
|
|
@@ -310,6 +312,8 @@ class Browser
|
|
310
312
|
wait_for_timers
|
311
313
|
|
312
314
|
wait_for_pending_requests
|
315
|
+
|
316
|
+
javascript.set_element_ids
|
313
317
|
end
|
314
318
|
|
315
319
|
if @add_request_transitions
|
@@ -828,7 +832,12 @@ class Browser
|
|
828
832
|
def wait_for_timers
|
829
833
|
delay = load_delay
|
830
834
|
return if !delay
|
831
|
-
|
835
|
+
|
836
|
+
sleep [Options.http.request_timeout, delay].min / 1000.0
|
837
|
+
end
|
838
|
+
|
839
|
+
def skip_path?( path )
|
840
|
+
enforce_scope? && super( path )
|
832
841
|
end
|
833
842
|
|
834
843
|
def response
|
@@ -998,6 +1007,13 @@ class Browser
|
|
998
1007
|
kill_process
|
999
1008
|
end
|
1000
1009
|
|
1010
|
+
# Something went really bad, the browser couldn't be spawned even
|
1011
|
+
# after our valiant efforts.
|
1012
|
+
#
|
1013
|
+
# Bail out for now and count on the BrowserCluster to retry to boot
|
1014
|
+
# another process ass needed.
|
1015
|
+
return if !@process
|
1016
|
+
|
1001
1017
|
begin
|
1002
1018
|
@pid = @process.pid
|
1003
1019
|
# Not supported on JRuby on MS Windows.
|
@@ -1077,6 +1093,8 @@ class Browser
|
|
1077
1093
|
|
1078
1094
|
set_cookies = {}
|
1079
1095
|
HTTP::Client.cookie_jar.for_url( url ).each do |cookie|
|
1096
|
+
cookie = cookie.dup
|
1097
|
+
cookie.data.delete :domain
|
1080
1098
|
set_cookies[cookie.name] = cookie
|
1081
1099
|
end
|
1082
1100
|
cookies.each do |name, value|
|
@@ -1181,7 +1199,7 @@ class Browser
|
|
1181
1199
|
transition.complete
|
1182
1200
|
end
|
1183
1201
|
|
1184
|
-
return if response.scope.out?
|
1202
|
+
return if enforce_scope? && response.scope.out?
|
1185
1203
|
|
1186
1204
|
intercept response
|
1187
1205
|
save_response response
|
@@ -1203,6 +1221,8 @@ class Browser
|
|
1203
1221
|
end
|
1204
1222
|
|
1205
1223
|
def ignore_request?( request )
|
1224
|
+
return if !enforce_scope?
|
1225
|
+
|
1206
1226
|
# Only allow CSS and JS resources to be loaded from out-of-scope domains.
|
1207
1227
|
!['css', 'js'].include?( request.parsed_url.resource_extension ) &&
|
1208
1228
|
request.scope.out? || request.scope.redundant?
|
@@ -1304,6 +1324,10 @@ class Browser
|
|
1304
1324
|
end
|
1305
1325
|
end
|
1306
1326
|
|
1327
|
+
def enforce_scope?
|
1328
|
+
!@ignore_scope
|
1329
|
+
end
|
1330
|
+
|
1307
1331
|
def normalize_watir_url( url )
|
1308
1332
|
normalize_url( ::URI.encode( url, ';' ) ).gsub( '%3B', '%253B' )
|
1309
1333
|
end
|
@@ -127,8 +127,15 @@ class ElementLocator
|
|
127
127
|
# List of attributes supported by Watir.
|
128
128
|
def self.supported_element_attributes_for( tag_name )
|
129
129
|
@supported_element_attributes_for ||= {}
|
130
|
-
|
131
|
-
|
130
|
+
|
131
|
+
tag_name = tag_name.to_sym
|
132
|
+
|
133
|
+
if (klass = Watir.tag_to_class[tag_name])
|
134
|
+
@supported_element_attributes_for[tag_name] ||=
|
135
|
+
Set.new( klass.attribute_list )
|
136
|
+
else
|
137
|
+
@supported_element_attributes_for[tag_name] ||= Set.new
|
138
|
+
end
|
132
139
|
end
|
133
140
|
|
134
141
|
end
|
@@ -228,6 +228,12 @@ class Javascript
|
|
228
228
|
taint_tracer.flush_data_flow_sinks
|
229
229
|
end
|
230
230
|
|
231
|
+
# Sets a custom ID attribute to elements with events but without a proper ID.
|
232
|
+
def set_element_ids
|
233
|
+
return '' if !supported?
|
234
|
+
dom_monitor.setElementIds
|
235
|
+
end
|
236
|
+
|
231
237
|
# @return [String]
|
232
238
|
# Digest of the current DOM tree (i.e. node names and their attributes
|
233
239
|
# without text-nodes).
|
@@ -142,5 +142,44 @@ var _tokenDOMMonitor = _tokenDOMMonitor || {
|
|
142
142
|
registerEvent: function ( element, event, handler ) {
|
143
143
|
if( !('events' in element) ) element['events'] = [];
|
144
144
|
element['events'].push( [event, handler] );
|
145
|
+
},
|
146
|
+
|
147
|
+
// Sets a unique enough custom ID attribute to elements that lack proper IDs.
|
148
|
+
// This gets called externally (by the Browser) once the page is settled.
|
149
|
+
setElementIds: function() {
|
150
|
+
var elements = document.getElementsByTagName("*");
|
151
|
+
var length = elements.length;
|
152
|
+
|
153
|
+
for( var i = 0; i < length; i++ ) {
|
154
|
+
var element = elements[i];
|
155
|
+
|
156
|
+
// Window and others don't have attributes.
|
157
|
+
if( typeof( element.getAttribute ) !== 'function' ||
|
158
|
+
typeof( element.setAttribute) !== 'function' ) continue;
|
159
|
+
|
160
|
+
// If the element has an ID we're cool, move on.
|
161
|
+
if( element.getAttribute('id') ) continue;
|
162
|
+
|
163
|
+
// Skip invisible elements.
|
164
|
+
if( element.offsetWidth <= 0 && element.offsetHeight <= 0 ) continue;
|
165
|
+
|
166
|
+
// We don't care about elements without events.
|
167
|
+
if( !element.events || element.events.length == 0 ) continue;
|
168
|
+
|
169
|
+
element.setAttribute( 'data-arachni-id', _tokenDOMMonitor.hashCode( element.innerHTML ) );
|
170
|
+
}
|
171
|
+
},
|
172
|
+
|
173
|
+
hashCode: function( str ) {
|
174
|
+
var hash = 0;
|
175
|
+
if( str.length == 0 ) return hash;
|
176
|
+
|
177
|
+
for( var i = 0; i < str.length; i++ ) {
|
178
|
+
var char = str.charCodeAt( i );
|
179
|
+
hash = ((hash << 5) - hash) + char;
|
180
|
+
hash = hash & hash; // Convert to 32bit integer
|
181
|
+
}
|
182
|
+
|
183
|
+
return hash;
|
145
184
|
}
|
146
185
|
};
|
@@ -384,32 +384,18 @@ class BrowserCluster
|
|
384
384
|
def initialize_workers
|
385
385
|
print_status "Initializing #{pool_size} browsers..."
|
386
386
|
|
387
|
-
# Calculate maximum HTTP connection concurrency for each worker based on
|
388
|
-
# the HTTP request concurrency setting of the framework.
|
389
|
-
#
|
390
|
-
# Ideally, we'd throttle the collective connections of all browsers
|
391
|
-
# for optimal concurrency, but that would require all browsers sharing
|
392
|
-
# the same proxy which would make things **really** dirty and complicated
|
393
|
-
# so let's avoid that for as long as possible.
|
394
|
-
#concurrency = [(Options.http.request_concurrency / pool_size).to_i, 1].max
|
395
|
-
|
396
387
|
@workers = []
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
pool_size.times do
|
412
|
-
@workers << workers.pop.tap { |b| @consumed_pids << b.pid }
|
388
|
+
pool_size.times do |i|
|
389
|
+
worker = Worker.new(
|
390
|
+
javascript_token: @javascript_token,
|
391
|
+
master: self,
|
392
|
+
width: Options.browser_cluster.screen_width,
|
393
|
+
height: Options.browser_cluster.screen_height
|
394
|
+
)
|
395
|
+
@workers << worker
|
396
|
+
@consumed_pids << worker.pid
|
397
|
+
|
398
|
+
print_status "Spawned ##{i+1} with PID #{worker.pid}."
|
413
399
|
end
|
414
400
|
@consumed_pids.compact!
|
415
401
|
|