comp_tree 0.5.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/CHANGES +24 -0
  2. data/README +19 -52
  3. data/Rakefile +1 -138
  4. data/comp_tree.gemspec +33 -30
  5. data/install.rb +3 -3
  6. data/lib/comp_tree/algorithm.rb +117 -156
  7. data/lib/comp_tree/driver.rb +39 -154
  8. data/lib/comp_tree/error.rb +18 -23
  9. data/lib/comp_tree/node.rb +46 -50
  10. data/lib/comp_tree.rb +56 -0
  11. data/rakelib/jumpstart/ruby.rb +51 -0
  12. data/{contrib/quix/lib/quix → rakelib/jumpstart}/simple_installer.rb +11 -13
  13. data/test/common.rb +29 -0
  14. data/test/test_basic.rb +189 -0
  15. data/test/test_circular.rb +34 -31
  16. data/test/test_drain.rb +38 -0
  17. data/test/test_exception.rb +37 -86
  18. data/test/test_flood.rb +14 -0
  19. data/test/test_grind.rb +77 -0
  20. data/test/test_sequential.rb +21 -0
  21. metadata +45 -58
  22. data/contrib/quix/Rakefile +0 -16
  23. data/contrib/quix/install.rb +0 -3
  24. data/contrib/quix/lib/quix/builtin/dir/casefold_brackets.rb +0 -7
  25. data/contrib/quix/lib/quix/builtin/kernel/tap.rb +0 -9
  26. data/contrib/quix/lib/quix/builtin/module/include.rb +0 -21
  27. data/contrib/quix/lib/quix/builtin/module/private.rb +0 -41
  28. data/contrib/quix/lib/quix/config.rb +0 -37
  29. data/contrib/quix/lib/quix/cygwin.rb +0 -60
  30. data/contrib/quix/lib/quix/diagnostic.rb +0 -44
  31. data/contrib/quix/lib/quix/enumerable.rb +0 -33
  32. data/contrib/quix/lib/quix/fileutils.rb +0 -37
  33. data/contrib/quix/lib/quix/hash_struct.rb +0 -27
  34. data/contrib/quix/lib/quix/kernel.rb +0 -61
  35. data/contrib/quix/lib/quix/lazy_struct.rb +0 -55
  36. data/contrib/quix/lib/quix/string.rb +0 -38
  37. data/contrib/quix/lib/quix/subpackager.rb +0 -52
  38. data/contrib/quix/lib/quix/thread_local.rb +0 -32
  39. data/contrib/quix/lib/quix/vars.rb +0 -138
  40. data/contrib/quix/lib/quix.rb +0 -32
  41. data/contrib/quix/test/all.rb +0 -12
  42. data/contrib/quix/test/test_deps.rb +0 -25
  43. data/contrib/quix/test/test_include.rb +0 -47
  44. data/contrib/quix/test/test_private.rb +0 -86
  45. data/contrib/quix/test/test_root.rb +0 -19
  46. data/contrib/quix/test/test_struct.rb +0 -48
  47. data/contrib/quix/test/test_vars.rb +0 -187
  48. data/lib/comp_tree/bucket_ipc.rb +0 -151
  49. data/lib/comp_tree/diagnostic.rb +0 -44
  50. data/lib/comp_tree/misc.rb +0 -61
  51. data/lib/comp_tree/retriable_fork.rb +0 -42
  52. data/lib/comp_tree/tap.rb +0 -9
  53. data/lib/comp_tree/task_node.rb +0 -22
  54. data/test/all.rb +0 -12
  55. data/test/test_bucketipc.rb +0 -72
  56. data/test/test_comp_tree.rb +0 -364
data/CHANGES ADDED
@@ -0,0 +1,24 @@
1
+
2
+ = CompTree ChangeLog
3
+
4
+ == Version 0.7.0
5
+
6
+ * remove fork and discard_result options
7
+ * remove contrib/
8
+ * remove block arg to CompTree.new; use CompTree.build
9
+ * remove CompTree::Error nesting
10
+ * Driver#compute accepts integer as second option
11
+ * remove method_missing and eval tricks for node definitions;
12
+ new project 'pure' makes this obsolete
13
+ * split up tests
14
+ * allow non-symbols for node IDs
15
+ * check_circular now returns the loop instead of raising
16
+ * allow computation result of nil
17
+
18
+ == Version 0.5.2
19
+
20
+ * internal project cleanup (no relevant code changes).
21
+
22
+ == Version 0.5.0
23
+
24
+ * Initial release.
data/README CHANGED
@@ -5,71 +5,38 @@
5
5
 
