mssh 0.0.1 → 0.0.4

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 (4) hide show
  1. data/bin/mcmd-ev +97 -0
  2. data/bin/mssh +35 -13
  3. data/lib/mcmd-ev.rb +390 -0
  4. metadata +6 -32
data/bin/mcmd-ev ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'pp'
4
+
5
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
6
+ require 'mcmd-ev'
7
+
8
+ require 'optparse'
9
+ options = {
10
+ :maxflight => 200,
11
+ :timeout => 60,
12
+ :global_timeout => 0,
13
+ }
14
+
15
+ optparse = OptionParser.new do |opts|
16
+ opts.on('-r', '--range RANGE', 'currently takes a CSV list') do |arg|
17
+ options[:range] = arg
18
+ end
19
+ opts.on('-m', '--maxflight 50', 'How many subprocesses? 50 by default') do |arg|
20
+ options[:maxflight] = arg
21
+ end
22
+ opts.on('-t', '--timeout 60', 'How many seconds may each individual process take? 0 for no timeout') do |arg|
23
+ options[:timeout] = arg
24
+ end
25
+ opts.on('-g', '--global_timeout 600', 'How many seconds for the whole shebang 0 for no timeout') do |arg|
26
+ options[:global_timeout] = arg
27
+ end
28
+ opts.on('--noshell', "Don't invoke a shell. Args will be passed to exec verbatim ") do |arg|
29
+ options[:noshell] = arg
30
+ end
31
+ opts.on('-c', '--collapse', "Collapse similar output ") do |arg|
32
+ options[:collapse] = arg
33
+ end
34
+ opts.on('-v', '--verbose', "Verbose output") do |arg|
35
+ options[:verbose] = arg
36
+ end
37
+ opts.on('-d', '--debug', "Debug output") do |arg|
38
+ options[:debug] = arg
39
+ end
40
+ # option to merge stdin/stdout into one buf? how should this work?
41
+ # option to ignore as-we-go yield output - this is off by default now except for success/fail
42
+ end
43
+ optparse.parse!
44
+
45
+ raise "Error, need -r argument" if options[:range].nil? or options[:range].empty?
46
+ raise "Error, need command to run" if ARGV.size.zero?
47
+
48
+ m = MultipleCmd.new
49
+
50
+ targets = options[:range].split ","
51
+
52
+ m.commands = targets.map { |t| ["/bin/sh", "-c"].push ARGV.map { |arg| arg.gsub('HOSTNAME', t)}.join " " }
53
+ command_to_target = Hash.new
54
+ targets.size.times do |i|
55
+ command_to_target[m.commands[i].object_id] = targets[i]
56
+ end
57
+ m.yield_startcmd = lambda { |p| puts "#{command_to_target[p.command.object_id]}: starting" } if options[:verbose]
58
+ m.yield_wait = lambda { |p| puts "#{p.success? ? 'SUCCESS' : 'FAILURE'} #{command_to_target[p.command.object_id]}: '#{p.stdout_buf}'" }
59
+ # m.yield_proc_timeout = lambda { |p| puts "am killing #{p.inspect}"}
60
+
61
+ m.perchild_timeout = options[:timeout].to_i
62
+ m.global_timeout = options[:global_timeout].to_i
63
+ m.maxflight = options[:maxflight].to_i
64
+ m.verbose = options[:verbose]
65
+ m.debug = options[:debug]
66
+
67
+ result = m.run
68
+
69
+ if options[:collapse]
70
+ # print a collapsed summary
71
+ stdout_matches_success = Hash.new
72
+ stdout_matches_failure = Hash.new
73
+ result.each do |r|
74
+ if r[:retval].success?
75
+ stdout_matches_success[r[:stdout_buf]] = [] if stdout_matches_success[r[:stdout_buf]].nil?
76
+ stdout_matches_success[r[:stdout_buf]] << command_to_target[r[:command].object_id]
77
+ else
78
+ stdout_matches_failure[r[:stdout_buf]] = [] if stdout_matches_failure[r[:stdout_buf]].nil?
79
+ stdout_matches_failure[r[:stdout_buf]] << command_to_target[r[:command].object_id]
80
+ end
81
+ end
82
+ # output => [targets ...]
83
+ stdout_matches_success.each_pair do |k,v|
84
+ puts "SUCCESS: #{v.join ','}: #{k}"
85
+ end
86
+ stdout_matches_failure.each_pair do |k,v|
87
+ puts "FAILURE: #{v.join ','}: #{k}"
88
+ end
89
+ else
90
+ # we already printed while in-flight; do nothing
91
+ # not collapse, print one per host
92
+ # result.each do |r|
93
+ # target = command_to_target[r[:command].object_id]
94
+ # puts "#{target}: '#{r[:stdout_buf].chomp}'\n"
95
+ # end
96
+ end
97
+
data/bin/mssh CHANGED
@@ -3,7 +3,6 @@
3
3
  require 'rubygems'
