comp_tree 0.5.2 → 0.7.0

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.
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
@@ -1,10 +1,6 @@
1
1
 
2
- require 'comp_tree/bucket_ipc'
3
- require 'comp_tree/diagnostic'
4
- require 'comp_tree/misc'
5
2
  require 'comp_tree/algorithm'
6
3
  require 'comp_tree/node'
7
- require 'comp_tree/task_node'
8
4
  require 'comp_tree/error'
9
5
 
10
6
  require 'thread'
@@ -15,53 +11,25 @@ module CompTree
15
11
  # responsible for defining nodes and running computations.
16
12
  #
17
13
  class Driver
18
- DEFAULTS = {
19
- :threads => 1,
20
- :fork => false,
21
- :timeout => 5.0,
22
- :wait_interval => 0.02,
23
- }
14
+ include Algorithm
24
15
 
25
- include Diagnostic
26
- include Misc
27
-
28
16
  #
29
- # Begin a new computation tree.
17
+ # Build and run a new computation tree.
30
18
  #
31
19
  # Options hash:
32
20
  #
33
21
  # <tt>:node_class</tt> -- (Class) CompTree::Node subclass from
34
22
  # which nodes are created.
35
23
  #
36
- # <tt>:discard_result</tt> -- (boolean) If you are <em>not</em>
37
- # interested in the final answer, but only in the actions which
38
- # complete the computation, then set this to +true+. This is
39
- # equivalent to saying <tt>:node_class => CompTree::TaskNode</tt>.
40
- # (If you are forking processes, it is good to know that IPC is
41
- # not needed to communicate the result.)
42
- #
43
24
  def initialize(opts = nil)
44
- if opts and opts[:node_class] and opts[:discard_result]
45
- raise(
46
- Error::ArgumentError,
47
- "#{self.class.name}.new: :discard_result and :node_class " +
48
- "are mutually exclusive")
49
- end
50
-
51
- @node_class =
25
+ @node_class = (
52
26
  if opts and opts[:node_class]
53
27
  opts[:node_class]
54
- elsif opts and opts[:discard_result]
55
- TaskNode
56
28
  else
57
29
  Node
58
30
  end
59
-
31
+ )
60
32
  @nodes = Hash.new
61
-
62
- if block_given?
63
- yield self
64
- end
65
33
  end
66
34
 
67
35
  #
@@ -72,55 +40,43 @@ module CompTree
72
40
  #
73
41
  # Define a computation node.
74
42
  #
75
- # There are three distinct forms of a node definition. In each of
76
- # the following examples, a computation node named +area+ is
77
- # defined which depends on the nodes +height+, +width+, +offset+.
43
+ # The first argument is the name of the node to define.
44
+ # Subsequent arguments are the names of this node's children.
78
45
  #
79
- # The method_missing form:
80
- # driver.define_area(:width, :height, :offset) { |width, height, offset|
81
- # width*height - offset
82
- # }
46
+ # The values of the child nodes are passed to the block. The
47
+ # block returns the result of this node.
83
48
  #
84
- # The eval form:
85
- # driver.define_area :width, :height, :offset, %{
86
- # width*height - offset
87
- # }
88
- # (Note the '%' before the brace.)
49
+ # In this example, a computation node named +area+ is defined
50
+ # which depends on the nodes +width+ and +height+.
89
51
  #
90
- # The raw form:
91
- # driver.define(:area, :width, :height, :offset) { |width, height, offset|
92
- # width*height - offset
93
- # }
52
+ # driver.define(:area, :width, :height) { |width, height|
53
+ # width*height
54
+ # }
94
55
  #
95
56
  def define(*args, &block)
96
57
  parent_name = args.first
97
58
  children_names = args[1..-1]
98
59
 
99
60
  unless parent_name
100
- raise Error::ArgumentError, "No name given for node"
61
+ raise ArgumentError, "No name given for node"
101
62
  end
102
63
 
103
64
  #
104
65
  # retrieve or create parent and children
105
66
  #
106
- parent =
107
- if t = @nodes[parent_name]
108
- t
109
- else
110
- @nodes[parent_name] = @node_class.new(parent_name)
111
- end
67
+ parent = @nodes[parent_name] || (
68
+ @nodes[parent_name] = @node_class.new(parent_name)
69
+ )
112
70
 
113
71
  if parent.function