6
6
  require 'comp_tree'
7
7
 
8
- CompTree::Driver.new { |driver|
8
+ CompTree.build do |driver|
9
9
 
10
- # Define a function named 'area' taking these three arguments.
11
- driver.define_area(:width, :height, :offset) { |width, height, offset|
12
- width*height - offset
10
+ # Define a function named 'area' taking these two arguments.
11
+ driver.define(:area, :width, :height) { |width, height|
12
+ width*height
13
13
  }
14
14
 
15
15
  # Define a function 'width' which takes a 'border' argument.
16
- driver.define_width(:border) { |border|
17
- 2 + border
16
+ driver.define(:width, :border) { |border|
17
+ 7 + border
18
18
  }
19
19
 
20
20
  # Ditto for 'height'.
21
- driver.define_height(:border) { |border|
22
- 3 + border
21
+ driver.define(:height, :border) { |border|
22
+ 5 + border
23
23
  }
24
24
 
25
25
  # Define a constant function 'border'.
26
- driver.define_border {
27
- 5
28
- }
29
-
30
- # Ditto for 'offset'.
31
- driver.define_offset {
32
- 7
26
+ driver.define(:border) {
27
+ 2
33
28
  }
34
29
 
35
30
  # Compute the area using four parallel threads.
36
- area = driver.compute(:area, :threads => 4)
31
+ puts driver.compute(:area, :threads => 4)
32
+ # => 63
37
33
 
38
34
  # We've done this computation.
39
- if area == (2 + 5)*(3 + 5) - 7
40
- puts "It worked!"
41
- else
42
- puts "Send bug report to ..."
43
- end
44
- }
35
+ puts((7 + 2)*(5 + 2))
36
+ # => 63
37
+ end
45
38
 
46
- === Alternative Forms for Function Definitions
47
-
48
- This form evals a lambda, saving you the repeat parameter list:
49
-
50
- driver.define_area :width, :height, :offset, %{
51
- width*height - offset
52
- }
53
-
54
- driver.define_width :border, %{
55
- 2 + border
56
- }
57
-
58
- Notice the '<code>%</code>' before the brace. The lambda is created
59
- just once, during the time of definition.
60
-
61
- Finally there is the raw form which uses no +eval+ or
62
- +method_missing+ tricks:
63
-
64
- driver.define(:area, :width, :height, :offset) { |width, height, offset|
65
- width*height - offset
66
- }
67
-
68
- driver.define(:width, :border) { |border|
69
- 2 + border
70
- }
71
-
72
- == Important Notes
39
+ == Notes
73
40
 
74
41
  The user should have a basic understanding of <em>functional
75
42
  programming</em> (see for example
@@ -82,7 +49,7 @@ function you define must explicitly depend on the data it uses.
82
49
  #
83
50
  # BAD example: depending on state -- offset not listed as a parameter
84
51
  #
85
- driver.define_area(:width, :height) { |width, height|
52
+ driver.define(:area, :width, :height) { |width, height|
86
53
  width*height - offset
87
54
  }
88
55
 
@@ -96,9 +63,9 @@ to affect a state (to produce a <em>side effect</em>).
96
63
  #
97
64
  # BAD example: affecting state
98
65
  #
99
- driver.define_area(:width, :height, :offset) { |width, height, offset|
66
+ driver.define(:area, :width, :height) { |width, height|
100
67
  ACCUMULATOR.add "more data"
101
- width*height - offset
68
+ width*height
102
69
  }
103
70
 
104
71
  Given a tree where nodes are modifying _ACCUMULATOR_, the end state of
data/Rakefile CHANGED
@@ -1,140 +1,3 @@
1
- $LOAD_PATH.unshift "contrib/quix/lib"
2
1
 
3
- require 'rake/gempackagetask'
4
- require 'rake/contrib/rubyforgepublisher'
2
+ # rakelib generates tasks
5
3
 
6
- $VERBOSE = nil
7
- require 'rdoc/rdoc'
8
- $VERBOSE = true
9
-
10
- require 'fileutils'
11
- include FileUtils
12
-
13
- GEMSPEC = eval(File.read("comp_tree.gemspec"))
14
- DOC_DIR = "html"
15
-
16
- ######################################################################
17
- # clean
18
-
19
- task :clean => [:clobber, :clean_doc] do
20
- end
21
-
22
- task :clean_doc do
23
- rm_rf(DOC_DIR)
24
- end
25
-
26
- ######################################################################
27
- # test
28
-
29
- task :test do
30
- require 'test/all'
31
- end
32
-
33
- ######################################################################
34
- # package
35
-
36
- task :package => :clean
37
-
38
- Rake::GemPackageTask.new(GEMSPEC) { |t|
39
- t.need_tar = true
40
- }
41
-
42
- ######################################################################
43
- # doc
44
-
45
- task :doc => :clean_doc do
46
- files = %w(README) + %w(driver error node task_node).map { |name|
47
- "lib/comp_tree/#{name}.rb"
48
- }
49
-
50
- options = [
51
- "-o", DOC_DIR,
52
- "--title", "comp_tree: #{GEMSPEC.summary}",
53
- "--main", "README"
54
- ]
55
-
56
- RDoc::RDoc.new.document(files + options)
57
- end
58
-
59
- ######################################################################
60
- # git
61
-
62
- def git(*args)
63
- cmd = ["git"] + args
64
- sh(*cmd)
65
- end
66
-
67
- task :init_contrib do
68
- unless `git remote`.split.include? "quix"
69
- git(*%w!remote add -f quix git@github.com:quix/quix.git!)
70
- end
71
- end
72
-
73
- task :add_contrib_first_time => :init_contrib do
74
- git(*%w!merge --squash -s ours --no-commit quix/master!)
75
- git(*%w!read-tree --prefix=contrib/quix -u quix/master!)
76
- git("commit", "-m", "add quix utils")
77
- end
78
-
79
- task :run_pull_contrib do
80
- git(*%w!pull --no-commit -s subtree quix master!)
81
- end
82
-
83
- task :pull_contrib => [ :init_contrib, :run_pull_contrib ]
84
-
85
- ######################################################################
86
- # publisher
87
-
88
- task :publish => :doc do
89
- Rake::RubyForgePublisher.new('comptree', 'quix').upload
90
- end
91
-
92
- ######################################################################
93
- # release
94
-
95
- task :prerelease => :clean do
96
- rm_rf(DOC_DIR)
97
- rm_rf("pkg")
98
- unless `git status` =~ %r!nothing to commit \(working directory clean\)!
99
- raise "Directory not clean"
100
- end
101
- unless `ping -c2 github.com` =~ %r!0% packet loss!i
102
- raise "No ping for github.com"
103
- end
104
- end
105
-
106
- def rubyforge(command, file)
107
- sh("rubyforge",
108
- command,
109
- GEMSPEC.rubyforge_project,
110
- GEMSPEC.rubyforge_project,
111
- GEMSPEC.version.to_s,
112
- file)
113
- end
114
-
115
- task :finish_release do
116
- gem, tgz = %w(gem tgz).map { |ext|
117
- "pkg/#{GEMSPEC.name}-#{GEMSPEC.version}.#{ext}"
118
- }
119
- gem_md5, tgz_md5 = [gem, tgz].map { |file|
120
- "#{file}.md5".tap { |md5|
121
- sh("md5sum #{file} > #{md5}")
122
- }
123
- }
124
-
125
- rubyforge("add_release", gem)
126
- rubyforge("add_file", gem_md5)
127
- rubyforge("add_file", tgz)
128
- rubyforge("add_file", tgz_md5)
129
-
130
- git("tag", "comp_tree-" + GEMSPEC.version.to_s)
131
- git(*%w(push --tags origin master))
132
- end
133
-
134
- task :release =>
135
- [
136
- :prerelease,
137
- :package,
138
- :publish,
139
- :finish_release,
140
- ]
data/comp_tree.gemspec CHANGED
@@ -1,38 +1,41 @@
1
1
 
2
- Gem::Specification.new { |t|
3
- t.author = "James M. Lawrence"
4
- t.email = "quixoticsycophant@gmail.com"
5
- t.summary = "Parallel Computation Tree"
6
- t.name = "comp_tree"
7
- t.rubyforge_project = "comptree"
8
- t.homepage = "comptree.rubyforge.org"
9
- t.version = "0.5.2"
10
- t.description = "Build a computation tree and execute it with N " +
11
- "parallel threads. Optionally fork computation nodes into new processes."
2
+ Gem::Specification.new { |g|
3
+ g.author = "James M. Lawrence"
4
+ g.email = "quixoticsycophant@gmail.com"
5
+ g.summary = "Parallel Computation Tree"
6
+ g.name = "comp_tree"
7
+ g.rubyforge_project = "comptree"
8
+ g.homepage = "comptree.rubyforge.org"
9
+ g.version = "0.7.0"
10
+ g.description =
11
+ "Build a computation tree and execute it with N parallel threads."
12
12
 
13
- t.files = %w{README comp_tree.gemspec} +
14
- Dir["./**/*.rb"] +
15
- Dir["./**/Rakefile"]
13
+ readme = "README"
16
14
 
17
- rdoc_exclude = %w{
18
- test
19
- contrib
20
- install
21
- quix
22
- fork
23
- diagnostic
24
- algorithm
25
- bucket
26
- comp_tree\.rb
15
+ g.files = %W[
16
+ CHANGES
17
+ #{readme}
18
+ Rakefile
19
+ #{g.name}.gemspec
20
+ install.rb
21
+ ] + %w[lib rakelib test].inject(Array.new) { |acc, dir|
22
+ acc + Dir[dir + "/**/*.rb"]
27
23
  }
28
- t.has_rdoc = true
29
- t.extra_rdoc_files = %w{README}
30
- t.rdoc_options += [
24
+ g.has_rdoc = true
25
+ rdoc_files = [
26
+ readme,
27
+ "lib/comp_tree.rb",
28
+ "lib/comp_tree/driver.rb",
29
+ "lib/comp_tree/error.rb"
30
+ ]
31
+ g.extra_rdoc_files += [readme]
32
+
33
+ g.rdoc_options += [
31
34
  "--main",
32
- "README",
35
+ readme,
33
36
  "--title",
34
- "comp_tree: #{t.summary}",
35
- ] + rdoc_exclude.inject(Array.new) { |acc, pattern|
36
- acc + ["--exclude", pattern]
37
+ "#{g.name}: #{g.summary}"
38
+ ] + (g.files - rdoc_files).inject(Array.new) { |acc, file|
39
+ acc + ["--exclude", file]
37
40
  }
38
41
  }
data/install.rb CHANGED
@@ -1,3 +1,3 @@
1
- $LOAD_PATH.unshift "./contrib/quix/lib"
2
- require 'quix/simple_installer'
3
- CompTree::SimpleInstaller.new.run
1
+ $LOAD_PATH.unshift "rakelib"
2
+ require 'jumpstart/simple_installer'
3
+ Jumpstart::SimpleInstaller.new.run
@@ -1,210 +1,171 @@
1
1
 
2
- require 'comp_tree/diagnostic'
3
- require 'comp_tree/retriable_fork'
4
-
5
2
  module CompTree
6
3
  module Algorithm
7
- include Diagnostic
4
+ module_function
5
+
6
+ def loop_with(leave, again)
7
+ catch(leave) {
8
+ while true
9
+ catch(again) {
10
+ yield
11
+ }
12
+ end
13
+ }
14
+ end
8
15
 
9
- def compute_multithreaded(root, num_threads, use_fork, buckets)
10
- trace "Computing #{root.name} with #{num_threads} threads"
11
- result = nil
12
- mutex = Mutex.new
16
+ def compute_multithreaded(root, num_threads)
17
+ #trace "Computing #{root.name} with #{num_threads} threads"
18
+ finished = nil
19
+ tree_mutex = Mutex.new
13
20
  node_finished_condition = ConditionVariable.new
14
21
  thread_wake_condition = ConditionVariable.new
15
- threads = []
16
-
17
- # workaround: jruby gives "run" status for waiting on
18
- # condition variable
19
- num_threads_ready = 0
20
-
21
- num_threads.times { |thread_index|
22
- threads << Thread.new {
22
+ num_threads_in_use = 0
23
+
24
+ threads = (0...num_threads).map { |thread_index|
25
+ Thread.new {
23
26
  #
24
27
  # wait for main thread
25
28
  #
26
- mutex.synchronize {
27
- trace "Thread #{thread_index} waiting to start"
28
- num_threads_ready += 1
29
- thread_wake_condition.wait(mutex)
29
+ tree_mutex.synchronize {
30
+ #trace "Thread #{thread_index} waiting to start"
31
+ num_threads_in_use += 1
32
+ thread_wake_condition.wait(tree_mutex)
30
33
  }
31
34
 
32
- while true
33
- trace "Thread #{thread_index} node search"
34
-
35
- #
36
- # Done! Thread will exit.
37
- #
38
- break if mutex.synchronize {
39
- result
40
- }
41
-
42
- #
43
- # Lock the tree and find a node. The node we
44
- # obtain, if any, is already locked.
45
- #
46
- node = mutex.synchronize {
47
- find_node(root)
35
+ loop_with(:leave, :again) {
36
+ node = tree_mutex.synchronize {
37
+ #trace "Thread #{thread_index} acquired tree lock; begin search"
38
+ if finished
39
+ #trace "Thread #{thread_index} detected finish"
40
+ num_threads_in_use -= 1
41
+ throw :leave
42
+ else
43
+ #
44
+ # Find a node. The node we obtain, if any, will be locked.
45
+ #
46
+ node = find_node(root)
47
+ if node
48
+ #trace "Thread #{thread_index} found node #{node.name}"
49
+ node
50
+ else
51
+ #trace "Thread #{thread_index}: no node found; sleeping."
52
+ thread_wake_condition.wait(tree_mutex)
53
+ throw :again
54
+ end
55
+ end
48
56
  }
49
57
 
50
- if node
51
- trace "Thread #{thread_index} found node #{node.name}"
52
-
53
- node_result =
54
- compute_node(
55
- node,
56
- use_fork,
57
- buckets ? buckets[thread_index] : nil)
58
-
59
- mutex.synchronize {
60
- node.result = node_result
61
- }
62
-
58
+ #trace "Thread #{thread_index} computing node"
59
+ #debug {
60
+ # node.trace_compute
61
+ #}
62
+ node.compute
63
+ #trace "Thread #{thread_index} node computed; waiting for tree lock"
64
+
65
+ tree_mutex.synchronize {
66
+ #trace "Thread #{thread_index} acquired tree lock"
67
+ #debug {
68
+ # name = "#{node.name}" + ((node == root) ? " (ROOT NODE)" : "")
69
+ # initial = "Thread #{thread_index} compute result for #{name}: "
70
+ # status = node.computed.is_a?(Exception) ? "error" : "success"
71
+ # trace initial + status
72
+ # trace "Thread #{thread_index} node result: #{node.result}"
73
+ #}
74
+
75
+ if node.computed.is_a? Exception
76
+ #
77
+ # An error occurred; we are done.
78
+ #
79
+ finished = node.computed
80
+ elsif node == root
81
+ #
82
+ # Root node was computed; we are done.
83
+ #
84
+ finished = true
85
+ end
86
+
63
87
  #
64
88
  # remove locks for this node (shared lock and own lock)
65
89
  #
66
- mutex.synchronize {
67
- node.unlock
68
- if node == root
69
- #
70
- # Root node was computed; we are done.
71
- #
72
- trace "Thread #{thread_index} got final answer"
73
- result = root.result
74
- end
75
- node_finished_condition.signal
76
- }
77
- else
78
- trace "Thread #{thread_index}: no node found; sleeping."
79
- mutex.synchronize {
80
- thread_wake_condition.wait(mutex)
81
- }
82
- end
83
- end
84
- trace "Thread #{thread_index} exiting"
90
+ node.unlock
91
+
92
+ #
93
+ # Tell the main thread that another node was computed.
94
+ #
95
+ node_finished_condition.signal
96
+ }
97
+ }
98
+ #trace "Thread #{thread_index} exiting"
85
99
  }
86
100
  }
87
101
 
88
- trace "Main: waiting for threads to launch and block."
89
- while true
90
- break if mutex.synchronize {
91
- num_threads_ready == num_threads
92
- }
102
+ #trace "Main: waiting for threads to launch and block."
103
+ until tree_mutex.synchronize { num_threads_in_use == num_threads }
93
104
  Thread.pass
94
105
  end
95
-
96
- trace "Main: entering main loop"
97
- mutex.synchronize {
98
- while true
99
- trace "Main: waking threads"
106
+
107
+ tree_mutex.synchronize {
108
+ #trace "Main: entering main loop"
109
+ until num_threads_in_use == 0
110
+ #trace "Main: waking threads"
100
111
  thread_wake_condition.broadcast
101
112
 
102
- if result
103
- trace "Main: detected finish."
113
+ if finished
114
+ #trace "Main: detected finish."
104
115
  break
105
116
  end
106
117
 
107
- trace "Main: waiting for a node"
108
- node_finished_condition.wait(mutex)
109
- trace "Main: got a node"
118
+ #trace "Main: waiting for a node"
119
+ node_finished_condition.wait(tree_mutex)
120
+ #trace "Main: got a node"
110
121
  end
111
122
  }
112
123
 
113
- trace "Main: waiting for threads to finish."
114
- catch(:done) {
115
- while true
116
- mutex.synchronize {
117
- throw :done if threads.all? { |thread|
118
- thread.status == false
119
- }
120
- thread_wake_condition.broadcast
121
- }
122
- Thread.pass
123
- end
124
+ #trace "Main: waiting for threads to finish."
125
+ loop_with(:leave, :again) {
126
+ tree_mutex.synchronize {
127
+ if threads.all? { |thread| thread.status == false }
128
+ throw :leave
129
+ end
130
+ thread_wake_condition.broadcast
131
+ }
132
+ Thread.pass
124
133
  }
125
134
 
126
- trace "Main: computation done."
127
- result
135
+ #trace "Main: computation done."
136
+ if finished.is_a? Exception
137
+ raise finished
138
+ else
139
+ root.result
140
+ end
128
141
  end
129
142
 
130
143
  def find_node(node)
131
- # --- only called inside mutex
132
- trace "Looking for a node, starting with #{node.name}"
133
- if node.result
144
+ # --- only called inside shared tree mutex
145
+ #trace "Looking for a node, starting with #{node.name}"
146
+ if node.computed
134
147
  #
135
148
  # already computed
136
149
  #
137
- trace "#{node.name} has been computed"
150
+ #trace "#{node.name} has been computed"
138
151
  nil
139
- elsif node.children_results and node.try_lock
152
+ elsif (children_results = node.find_children_results) and node.try_lock
140
153
  #
141
154
  # Node is not computed and its children are computed;
142
155
  # and we have the lock. Ready to compute.
143
156
  #
157
+ node.children_results = children_results
144
158
  node
145
159
  else
146
160
  #
147
161
  # locked or children not computed; recurse to children
148
162
  #
149
- trace "Checking #{node.name}'s children"
163
+ #trace "Checking #{node.name}'s children"
150
164
  node.each_child { |child|
151
- if next_node = find_node(child)
152
- return next_node
153
- end
165
+ next_node = find_node(child) and return next_node
154
166
  }
155
167
  nil
156
168
  end
157
169
  end
158
-
159
- def compute_node(node, use_fork, bucket)
160
- if use_fork
161
- trace "About to fork for node #{node.name}"
162
- if bucket
163
- #
164
- # Use our assigned bucket to transfer the result.
165
- #
166
- fork_node(node) {
167
- node.trace_compute
168
- bucket.contents = node.compute
169
- }
170
- bucket.contents
171
- else
172
- #
173
- # No bucket -- discarding result
174
- #
175
- fork_node(node) {
176
- node.trace_compute
177
- node.compute
178
- }
179
- true
180
- end
181
- else
182
- #
183
- # No fork
184
- #
185
- node.trace_compute
186
- node.compute
187
- end
188
- end
189
-
190
- def fork_node(node)
191
- trace "About to fork for node #{node.name}"
192
- process_id = RetriableFork.fork {
193
- trace "Fork: process #{Process.pid}"
194
- node.trace_compute
195
- yield
196
- trace "Fork: computation done"
197
- }
198
- trace "Waiting for process #{process_id}"
199
- Process.wait(process_id)
200
- trace "Process #{process_id} finished"
201
- exitstatus = $?.exitstatus
202
- if exitstatus != 0
203
- trace "Process #{process_id} returned #{exitstatus}; exiting."
204
- exit(1)
205
- end
206
- end
207
-
208
- extend self
209
170
  end
210
171
  end