4
4
  require 'pp'
5
5
  require 'mcmd'
6
- require 'rangeclient'
7
6
 
8
7
  require 'optparse'
9
8
  options = {
@@ -15,6 +14,10 @@ optparse = OptionParser.new do |opts|
15
14
  opts.on('-r', '--range RANGE', 'currently takes a CSV list') do |arg|
16
15
  options[:range] = arg
17
16
  end
17
+ opts.on('-f', '--file FILE', 'List of hostnames in a FILE \
18
+ use (/dev/stdin) for reading from stdin') do |arg|
19
+ options[:file] = arg
20
+ end
18
21
  opts.on('-m', '--maxflight 50', 'How many subprocesses? 50 by default') do |arg|
19
22
  options[:maxflight] = arg
20
23
  end
@@ -38,18 +41,26 @@ optparse = OptionParser.new do |opts|
38
41
  end
39
42
  optparse.parse!
40
43
 
41
- raise "Error, need -r argument" if options[:range].nil? or options[:range].empty?
44
+ targets = []
45
+ if (!options[:range].nil?)
46
+ require 'rangeclient'
47
+ range = Range::Client.new
48
+ targets = range.expand options[:range]
49
+ elsif (!options[:file].nil?)
50
+ targets_fd = File.open(options[:file])
51
+ targets_fd.read.each { |x| targets << x.chomp }
52
+ else
53
+ raise "Error, need -r or -f option"
54
+ end
55
+
56
+
42
57
  raise "Error, need command to run" if ARGV.size.zero?
43
58
  raise "Error, too many arguments" if ARGV.size != 1
44
59
 
45
60
 
46
- range = Range::Client.new
47
61
 
48
62
  m = MultipleCmd.new
49
-
50
- targets = range.expand options[:range]
51
-
52
- m.commands = targets.map { |t| ["/usr/bin/ssh", "-2", "-oPasswordAuthentication=no", "-A", t].push ARGV.first }
63
+ m.commands = targets.map { |t| ["/usr/bin/ssh","-2", "-oBatchMode=yes", "-A", t].push ARGV.first }
53
64
  command_to_target = Hash.new
54
65
  targets.size.times do |i|
55
66
  command_to_target[m.commands[i].object_id] = targets[i]
@@ -65,15 +76,18 @@ m.debug = options[:debug]
65
76
 
66
77
  result = m.run
67
78
 
68
- if options[:collapse]
79
+ if (options[:collapse] and options[:range])
69
80
  # print a collapsed summary
70
- stdout_matches = Hash.new
81
+ out_matches = Hash.new
71
82
  result.each do |r|
72
- stdout_matches[r[:stdout_buf]] = [] if stdout_matches[r[:stdout_buf]].nil?
73
- stdout_matches[r[:stdout_buf]] << command_to_target[r[:command].object_id]
83
+ out = ""
84
+ out += r[:stdout_buf].chomp if(!r[:stdout_buf].nil?)
85
+ out += r[:stderr_buf].chomp if(!r[:stderr_buf].nil?)
86
+ out_matches[out] = [] if out_matches[out].nil?
87
+ out_matches[out] << command_to_target[r[:command].object_id]
74
88
  end
75
89
  # output => [targets ...]
76
- stdout_matches.each_pair do |k,v|
90
+ out_matches.each_pair do |k,v|
77
91
  hosts = range.compress v
78
92
  puts "#{hosts}: '#{k.chomp}'"
79
93
  end
@@ -81,6 +95,14 @@ else
81
95
  # not collapse, print one per host
82
96
  result.each do |r|
83
97
  target = command_to_target[r[:command].object_id]
84
- puts "#{target}: #{r[:retval] == 0 ? 'SUCCESS:':'FAILURE:'} '#{r[:stdout_buf].chomp}'\n"
98
+ out = ""
99
+ out += r[:stdout_buf].chomp if(!r[:stdout_buf].nil?)
100
+ out += r[:stderr_buf].chomp if(!r[:stderr_buf].nil?)
101
+ if (r[:retval] == 0 )
102
+ puts "#{target}:SUCCESS: #{out}\n"
103
+ else
104
+ exit_code = r[:retval].exitstatus.to_s if(!r[:retval].nil?)
105
+ puts "#{target}:FAILURE[#{exit_code}]: #{out}\n"
106
+ end
85
107
  end
86
108
  end
data/lib/mcmd-ev.rb ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'pp'
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+
7
+
8
+ # TODO - figure out why IO.sysread isn't raising on EOF
9
+ module FdWatcher
10
+ def notify_readable
11
+ puts "readable: '#{@io.inspect}'"
12
+ puts "mcmd obj: '#{@mcmd}'"
13
+ # read available bytes, add to the subproc's read buf
14
+ if not @mcmd.subproc_by_fd.has_key?(@io)
15
+ raise "Select returned a fd which I have not seen! fd: #{@io.inspect}"
16
+ end
17
+ subproc = @mcmd.subproc_by_fd[@io]
18
+ buf = ""
19
+ begin
20
+ buf = @io.sysread(4096)
21
+ puts "saw #{buf}"
22
+ if buf.nil?
23
+ raise " Impossible result from sysread()"
24
+ end
25
+ # no exception? bytes were read. append them.
26
+ if @io == subproc.stdout_fd
27
+ subproc.stdout_buf << buf
28
+ # FIXME if we've read > maxbuf, allow closing/ignoring the fd instead of hard kill
29
+ if subproc.stdout_buf.bytesize > @mcmd.max_read_size
30
+ # self.kill_process(subproc) # can't kill this here, need a way to mark-to-kill
31
+ end
32
+ elsif @io == subproc.stderr_fd
33
+ subproc.stderr_buf << buf
34
+ # FIXME if we've read > maxbuf, allow closing/ignoring the fd instead of hard kill
35
+ if subproc.stderr_buf.bytesize > @mcmd.max_read_size
36
+ # self.kill_process(subproc) # "" above
37
+ end
38
+ end
39
+ rescue SystemCallError, EOFError => ex
40
+ puts "DEBUG: saw read exception #{ex}"
41
+ @mcmd.periodic
42
+ rescue => ex
43
+ puts "else #{ex.inspect}"
44
+ end
45
+ end
46
+
47
+ def unbind
48
+ puts "GOT UNBIND"
49
+ @mcmd.periodic
50
+ # move periodic() bits under here, to be called as necessary
51
+ end
52
+ def notify_writable
53
+ # puts "writable: '#{@io.inspect}'"
54
+ end
55
+ end
56
+
57
+ class FdClass < EventMachine::Connection
58
+ include FdWatcher
59
+ end
60
+
61
+ module ProcessWatcher
62
+ def process_exited
63
+ puts 'the forked child died!'
64
+ EM.stop_event_loop
65
+ puts "stopped"
66
+ end
67
+ end
68
+
69
+
70
+ class MultipleCmd
71
+
72
+ attr_accessor :global_timeout, :maxflight, :perchild_timeout, :commands
73
+ attr_accessor :yield_wait, :yield_startcmd, :debug, :yield_proc_timeout
74
+ attr_accessor :verbose, :poll_period, :max_read_size
75
+ attr_accessor :subproc_by_fd
76
+
77
+ def initialize
78
+ # these are re-initialized after every run
79
+ @subproc_by_pid = Hash.new
80
+ @subproc_by_fd = Hash.new
81
+ @processed_commands = []
82
+ # end items which are re-initialized
83
+
84
+ self.commands = []
85
+ self.perchild_timeout = 60
86
+ self.global_timeout = 0
87
+ self.maxflight = 200
88
+ self.debug = false
89
+ self.poll_period = 0.5 # shouldn't need adjusting
90
+ self.max_read_size = 2 ** 19 # 512k
91
+ end
92
+
93
+ def noshell_exec(cmd)
94
+ if cmd.length == 1
95
+ Kernel.exec([cmd[0], cmd[0]])
96
+ else
97
+ Kernel.exec([cmd[0], cmd[0]], *cmd[1..-1])
98
+ end
99
+ end
100
+
101
+ # I should probably move this whole method
102
+ # into SubProc and make the subproc_by_* into
103
+ # class variables
104
+ def add_subprocess(cmd)
105
+ stdin_rd, stdin_wr = IO.pipe
106
+ stdout_rd, stdout_wr = IO.pipe
107
+ stderr_rd, stderr_wr = IO.pipe
108
+ subproc = MultipleCmd::SubProc.new
109
+ subproc.stdin_fd = stdin_wr
110
+ subproc.stdout_fd = stdout_rd
111
+ subproc.stderr_fd = stderr_rd
112
+ subproc.command = cmd
113
+
114
+ puts "Before fork"
115
+ pid = fork
116
+ if not pid.nil?
117
+ # parent
118
+ stdin_rd.close rescue true
119
+ stdout_wr.close rescue true
120
+ stderr_wr.close rescue true
121
+ # for mapping to subproc by pid
122
+ subproc.pid = pid
123
+ @subproc_by_pid[pid] = subproc
124
+ # for mapping to subproc by i/o handle (returned from select)
125
+ @subproc_by_fd[stdin_rd] = subproc
126
+ @subproc_by_fd[stdin_wr] = subproc
127
+ @subproc_by_fd[stdout_rd] = subproc
128
+ @subproc_by_fd[stdout_wr] = subproc
129
+ @subproc_by_fd[stderr_rd] = subproc
130
+ @subproc_by_fd[stderr_wr] = subproc
131
+
132
+ self.yield_startcmd.call(subproc) unless self.yield_startcmd.nil?
133
+ temp_self = self
134
+ # now add fds to EM
135
+ EM.watch(stdin_wr, FdClass) { |c|
136
+ c.notify_writable = true
137
+ c.instance_eval do
138
+ @mcmd = temp_self
139
+ end
140
+ }
141
+ EM.watch(stdout_rd, FdClass) { |c|
142
+ c.notify_readable = true
143
+ c.instance_eval do
144
+ @mcmd = temp_self
145
+ end
146
+ }
147
+ EM.watch(stderr_rd, FdClass) { |c|
148
+ c.notify_readable = true
149
+ c.instance_eval do
150
+ @mcmd = temp_self
151
+ end
152
+ }
153
+ # EM.watch_process(pid, ProcessWatcher) # is this kqueue only? If so, probably don't need it FIXME
154
+ else
155
+ # child
156
+ # setup stdin, out, err
157
+ stdin_wr.close rescue true
158
+ stdout_rd.close rescue true
159
+ stderr_rd.close rescue true
160
+ STDIN.reopen(stdin_rd)
161
+ STDOUT.reopen(stdout_wr)
162
+ STDERR.reopen(stderr_wr)
163
+ puts "EXEC: #{cmd}"
164
+ noshell_exec(cmd)
165
+ raise "can't be reached!!. exec failed!!"
166
+ end
167
+ end
168
+
169
+ def process_read_fds(read_fds)
170
+ read_fds.each do |fd|
171
+ begin
172
+ buf = fd.sysread(4096)
173
+
174
+ if buf.nil?
175
+ raise " Impossible result from sysread()"
176
+ end
177
+ # no exception? bytes were read. append them.
178
+ if fd == subproc.stdout_fd
179
+ subproc.stdout_buf << buf
180
+ # FIXME if we've read > maxbuf, allow closing/ignoring the fd instead of hard kill
181
+ if subproc.stdout_buf.bytesize > self.max_read_size
182
+ # self.kill_process(subproc) # can't kill this here, need a way to mark-to-kill
183
+ end
184
+ elsif fd == subproc.stderr_fd
185
+ subproc.stderr_buf << buf
186
+ # FIXME if we've read > maxbuf, allow closing/ignoring the fd instead of hard kill
187
+ if subproc.stderr_buf.bytesize > self.max_read_size
188
+ # self.kill_process(subproc) # "" above
189
+ end
190
+ end
191
+ rescue SystemCallError, EOFError => ex
192
+ puts "DEBUG: saw read exception #{ex}" if self.debug
193
+ detach
194
+ # clear out the read fd for this subproc
195
+ # finalize read i/o
196
+ # if we're reading, it was the process's stdout or stderr
197
+ if fd == subproc.stdout_fd
198
+ subproc.stdout_fd = nil
199
+ elsif fd == subproc.stderr_fd
200
+ subproc.stderr_fd = nil
201
+ else
202
+ raise "impossible: operating on a subproc where the fd isn't found, even though it's mapped"
203
+ end
204
+ fd.close rescue true
205
+ end
206
+ end
207
+ end # process_read_fds()
208
+ def process_write_fds(write_fds)
209
+ write_fds.each do |fd|
210
+ raise "working on an unknown fd #{fd}" unless @subproc_by_fd.has_key?(fd)
211
+ subproc = @subproc_by_fd[fd]
212
+ buf = ""
213
+ # add writing here, todo. not core feature
214
+ end
215
+ end
216
+ def process_err_fds(err_fds)
217
+ end
218
+
219
+ # iterate and service fds in child procs, collect data and status
220
+ def service_subprocess_io
221
+ write_fds = @subproc_by_pid.values.select {|x| not x.stdin_fd.nil? and not x.terminated}.map {|x| x.stdin_fd}
222
+ read_fds = @subproc_by_pid.values.select {|x| not x.terminated}.map {|x| [x.stdout_fd, x.stderr_fd].select {|x| not x.nil? } }.flatten
223
+
224
+ read_fds, write_fds, err_fds = IO.select(read_fds, write_fds, nil, self.poll_period)
225
+
226
+ self.process_read_fds(read_fds) unless read_fds.nil?
227
+ self.process_write_fds(write_fds) unless write_fds.nil?
228
+ self.process_err_fds(err_fds) unless err_fds.nil?
229
+ # errors?
230
+ end
231
+
232
+ def process_timeouts
233
+ now = Time.now.to_i
234
+ @subproc_by_pid.values.each do |p|
235
+ if (now - p.time_start) > self.perchild_timeout
236
+ # expire this child process
237
+
238
+ self.yield_proc_timeout.call(p) unless self.yield_proc_timeout.nil?
239
+ self.kill_process(p)
240
+ end
241
+ end
242
+ end
243
+
244
+ def kill_process(p)
245
+ # do not remove from pid list until waited on
246
+ @subproc_by_fd.delete(p.stdin_fd)
247
+ @subproc_by_fd.delete(p.stdout_fd)
248
+ @subproc_by_fd.delete(p.stderr_fd)
249
+ # must kill after deleting from maps
250
+ # kill closes fds
251
+ p.kill
252
+ end
253
+
254
+ def periodic
255
+ puts "in periodic"
256
+ self.wait
257
+ # This could probably move into the child-cleanup callback
258
+ # start up as many as maxflight processes
259
+ while @subproc_by_pid.length < self.maxflight and not @commands.empty?
260
+ # take one from @commands and start it
261
+ commands = @commands.shift
262
+ self.add_subprocess(commands)
263
+ end
264
+
265
+ puts "have #{@subproc_by_pid.length} left to go"
266
+ # if we have nothing in flight (active pid)
267
+ # and nothing pending on the input list
268
+ # then we're done
269
+ if @subproc_by_pid.length.zero? and @commands.empty?
270
+ done = true
271
+ puts "stopping ev loop"
272
+ EM.stop_event_loop
273
+ end
274
+ end
275
+
276
+ def run
277
+ @global_time_start = Time.now.to_i
278
+ done = false
279
+ EM.run do
280
+ EM.schedule do
281
+ puts "schedule!"
282
+ self.periodic
283
+ end
284
+ # EM.add_periodic_timer(0.5) do
285
+ # puts "TIMER!!"
286
+ # self.periodic
287
+ # end # end periodic timer
288
+ end
289
+
290
+ ## These things get broken up into individual proc/fd callbacks
291
+ # service running processes
292
+ # self.service_subprocess_io
293
+ # timeout overdue processes
294
+ # self.process_timeouts
295
+ # service process cleanup
296
+
297
+ ## this is still the same, post-event-loop
298
+ data = self.return_rundata
299
+ # these are re-initialized after every run
300
+ @subproc_by_pid = Hash.new
301
+ @subproc_by_fd = Hash.new
302
+ @processed_commands = []
303
+ # end items which are re-initialized
304
+ return data
305
+ end
306
+
307
+ def return_rundata
308
+ data = []
309
+ @processed_commands.each do |c|
310
+ #FIXME pass through the process object
311
+ data << {
312
+ :pid => c.pid,
313
+ :write_buf_position => c.write_buf_position,
314
+ :stdout_buf => c.stdout_buf,
315
+ :stderr_buf => c.stderr_buf,
316
+ :command => c.command,
317
+ :time_start => c.time_start,
318
+ :time_end => c.time_end,
319
+ :retval => c.retval,
320
+ }
321
+ end
322
+ return data
323
+ end
324
+
325
+ def wait
326
+ possible_children = true
327
+ just_reaped = Array.new
328
+ while possible_children
329
+ begin
330
+ pid = Process::waitpid(-1, Process::WNOHANG)
331
+ if pid.nil?
332
+ possible_children = false
333
+ else
334
+ # pid is now gone. remove from subproc_by_pid and
335
+ # add to the processed commands list
336
+ p = @subproc_by_pid[pid]
337
+ p.time_end = Time.now.to_i
338
+ p.retval = $?
339
+ @subproc_by_pid.delete(pid)
340
+ @processed_commands << p
341
+ just_reaped << p
342
+ end
343
+ rescue Errno::ECHILD => ex
344
+ # ECHILD. ignore.
345
+ possible_children = false
346
+ end
347
+ end
348
+ # We may have waited on a child before reading all its output. Collect those missing bits. No blocking.
349
+ if not just_reaped.empty?
350
+ read_fds = just_reaped.select {|x| not x.terminated}.map {|x| [x.stdout_fd, x.stderr_fd].select {|x| not x.nil? } }.flatten
351
+ read_fds, write_fds, err_fds = IO.select(read_fds, nil, nil, 0)
352
+ self.process_read_fds(read_fds) unless read_fds.nil?
353
+ end
354
+ just_reaped.each do |p|
355
+ self.yield_wait.call(p) unless self.yield_wait.nil?
356
+ end
357
+ end
358
+
359
+ end
360
+
361
+ class MultipleCmd::SubProc
362
+ attr_accessor :stdin_fd, :stdout_fd, :stderr_fd, :write_buf_position
363
+ attr_accessor :time_start, :time_end, :pid, :retval, :stdout_buf, :stderr_buf, :command, :terminated
364
+
365
+ def initialize
366
+ self.write_buf_position = 0
367
+ self.time_start = Time.now.to_i
368
+ self.stdout_buf = ""
369
+ self.stderr_buf = ""
370
+ self.terminated = false
371
+ end
372
+
373
+ # when a process has out-stayed its welcome
374
+ def kill
375
+ self.stdin_fd.close rescue true
376
+ self.stdout_fd.close rescue true
377
+ self.stderr_fd.close rescue true
378
+ #TODO configurable sig?
379
+ Process::kill("KILL", self.pid)
380
+ self.terminated = true
381
+ end
382
+
383
+
384
+ # some heuristic to determine if this job was successful
385
+ # for now, trust retval. Also check stderr?
386
+ def success?
387
+ self.retval.success?
388
+ end
389
+ end
390
+
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mssh
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 4
10
+ version: 0.0.4
11
11
  platform: ruby
12
12
  authors:
13
13
  - Evan Miller
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-05-16 00:00:00 Z
18
+ date: 2012-09-04 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: json
@@ -31,34 +31,6 @@ dependencies:
31
31
  version: "0"
32
32
  type: :runtime
33
33
  version_requirements: *id001
34
- - !ruby/object:Gem::Dependency
35
- name: rangeclient
36
- prerelease: false
37
- requirement: &id002 !ruby/object:Gem::Requirement
38
- none: false
39
- requirements:
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- hash: 3
43
- segments:
44
- - 0
45
- version: "0"
46
- type: :runtime
47
- version_requirements: *id002
48
- - !ruby/object:Gem::Dependency
49
- name: rdoc
50
- prerelease: false
51
- requirement: &id003 !ruby/object:Gem::Requirement
52
- none: false
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- hash: 3
57
- segments:
58
- - 0
59
- version: "0"
60
- type: :runtime
61
- version_requirements: *id003
62
34
  description: Simple library for running jobs and sshing to many hosts at once.
63
35
  email:
64
36
  - github@squareup.com
@@ -70,8 +42,10 @@ extensions: []
70
42
  extra_rdoc_files:
71
43
  - LICENSE.md
72
44
  files:
45
+ - lib/mcmd-ev.rb
73
46
  - lib/mcmd.rb
74
47
  - bin/mcmd
48
+ - bin/mcmd-ev
75
49
  - bin/mssh
76
50
  - README.md
77
51
  - LICENSE.md