114
- raise Error::RedefinitionError, "Node #{parent.name} already defined."
72
+ raise RedefinitionError, "Node `#{parent.name.inspect}' redefined."
115
73
  end
116
74
  parent.function = block
117
75
 
118
76
  children = children_names.map { |child_name|
119
- if t = @nodes[child_name]
120
- t
121
- else
77
+ @nodes[child_name] || (
122
78
  @nodes[child_name] = @node_class.new(child_name)
123
- end
79
+ )
124
80
  }
125
81
 
126
82
  #
@@ -132,72 +88,37 @@ module CompTree
132
88
  }
133
89
  end
134
90
 
135
- #
136
- # parsing/evaling helper
137
- #
138
- def evaling_define(*args) #:nodoc:
139
- function_name = args[0]
140
- function_arg_names = args[1..-2]
141
- function_string = args.last.to_str
142
-
143
- comma_separated = function_arg_names.map { |name|
144
- name.to_s
145
- }.join(",")
146
-
147
- eval_me = %{
148
- lambda { |#{comma_separated}|
149
- #{function_string}
150
- }
151
- }
152
-
153
- function = eval(eval_me, TOPLEVEL_BINDING)
154
-
155
- define(function_name, *function_arg_names, &function)
156
- end
157
-
158
- def method_missing(symbol, *args, &block) #:nodoc:
159
- if match = symbol.to_s.match(%r!\Adefine_(\w+)\Z!)
160
- method_name = match.captures.first.to_sym
161
- if block
162
- define(method_name, *args, &block)
163
- else
164
- evaling_define(method_name, *args)
165
- end
166
- else
167
- super(symbol, *args, &block)
168
- end
169
- end
170
-
171
91
  #
172
92
  # Mark this node and all its children as uncomputed.
173
93
  #
174
94
  # Arguments:
175
95
  #
176
- # +name+ -- (Symbol) node name.
96
+ # +name+ -- unique node identifier (usually a symbol).
177
97
  #
178
98
  def reset(name)
179
99
  @nodes[name].reset
180
100
  end
181
101
 
182
102
  #
183
- # Check for a cyclic graph below the given node. Raises
184
- # CompTree::Error::CircularError if found.
103
+ # Check for a cyclic graph below the given node. If found,
104
+ # returns the names of the nodes (in order) which form a loop.
105
+ # Otherwise returns nil.
185
106
  #
186
107
  # Arguments:
187
108
  #
188
- # +name+ -- (Symbol) node name.
109
+ # +name+ -- unique node identifier (usually a symbol).
189
110
  #
190
111
  def check_circular(name)
