mssh 0.0.11 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/mcmd +11 -83
- data/bin/mssh +16 -113
- data/lib/cli.rb +182 -0
- data/lib/mcmd.rb +30 -27
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff5ab53b19325189d26958ba44305d99d38a6a58
|
4
|
+
data.tar.gz: 957c71e1f478c67882899721be1b210ade946c87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7d6388c50fd34c95d9e4258907265384c9ea8961d94e13e15ce89da5feefa29644dc35273a92335c4e62c980fbb047a0d00bf4e30839e90568bcb9e22ce46cb
|
7
|
+
data.tar.gz: ebfeb295fadbf60a0ebfd4b8fcf102371efdbf69046d1c4e03ebc1bf227d9f76eab47fb6abfe1fc983bc7f4a9779f8d6c083f6af7858894834e5ada84123fd1f
|
data/bin/mcmd
CHANGED
@@ -1,97 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
require_relative '../lib/cli'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
require 'optparse'
|
9
|
-
options = {
|
5
|
+
cli = CommonCli.new({
|
6
|
+
# defaults
|
10
7
|
:maxflight => 200,
|
11
8
|
:timeout => 60,
|
12
9
|
:global_timeout => 0,
|
13
|
-
}
|
10
|
+
})
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
options[:
|
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
|
12
|
+
cli.extra_options = lambda do |parser, options|
|
13
|
+
parser.on('--noshell', "Don't invoke a shell. Args will be passed to exec verbatim ") do |arg|
|
14
|
+
options[:noshell] = arg
|
39
15
|
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
16
|
end
|
43
|
-
optparse.parse!
|
44
17
|
|
45
|
-
|
46
|
-
raise "Error, need command to run" if ARGV.size.zero?
|
18
|
+
cli.parse ARGV
|
47
19
|
|
48
|
-
|
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]
|
20
|
+
result = cli.runcmd do |cmd, t|
|
21
|
+
if cli.options[:noshell] then [cmd] else ["/bin/sh", "-c"].push cmd end
|
56
22
|
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
23
|
|
69
|
-
|
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
|
24
|
+
cli.output result
|
97
25
|
|
data/bin/mssh
CHANGED
@@ -1,128 +1,31 @@
|
|
1
|
-
#!/usr/bin/ruby
|
1
|
+
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require 'pp'
|
5
|
-
require 'mcmd'
|
3
|
+
require_relative '../lib/cli'
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
options = {
|
5
|
+
cli = CommonCli.new({
|
6
|
+
# defaults
|
10
7
|
:maxflight => 50,
|
11
8
|
:timeout => 60,
|
12
9
|
:global_timeout => 600,
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
opts.on('-f', '--file FILE', 'List of hostnames in a FILE use (/dev/stdin) for reading from stdin') do |arg|
|
22
|
-
options[:file] = arg
|
23
|
-
end
|
24
|
-
opts.on('-m', '--maxflight 50', 'How many subprocesses? 50 by default') do |arg|
|
25
|
-
options[:maxflight] = arg
|
26
|
-
end
|
27
|
-
opts.on('-t', '--timeout 60', 'How many seconds may each individual process take? 0 for no timeout') do |arg|
|
28
|
-
options[:timeout] = arg
|
29
|
-
end
|
30
|
-
opts.on('-g', '--global_timeout 600', 'How many seconds for the whole shebang 0 for no timeout') do |arg|
|
31
|
-
options[:global_timeout] = arg
|
32
|
-
end
|
33
|
-
opts.on('-c', '--collapse', "Collapse similar output ") do |arg|
|
34
|
-
options[:collapse] = arg
|
35
|
-
end
|
36
|
-
opts.on('-v', '--verbose', "verbose ") do |arg|
|
37
|
-
options[:verbose] = arg
|
38
|
-
end
|
39
|
-
opts.on('-d', '--debug', "Debug output") do |arg|
|
40
|
-
options[:debug] = arg
|
41
|
-
end
|
42
|
-
opts.on('--sshopt OPT', "ssh options, default [-2, -oBatchMode=yes, -A]. Can be used multiple times.") do |arg|
|
10
|
+
:sshcmd => "/usr/bin/ssh",
|
11
|
+
:sshopt => ["-2", "-oBatchMode=yes", "-A"]
|
12
|
+
})
|
13
|
+
|
14
|
+
## Extra options for the SSH version
|
15
|
+
cli.extra_options = lambda do |parser, options|
|
16
|
+
parser.on('--sshopt OPT', "ssh options, default [-2, -oBatchMode=yes, -A]. Can be used multiple times.") do |arg|
|
43
17
|
options[:sshopt] ||= []
|
44
18
|
options[:sshopt] << arg
|
45
19
|
end
|
46
|
-
|
20
|
+
parser.on('--sshcmd BINARY', "Path to the ssh binary (default /usr/bin/ssh)") do |arg|
|
47
21
|
options[:sshcmd] = arg
|
48
22
|
end
|
49
|
-
# option to merge stdin/stdout into one buf?
|
50
|
-
# option to ignore as-we-go yield output
|
51
23
|
end
|
52
|
-
optparse.parse!
|
53
24
|
|
54
|
-
|
55
|
-
options[:sshopt] ||= ["-2", "-oBatchMode=yes", "-A"]
|
56
|
-
options[:sshcmd] = "/usr/bin/ssh"
|
25
|
+
cli.parse ARGV
|
57
26
|
|
58
|
-
|
59
|
-
|
60
|
-
raise "Error, need -r or -f or --hostlist option"
|
27
|
+
result = cli.runcmd do |command, target|
|
28
|
+
[cli.options[:sshcmd], *cli.options[:sshopt]].push target, command
|
61
29
|
end
|
62
30
|
|
63
|
-
|
64
|
-
require 'rangeclient'
|
65
|
-
range = Range::Client.new
|
66
|
-
targets.push *range.expand(options[:range])
|
67
|
-
end
|
68
|
-
if (!options[:file].nil?)
|
69
|
-
targets_fd = File.open(options[:file])
|
70
|
-
targets_fd.read.each_line { |x| targets << x.chomp }
|
71
|
-
end
|
72
|
-
if (!options[:hostlist].nil?)
|
73
|
-
targets.push *options[:hostlist]
|
74
|
-
end
|
75
|
-
|
76
|
-
raise "no targets specified. Check your -r, -f or --hostlist inputs" if targets.size.zero?
|
77
|
-
raise "need command to run" if ARGV.size.zero?
|
78
|
-
raise "too many arguments" if ARGV.size != 1
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
m = MultipleCmd.new
|
83
|
-
m.commands = targets.map { |t| [options[:sshcmd], *options[:sshopt] ].push t, ARGV.first }
|
84
|
-
command_to_target = Hash.new
|
85
|
-
targets.size.times do |i|
|
86
|
-
command_to_target[m.commands[i].object_id] = targets[i]
|
87
|
-
end
|
88
|
-
m.yield_startcmd = lambda { |p| puts "#{command_to_target[p.command.object_id]}: starting" } if options[:verbose]
|
89
|
-
m.yield_wait = lambda { |p| puts "#{command_to_target[p.command.object_id]}: finished" } if options[:verbose]
|
90
|
-
|
91
|
-
m.perchild_timeout = options[:timeout].to_i
|
92
|
-
m.global_timeout = options[:global_timeout].to_i
|
93
|
-
m.maxflight = options[:maxflight].to_i
|
94
|
-
m.verbose = options[:verbose]
|
95
|
-
m.debug = options[:debug]
|
96
|
-
|
97
|
-
result = m.run
|
98
|
-
|
99
|
-
if (options[:collapse] and options[:range])
|
100
|
-
# print a collapsed summary
|
101
|
-
out_matches = Hash.new
|
102
|
-
result.each do |r|
|
103
|
-
out = ""
|
104
|
-
out += r[:stdout_buf].chomp if(!r[:stdout_buf].nil?)
|
105
|
-
out += r[:stderr_buf].chomp if(!r[:stderr_buf].nil?)
|
106
|
-
out_matches[out] = [] if out_matches[out].nil?
|
107
|
-
out_matches[out] << command_to_target[r[:command].object_id]
|
108
|
-
end
|
109
|
-
# output => [targets ...]
|
110
|
-
out_matches.each_pair do |k,v|
|
111
|
-
hosts = range.compress v
|
112
|
-
puts "#{hosts}: '#{k.chomp}'"
|
113
|
-
end
|
114
|
-
else
|
115
|
-
# not collapse, print one per host
|
116
|
-
result.each do |r|
|
117
|
-
target = command_to_target[r[:command].object_id]
|
118
|
-
out = ""
|
119
|
-
out += r[:stdout_buf].chomp if(!r[:stdout_buf].nil?)
|
120
|
-
out += r[:stderr_buf].chomp if(!r[:stderr_buf].nil?)
|
121
|
-
if (r[:retval] == 0 )
|
122
|
-
puts "#{target}:SUCCESS: #{out}\n"
|
123
|
-
else
|
124
|
-
exit_code = r[:retval].exitstatus.to_s if(!r[:retval].nil?)
|
125
|
-
puts "#{target}:FAILURE[#{exit_code}]: #{out}\n"
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
31
|
+
cli.output result
|
data/lib/cli.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative './mcmd'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Implements what is shared
|
6
|
+
# between mssh and mcmd.
|
7
|
+
class CommonCli
|
8
|
+
attr_accessor :options, :result, :command_to_target, :targets
|
9
|
+
attr_writer :extra_options
|
10
|
+
|
11
|
+
def initialize(defaults)
|
12
|
+
@options = defaults
|
13
|
+
@options[:hostname_token] = 'HOSTNAME'
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Parses a list of arguments, and yells
|
18
|
+
# at us if any are wrong.
|
19
|
+
def parse(argv)
|
20
|
+
|
21
|
+
@defs = Hash[@options.map{|k,v| [k, " (default: #{v})"] } ]
|
22
|
+
|
23
|
+
optparse = OptionParser.new do |opts|
|
24
|
+
opts.on('-r', '--range RANGE', 'currently takes a CSV list' + @defs[:range].to_s ) do |arg|
|
25
|
+
@options[:range] = arg
|
26
|
+
end
|
27
|
+
opts.on('--hostlist x,y,z', Array, 'List of hostnames to execute on' + @defs[:hostlist].to_s) do |arg|
|
28
|
+
@options[:hostlist] = arg
|
29
|
+
end
|
30
|
+
opts.on('-f', '--file FILE', 'List of hostnames in a FILE use (/dev/stdin) for reading from stdin' + @defs[:file].to_s) do |arg|
|
31
|
+
@options[:file] = arg
|
32
|
+
end
|
33
|
+
opts.on('-m', '--maxflight 200', 'How many subprocesses? 50 by default' + @defs[:maxflight].to_s) do |arg|
|
34
|
+
@options[:maxflight] = arg
|
35
|
+
end
|
36
|
+
opts.on('-t', '--timeout 60', 'How many seconds may each individual process take? 0 for no timeout' + @defs[:timeout].to_s) do |arg|
|
37
|
+
@options[:timeout] = arg
|
38
|
+
end
|
39
|
+
opts.on('-g', '--global_timeout 0', 'How many seconds for the whole shebang 0 for no timeout' + @defs[:global_timeout].to_s) do |arg|
|
40
|
+
@options[:global_timeout] = arg
|
41
|
+
end
|
42
|
+
opts.on('-c', '--collapse', "Collapse similar output " + @defs[:collapse].to_s) do |arg|
|
43
|
+
@options[:collapse] = arg
|
44
|
+
end
|
45
|
+
opts.on('-v', '--verbose', "Verbose output" + @defs[:verbose].to_s) do |arg|
|
46
|
+
@options[:verbose] = arg
|
47
|
+
end
|
48
|
+
opts.on('-d', '--debug', "Debug output" + @defs[:debug].to_s) do |arg|
|
49
|
+
@options[:debug] = arg
|
50
|
+
end
|
51
|
+
opts.on('--json', "Output results as JSON" + @defs[:json_out].to_s) do |arg|
|
52
|
+
@options[:json_out] = arg
|
53
|
+
end
|
54
|
+
opts.on('--hntoken HOSTNAME', "Token used for HOSTNAME substitution" + @defs[:hostname_token].to_s) do |arg|
|
55
|
+
@options[:hostname_token] = arg
|
56
|
+
end
|
57
|
+
|
58
|
+
@extra_options.call(opts, @options) if !@extra_options.nil?
|
59
|
+
|
60
|
+
# option to merge stdin/stdout into one buf? how should this work?
|
61
|
+
# option to ignore as-we-go yield output - this is off by default now except for success/fail
|
62
|
+
end
|
63
|
+
|
64
|
+
optparse.parse! argv
|
65
|
+
|
66
|
+
if options[:range] || options[:collapse]
|
67
|
+
require 'rangeclient'
|
68
|
+
@rangeclient = Range::Client.new
|
69
|
+
end
|
70
|
+
|
71
|
+
## Get targets from -r or -f or --hostlist
|
72
|
+
@targets = []
|
73
|
+
if (options[:range].nil? and options[:file].nil? and options[:hostlist].nil?)
|
74
|
+
raise "Error, need -r or -f or --hostlist option"
|
75
|
+
end
|
76
|
+
|
77
|
+
if (!options[:range].nil?)
|
78
|
+
@targets.push *@rangeclient.expand(options[:range])
|
79
|
+
end
|
80
|
+
|
81
|
+
if (!options[:file].nil?)
|
82
|
+
targets_fd = File.open(options[:file])
|
83
|
+
targets_fd.read.each_line { |x| @targets << x.chomp }
|
84
|
+
end
|
85
|
+
|
86
|
+
if (!options[:hostlist].nil?)
|
87
|
+
@targets.push *options[:hostlist]
|
88
|
+
end
|
89
|
+
|
90
|
+
raise "no targets specified. Check your -r, -f or --hostlist inputs" if @targets.size.zero?
|
91
|
+
raise "need command to run" if argv.size.zero?
|
92
|
+
raise "too many arguments" if argv.size != 1
|
93
|
+
|
94
|
+
@command = argv.first
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
def runcmd
|
99
|
+
|
100
|
+
m = MultipleCmd.new
|
101
|
+
|
102
|
+
# We let the caller build the command
|
103
|
+
m.commands = @targets.map do |t|
|
104
|
+
yield(@command.gsub(@options[:hostname_token], t), t)
|
105
|
+
end
|
106
|
+
|
107
|
+
@command_to_target = Hash.new
|
108
|
+
@targets.size.times do |i|
|
109
|
+
@command_to_target[m.commands[i].object_id] = @targets[i]
|
110
|
+
end
|
111
|
+
|
112
|
+
m.yield_startcmd = lambda { |p| puts "#{@command_to_target[p.command.object_id]}: starting" } if @options[:verbose]
|
113
|
+
m.yield_wait = lambda { |p| puts "#{p.success? ? 'SUCCESS' : 'FAILURE'} #{@command_to_target[p.command.object_id]}: '#{p.stdout_buf}'" } if @options[:verbose]
|
114
|
+
# todo, from mssh m.yield_wait = lambda { |p| puts "#{@command_to_target[p.command.object_id]}: finished" } if @options[:verbose]
|
115
|
+
|
116
|
+
# was commented out in mcmd already: m.yield_proc_timeout = lambda { |p| puts "am killing #{p.inspect}"}
|
117
|
+
|
118
|
+
m.perchild_timeout = @options[:timeout].to_i
|
119
|
+
m.global_timeout = @options[:global_timeout].to_i
|
120
|
+
m.maxflight = @options[:maxflight].to_i
|
121
|
+
m.verbose = @options[:verbose]
|
122
|
+
m.debug = @options[:debug]
|
123
|
+
|
124
|
+
return m.run
|
125
|
+
end
|
126
|
+
|
127
|
+
def output result
|
128
|
+
|
129
|
+
## Print as JSON array
|
130
|
+
if (@options[:json_out])
|
131
|
+
require 'json'
|
132
|
+
puts JSON.generate(result)
|
133
|
+
return
|
134
|
+
end
|
135
|
+
|
136
|
+
# Concat stdout / stderr -> :all_buf
|
137
|
+
result.each do |r|
|
138
|
+
r[:all_buf] = ""
|
139
|
+
r[:all_buf] += r[:stdout_buf].chomp if(!r[:stdout_buf].nil?)
|
140
|
+
r[:all_buf] += r[:stderr_buf].chomp if(!r[:stderr_buf].nil?)
|
141
|
+
end
|
142
|
+
|
143
|
+
## Collapse similar results
|
144
|
+
if @options[:collapse]
|
145
|
+
stdout_matches_success = Hash.new
|
146
|
+
stdout_matches_failure = Hash.new
|
147
|
+
result.each do |r|
|
148
|
+
|
149
|
+
if r[:retval].success?
|
150
|
+
stdout_matches_success[r[:all_buf]] = [] if stdout_matches_success[r[:all_buf]].nil?
|
151
|
+
stdout_matches_success[r[:all_buf]] << @command_to_target[r[:command].object_id]
|
152
|
+
else
|
153
|
+
stdout_matches_failure[r[:all_buf]] = [] if stdout_matches_failure[r[:all_buf]].nil?
|
154
|
+
stdout_matches_failure[r[:all_buf]] << @command_to_target[r[:command].object_id]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
# output => [targets ...]
|
158
|
+
stdout_matches_success.each_pair do |k,v|
|
159
|
+
hosts = @rangeclient.compress v
|
160
|
+
# puts "#{hosts}: '#{k.chomp}'"
|
161
|
+
puts "SUCCESS: #{hosts}: #{k}"
|
162
|
+
end
|
163
|
+
stdout_matches_failure.each_pair do |k,v|
|
164
|
+
hosts = @rangeclient.compress v
|
165
|
+
puts "FAILURE: #{hosts}: #{k}"
|
166
|
+
end
|
167
|
+
|
168
|
+
## Dont collapse similar resutls
|
169
|
+
else
|
170
|
+
result.each do |r|
|
171
|
+
target = @command_to_target[r[:command].object_id]
|
172
|
+
if (r[:retval].success?)
|
173
|
+
puts "#{target}:SUCCESS: #{r[:all_buf]}\n"
|
174
|
+
else
|
175
|
+
exit_code = r[:retval].exitstatus.to_s if(!r[:retval].nil?)
|
176
|
+
puts "#{target}:FAILURE[#{exit_code}]: #{r[:all_buf]}\n"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
data/lib/mcmd.rb
CHANGED
@@ -10,6 +10,9 @@ class MultipleCmd
|
|
10
10
|
attr_accessor :yield_wait, :yield_startcmd, :debug, :yield_proc_timeout
|
11
11
|
attr_accessor :verbose, :poll_period, :max_read_size
|
12
12
|
|
13
|
+
# TODO: move these to cattrs on MultipleCmd::SubProc
|
14
|
+
attr_accessor :subproc_by_pid, :subproc_by_fd
|
15
|
+
|
13
16
|
def initialize
|
14
17
|
# these are re-initialized after every run
|
15
18
|
@subproc_by_pid = Hash.new
|
@@ -28,9 +31,9 @@ class MultipleCmd
|
|
28
31
|
|
29
32
|
def noshell_exec(cmd)
|
30
33
|
if cmd.length == 1
|
31
|
-
|
34
|
+
exec([cmd[0], cmd[0]])
|
32
35
|
else
|
33
|
-
|
36
|
+
exec([cmd[0], cmd[0]], *cmd[1..-1])
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
@@ -48,18 +51,18 @@ class MultipleCmd
|
|
48
51
|
subproc.command = cmd
|
49
52
|
|
50
53
|
pid = fork
|
51
|
-
if
|
54
|
+
if pid
|
52
55
|
# parent
|
53
56
|
# for mapping to subproc by pid
|
54
57
|
subproc.pid = pid
|
55
|
-
|
58
|
+
subproc_by_pid[pid] = subproc
|
56
59
|
# for mapping to subproc by i/o handle (returned from select)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
subproc_by_fd[stdin_rd] = subproc
|
61
|
+
subproc_by_fd[stdin_wr] = subproc
|
62
|
+
subproc_by_fd[stdout_rd] = subproc
|
63
|
+
subproc_by_fd[stdout_wr] = subproc
|
64
|
+
subproc_by_fd[stderr_rd] = subproc
|
65
|
+
subproc_by_fd[stderr_wr] = subproc
|
63
66
|
|
64
67
|
self.yield_startcmd.call(subproc) unless self.yield_startcmd.nil?
|
65
68
|
else
|
@@ -76,10 +79,10 @@ class MultipleCmd
|
|
76
79
|
def process_read_fds(read_fds)
|
77
80
|
read_fds.each do |fd|
|
78
81
|
# read available bytes, add to the subproc's read buf
|
79
|
-
if not
|
82
|
+
if not subproc_by_fd.has_key?(fd)
|
80
83
|
raise "Select returned a fd which I have not seen! fd: #{fd.inspect}"
|
81
84
|
end
|
82
|
-
subproc =
|
85
|
+
subproc = subproc_by_fd[fd]
|
83
86
|
buf = ""
|
84
87
|
begin
|
85
88
|
buf = fd.sysread(4096)
|
@@ -119,8 +122,8 @@ class MultipleCmd
|
|
119
122
|
end # process_read_fds()
|
120
123
|
def process_write_fds(write_fds)
|
121
124
|
write_fds.each do |fd|
|
122
|
-
raise "working on an unknown fd #{fd}" unless
|
123
|
-
subproc =
|
125
|
+
raise "working on an unknown fd #{fd}" unless subproc_by_fd.has_key?(fd)
|
126
|
+
subproc = subproc_by_fd[fd]
|
124
127
|
buf = ""
|
125
128
|
# add writing here, todo. not core feature
|
126
129
|
end
|
@@ -130,8 +133,8 @@ class MultipleCmd
|
|
130
133
|
|
131
134
|
# iterate and service fds in child procs, collect data and status
|
132
135
|
def service_subprocess_io
|
133
|
-
write_fds =
|
134
|
-
read_fds =
|
136
|
+
write_fds = subproc_by_pid.values.select {|x| not x.stdin_fd.nil? and not x.terminated}.map {|x| x.stdin_fd}
|
137
|
+
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
|
135
138
|
|
136
139
|
read_fds, write_fds, err_fds = IO.select_using_poll(read_fds, write_fds, nil, self.poll_period)
|
137
140
|
|
@@ -143,7 +146,7 @@ class MultipleCmd
|
|
143
146
|
|
144
147
|
def process_timeouts
|
145
148
|
now = Time.now.to_i
|
146
|
-
|
149
|
+
subproc_by_pid.values.each do |p|
|
147
150
|
if ((now - p.time_start) > self.perchild_timeout) and self.perchild_timeout > 0
|
148
151
|
# expire this child process
|
149
152
|
|
@@ -155,9 +158,9 @@ class MultipleCmd
|
|
155
158
|
|
156
159
|
def kill_process(p)
|
157
160
|
# do not remove from pid list until waited on
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
+
subproc_by_fd.delete(p.stdin_fd)
|
162
|
+
subproc_by_fd.delete(p.stdout_fd)
|
163
|
+
subproc_by_fd.delete(p.stderr_fd)
|
161
164
|
# must kill after deleting from maps
|
162
165
|
# kill closes fds
|
163
166
|
p.kill
|
@@ -168,7 +171,7 @@ class MultipleCmd
|
|
168
171
|
done = false
|
169
172
|
while not done
|
170
173
|
# start up as many as maxflight processes
|
171
|
-
while
|
174
|
+
while subproc_by_pid.length < self.maxflight and not @commands.empty?
|
172
175
|
# take one from @commands and start it
|
173
176
|
commands = @commands.shift
|
174
177
|
self.add_subprocess(commands)
|
@@ -179,19 +182,19 @@ class MultipleCmd
|
|
179
182
|
self.process_timeouts
|
180
183
|
# service process cleanup
|
181
184
|
self.wait
|
182
|
-
puts "have #{
|
185
|
+
puts "have #{subproc_by_pid.length} left to go" if self.debug
|
183
186
|
# if we have nothing in flight (active pid)
|
184
187
|
# and nothing pending on the input list
|
185
188
|
# then we're done
|
186
|
-
if
|
189
|
+
if subproc_by_pid.length.zero? and @commands.empty?
|
187
190
|
done = true
|
188
191
|
end
|
189
192
|
end
|
190
193
|
|
191
194
|
data = self.return_rundata
|
192
195
|
# these are re-initialized after every run
|
193
|
-
|
194
|
-
|
196
|
+
subproc_by_pid = Hash.new
|
197
|
+
subproc_by_fd = Hash.new
|
195
198
|
@processed_commands = []
|
196
199
|
# end items which are re-initialized
|
197
200
|
return data
|
@@ -226,10 +229,10 @@ class MultipleCmd
|
|
226
229
|
else
|
227
230
|
# pid is now gone. remove from subproc_by_pid and
|
228
231
|
# add to the processed commands list
|
229
|
-
p =
|
232
|
+
p = subproc_by_pid[pid]
|
230
233
|
p.time_end = Time.now.to_i
|
231
234
|
p.retval = $?
|
232
|
-
|
235
|
+
subproc_by_pid.delete(pid)
|
233
236
|
@processed_commands << p
|
234
237
|
just_reaped << p
|
235
238
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Miller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
description: Simple library for running jobs and sshing to many hosts at once.
|
42
56
|
email:
|
43
57
|
- github@squareup.com
|
@@ -52,6 +66,7 @@ files:
|
|
52
66
|
- README.md
|
53
67
|
- bin/mcmd
|
54
68
|
- bin/mssh
|
69
|
+
- lib/cli.rb
|
55
70
|
- lib/mcmd.rb
|
56
71
|
homepage: http://github.com/square/mssh
|
57
72
|
licenses: []
|
@@ -73,9 +88,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
88
|
version: 1.3.6
|
74
89
|
requirements: []
|
75
90
|
rubyforge_project:
|
76
|
-
rubygems_version: 2.
|
91
|
+
rubygems_version: 2.5.1
|
77
92
|
signing_key:
|
78
93
|
specification_version: 4
|
79
94
|
summary: Parallel ssh and command execution.
|
80
95
|
test_files: []
|
81
|
-
has_rdoc:
|