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.
- data/bin/mcmd-ev +97 -0
- data/bin/mssh +35 -13
- data/lib/mcmd-ev.rb +390 -0
- 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
|
-
|
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
|
-
|
81
|
+
out_matches = Hash.new
|
71
82
|
result.each do |r|
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
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:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
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-
|
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
|