191
- helper = lambda { |root, chain|
112
+ helper = Proc.new { |root, chain|
192
113
  if chain.include? root
193
- raise Error::CircularError,
194
- "Circular dependency detected: #{root} => #{chain.last} => #{root}"
114
+ return chain + [root]
195
115
  end
196
116
  @nodes[root].children.each { |child|
197
117
  helper.call(child.name, chain + [root])
198
118
  }
199
119
  }
200
120
  helper.call(name, [])
121
+ nil
201
122
  end
202
123
 
203
124
  #
@@ -205,62 +126,26 @@ module CompTree
205
126
  #
206
127
  # Arguments:
207
128
  #
208
- # +name+ -- (Symbol) node name.
209
- #
210
- # Options hash:
211
- #
212
- # <tt>:threads</tt> -- (Integer) Number of parallel threads.
129
+ # +name+ -- unique node identifier (usually a symbol).
213
130
  #
214
- # <tt>:fork</tt> -- (boolean) Whether to fork each computation
215
- # node into its own process.
131
+ # +threads+ -- (Integer) number of threads.
216
132
  #
217
- # Defaults options are taken from Driver::DEFAULTS.
133
+ # compute(:volume, :threads => 4) syntax is also accepted.
218
134
  #
219
- def compute(name, opts = nil)
220
- #
221
- # Undocumented options:
222
- #
223
- # <tt>:wait_interval</tt> -- (seconds) (Obscure) How long to
224
- # wait after an IPC failure.
225
- #
226
- # <tt>:timeout</tt> -- (seconds) (Obscure) Give up after this
227
- # period of persistent IPC failures.
228
- #
229
-
230
- abort_on_exception {
231
- compute_private(name, opts || Hash.new)
232
- }
233
- end
234
-
235
- private
236
-
237
- def compute_private(name, opts_in)
238
- opts = DEFAULTS.merge(opts_in)
135
+ def compute(name, opts)
136
+ threads = opts.is_a?(Hash) ? opts[:threads] : opts
239
137
  root = @nodes[name]
240
138
 
241
- if opts[:threads] < 1
242
- raise Error::ArgumentError, "threads is #{opts[:threads]}"
139
+ if threads < 1
140
+ raise ArgumentError, "threads is #{threads}"
243
141
  end
244
142
 
245
- if opts[:threads] == 1
143
+ if root.computed
144
+ root.result
145
+ elsif threads == 1
246
146
  root.result = root.compute_now
247
- elsif opts[:fork] and not @node_class.discard_result?
248
- #
249
- # Use buckets to send results across forks.
250
- #
251
- result = nil
252
- BucketIPC::Driver.new(opts[:threads], opts) { |buckets|
253
- result =
254
- Algorithm.compute_multithreaded(
255
- root, opts[:threads], opts[:fork], buckets)
256
- }
257
- result
258
147
  else
259
- #
260
- # Multithreaded computation without fork.
261
- #
262
- Algorithm.compute_multithreaded(
263
- root, opts[:threads], opts[:fork], nil)
148
+ compute_multithreaded(root, threads)
264
149
  end
265
150
  end
266
151
  end
@@ -1,27 +1,22 @@
1
1
 
2
2
  module CompTree
3
- module Error
4
- # Base class for CompTree errors.
5
- class Base < StandardError ; end
3
+ # Base class for CompTree errors.
4
+ class Error < StandardError ; end
6
5
 
7
- # Internal error inside CompTree. Please send a bug report.
8
- class AssertionFailed < Base ; end
9
-
10
- # Bad arguments were passed to a method.
11
- class ArgumentError < Base ; end
12
-
13
- #
14
- # Attempt to redefine a Node.
15
- #
16
- # If you wish to only replace the function, set
17
- # driver.nodes[name].function = some_new_lambda
18
- #
19
- class RedefinitionError < Base ; end
20
-
21
- # A Cyclic graph was detected.
22
- class CircularError < Base ; end
23
-
24
- # No function was defined for this node.
25
- class NoFunctionError < Base ; end
26
- end
6
+ # Internal error inside CompTree. Please send a bug report.
7
+ class AssertionFailedError < Error ; end
8
+
9
+ # Bad arguments were passed to a method.
10
+ class ArgumentError < Error ; end
11
+
12
+ #
13
+ # Attempt to redefine a Node.
14
+ #
15
+ # If you wish to only replace the function, set
16
+ # driver.nodes[name].function = some_new_lambda
17
+ #
18
+ class RedefinitionError < Error ; end
19
+
20
+ # No function was defined for this node.
21
+ class NoFunctionError < Error ; end
27
22
  end
@@ -1,5 +1,4 @@
1
1
 
2
- require 'comp_tree/diagnostic'
3
2
  require 'thread'
4
3
 
5
4
  module CompTree
@@ -7,24 +6,26 @@ module CompTree
7
6
  # Base class for nodes in the computation tree.
8
7
  #
9
8
  class Node
10
- include Diagnostic
11
-
12
9
  attr_reader :name #:nodoc:
13
10
 
14
11
  attr_accessor :parents #:nodoc:
15
12
  attr_accessor :children #:nodoc:
16
13
  attr_accessor :function #:nodoc:
17
14
  attr_accessor :result #:nodoc:
15
+ attr_accessor :computed #:nodoc:
18
16
  attr_accessor :shared_lock #:nodoc:
19
17
 
18
+ attr_writer :children_results #:nodoc:
19
+
20
20
  #
21
21
  # Create a node
22
22
  #
23
23
  def initialize(name) #:nodoc:
24
24
  @name = name
25
- @mutex = Mutex.new
26
- @children = []
27
25
  @parents = []
26
+ @children = []
27
+ @function = nil
28
+ @mutex = Mutex.new
28
29
  reset_self
29
30
  end
30
31
 
@@ -32,9 +33,10 @@ module CompTree
32
33
  # Reset the computation for this node.
33
34
  #
34
35
  def reset_self #:nodoc:
36
+ @result = nil
37
+ @computed = nil
35
38
  @shared_lock = 0
36
39
  @children_results = nil
37
- @result = nil
38
40
  end
39
41
 
40
42
  #
@@ -83,50 +85,55 @@ module CompTree
83
85
  # If all children have been computed, return their results;
84
86
  # otherwise return nil.
85
87
  #
86
- def children_results #:nodoc:
87
- if @children_results
88
- @children_results
89
- else
90
- results = @children.map { |child|
91
- if child_result = child.result
92
- child_result
93
- else
88
+ # Do not assign to @children_results since own lock is not
89
+ # necessarily aquired.
90
+ #
91
+ def find_children_results #:nodoc:
92
+ @children_results or (
93
+ @children.map { |child|
94
+ unless child.computed
94
95
  return nil
95
96
  end
97
+ child.result
96
98
  }
97
- @children_results = results
98
- end
99
+ )
99
100
  end
100
101
 
101
- def trace_compute #:nodoc:
102
- debug {
103
- # --- own mutex
104
- trace "Computing #{@name}"
105
- raise Error::AssertionFailed if @result
106
- raise Error::AssertionFailed unless @mutex.locked?
107
- raise Error::AssertionFailed unless @children_results
108
- }
109
- end
102
+ #def trace_compute #:nodoc:
103
+ # debug {
104
+ # # --- own mutex
105
+ # trace "Computing #{@name}"
106
+ # raise AssertionFailedError if @computed
107
+ # raise AssertionFailedError unless @mutex.locked?
108
+ # raise AssertionFailedError unless @children_results
109
+ # }
110
+ #end
110
111
 
111
112
  #
112
113
  # Compute this node; children must be computed and lock must be
113
114
  # already acquired.
114
115
  #
115
116
  def compute #:nodoc:
116
- unless defined?(@function) and @function
117
- raise Error::NoFunctionError,
117
+ begin
118
+ unless @function
119
+ raise NoFunctionError,
118
120
  "No function was defined for node '#{@name.inspect}'"
121
+ end
122
+ @result = @function.call(*@children_results)
123
+ @computed = true
124
+ rescue Exception => e
125
+ @computed = e
119
126
  end
120
- @function.call(*@children_results)
127
+ @result
121
128
  end
122
129
 
123
130
  def try_lock #:nodoc:
124
131
  # --- shared tree mutex and own mutex
125
132
  if @shared_lock == 0 and @mutex.try_lock
126
- trace "Locking #{@name}"
133
+ #trace "Locking #{@name}"
127
134
  each_upward { |node|
128
135
  node.shared_lock += 1
129
- trace "#{node.name} locked by #{@name}: level: #{node.shared_lock}"
136
+ #trace "#{node.name} locked by #{@name}: level: #{node.shared_lock}"
130
137
  }
131
138
  true
132
139
  else
@@ -136,30 +143,19 @@ module CompTree
136
143
 
137
144
  def unlock #:nodoc:
138
145
  # --- shared tree mutex and own mutex
139
- debug {
140
- raise Error::AssertionFailed unless @mutex.locked?
141
- trace "Unlocking #{@name}"
142
- }
146
+ #debug {
147
+ # raise AssertionFailedError unless @mutex.locked?
148
+ # trace "Unlocking #{@name}"
149
+ #}
143
150
  each_upward { |node|
144
151
  node.shared_lock -= 1
145
- debug {
146
- if node.shared_lock == 0
147
- trace "#{node.name} unlocked by #{@name}"
148
- end
149
- }
152
+ #debug {
153
+ # if node.shared_lock == 0
154
+ # trace "#{node.name} unlocked by #{@name}"
155
+ # end
156
+ #}
150
157
  }
151
158
  @mutex.unlock
152
159
  end
153
-
154
- class << self
155
- #
156
- # Throw away the computation result?
157
- #
158
- # This Node base class always returns false.
159
- #
160
- def discard_result?
161
- false
162
- end
163
- end
164
160
  end
165
161
  end
data/lib/comp_tree.rb CHANGED
@@ -21,3 +21,59 @@
21
21
  #
22
22
 
23
23
  require 'comp_tree/driver'
24
+
25
+ #
26
+ # CompTree -- Parallel Computation Tree.
27
+ #
28
+ # See README.
29
+ #
30
+ module CompTree
31
+ class << self
32
+ #
33
+ # Build and run a new computation tree.
34
+ #
35
+ # A Driver instance is passed to the given block.
36
+ #
37
+ # Options hash:
38
+ #
39
+ # <tt>:node_class</tt> -- (Class) CompTree::Node subclass from
40
+ # which nodes are created.
41
+ #
42
+ # Example:
43
+ # CompTree.build do |driver|
44
+ #
45
+ # # Define a function named 'area' taking these two arguments.
46
+ # driver.define(:area, :width, :height) { |width, height|
47
+ # width*height
48
+ # }
49
+ #
50
+ # # Define a function 'width' which takes a 'border' argument.
51
+ # driver.define(:width, :border) { |border|
52
+ # 7 + border
53
+ # }
54
+ #
55
+ # # Ditto for 'height'.
56
+ # driver.define(:height, :border) { |border|
57
+ # 5 + border
58
+ # }
59
+ #
60
+ # #
61
+ # # Define a constant function 'border'.
62
+ # driver.define(:border) {
63
+ # 2
64
+ # }
65
+ #
66
+ # # Compute the area using four parallel threads.
67
+ # puts driver.compute(:area, :threads => 4)
68
+ # # => 63
69
+ #
70
+ # # We've done this computation.
71
+ # puts((7 + 2)*(5 + 2))
72
+ # # => 63
73
+ # end
74
+ #
75
+ def build(opts = nil)
76
+ yield Driver.new(opts)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,51 @@
1
+
2
+ require 'rbconfig'
3
+
4
+ module Jumpstart
5
+ module Ruby
6
+ EXECUTABLE = lambda {
7
+ name = File.join(
8
+ Config::CONFIG["bindir"],
9
+ Config::CONFIG["RUBY_INSTALL_NAME"]
10
+ )
11
+
12
+ if Config::CONFIG["host"] =~ %r!(mswin|cygwin|mingw)! and
13
+ File.basename(name) !~ %r!\.(exe|com|bat|cmd)\Z!i
14
+ name + Config::CONFIG["EXEEXT"]
15
+ else
16
+ name
17
+ end
18
+ }.call
19
+
20
+ class << self
21
+ def run(*args)
22
+ system(EXECUTABLE, *args)
23
+ end
24
+
25
+ def run_or_raise(*args)
26
+ cmd = [EXECUTABLE, *args]
27
+ unless system(*cmd)
28
+ msg = (
29
+ "failed to launch ruby: " +
30
+ "system(*#{cmd.inspect}) failed with status #{$?.exitstatus}"
31
+ )
32
+ raise msg
33
+ end
34
+ end
35
+
36
+ def with_warnings(value = true)
37
+ previous = $VERBOSE
38
+ $VERBOSE = value
39
+ begin
40
+ yield
41
+ ensure
42
+ $VERBOSE = previous
43
+ end
44
+ end
45
+
46
+ def no_warnings(&block)
47
+ with_warnings(false, &block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -2,13 +2,9 @@
2
2
  require 'rbconfig'
3
3
  require 'fileutils'
4
4
  require 'find'
5
- require 'fileutils'
6
- require 'quix/vars'
7
-
8
- module CompTree
9
- class SimpleInstaller
10
- include CompTree::Vars
11
5
 
6
+ module Jumpstart
7
+ class SimpleInstaller
12
8
  def initialize
13
9
  dest_root = Config::CONFIG["sitelibdir"]
14
10
  sources = []
@@ -49,19 +45,21 @@ module CompTree
49
45
  end
50
46
  }
51
47
 
52
- acc << locals_to_hash {%{source, dest, install, uninstall}}
48
+ acc << {
49
+ :source => source,
50
+ :dest => dest,
51
+ :install => install,
52
+ :uninstall => uninstall,
53
+ }
53
54
  end
54
55
  }
55
56
  end
56
57
 
57
58
  def install_file?(source)
58
- !File.symlink?(source) and
59
- (File.directory?(source) or
60
- (File.file?(source) and File.extname(source) == ".rb"))
59
+ File.directory?(source) or
60
+ (File.file?(source) and File.extname(source) == ".rb")
61
61
  end
62
62
 
63
- attr_accessor :spec
64
-
65
63
  def install
66
64
  @spec.each { |entry|
67
65
  entry[:install].call
@@ -75,7 +73,7 @@ module CompTree
75
73
  end
76
74
 
77
75
  def run(args = ARGV)
78
- if args.empty? or (args.size == 1 and args.first == "install")
76
+ if args.empty?
79
77
  install
80
78
  elsif args.size == 1 and args.first == "--uninstall"
81
79
  uninstall