sshkit 1.7.1 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -2
- data/BREAKING_API_WISHLIST.md +14 -0
- data/CHANGELOG.md +74 -0
- data/CONTRIBUTING.md +43 -0
- data/EXAMPLES.md +265 -169
- data/Gemfile +7 -0
- data/README.md +274 -9
- data/RELEASING.md +16 -8
- data/Rakefile +8 -0
- data/lib/sshkit.rb +0 -9
- data/lib/sshkit/all.rb +6 -4
- data/lib/sshkit/backends/abstract.rb +42 -42
- data/lib/sshkit/backends/connection_pool.rb +57 -8
- data/lib/sshkit/backends/local.rb +21 -50
- data/lib/sshkit/backends/netssh.rb +45 -98
- data/lib/sshkit/backends/printer.rb +3 -23
- data/lib/sshkit/backends/skipper.rb +4 -8
- data/lib/sshkit/color.rb +51 -20
- data/lib/sshkit/command.rb +68 -47
- data/lib/sshkit/configuration.rb +38 -5
- data/lib/sshkit/deprecation_logger.rb +17 -0
- data/lib/sshkit/formatters/abstract.rb +28 -4
- data/lib/sshkit/formatters/black_hole.rb +1 -2
- data/lib/sshkit/formatters/dot.rb +3 -10
- data/lib/sshkit/formatters/pretty.rb +31 -56
- data/lib/sshkit/formatters/simple_text.rb +6 -44
- data/lib/sshkit/host.rb +5 -6
- data/lib/sshkit/logger.rb +0 -1
- data/lib/sshkit/mapping_interaction_handler.rb +47 -0
- data/lib/sshkit/runners/parallel.rb +1 -1
- data/lib/sshkit/runners/sequential.rb +1 -1
- data/lib/sshkit/version.rb +1 -1
- data/sshkit.gemspec +0 -1
- data/test/functional/backends/test_local.rb +14 -1
- data/test/functional/backends/test_netssh.rb +58 -50
- data/test/helper.rb +2 -2
- data/test/unit/backends/test_abstract.rb +145 -0
- data/test/unit/backends/test_connection_pool.rb +27 -2
- data/test/unit/backends/test_printer.rb +47 -47
- data/test/unit/formatters/test_custom.rb +65 -0
- data/test/unit/formatters/test_dot.rb +25 -32
- data/test/unit/formatters/test_pretty.rb +114 -22
- data/test/unit/formatters/test_simple_text.rb +83 -0
- data/test/unit/test_color.rb +69 -5
- data/test/unit/test_command.rb +53 -18
- data/test/unit/test_command_map.rb +0 -4
- data/test/unit/test_configuration.rb +47 -7
- data/test/unit/test_coordinator.rb +45 -52
- data/test/unit/test_deprecation_logger.rb +38 -0
- data/test/unit/test_host.rb +3 -4
- data/test/unit/test_logger.rb +0 -1
- data/test/unit/test_mapping_interaction_handler.rb +101 -0
- metadata +37 -41
- data/lib/sshkit/utils/capture_output_methods.rb +0 -13
- data/test/functional/test_coordinator.rb +0 -17
@@ -1,5 +1,19 @@
|
|
1
1
|
require "thread"
|
2
2
|
|
3
|
+
# Since we call to_s on new_connection_args and use that as a hash
|
4
|
+
# We need to make sure the memory address of the object is not used as part of the key
|
5
|
+
# Otherwise identical objects with different memory address won't get a hash hit.
|
6
|
+
# In the case of proxy commands, this can lead to proxy processes leaking
|
7
|
+
# And in severe cases can cause deploys to fail due to default file descriptor limits
|
8
|
+
# An alternate solution would be to use a different means of generating hash keys
|
9
|
+
module Net; module SSH; module Proxy
|
10
|
+
class Command
|
11
|
+
def inspect
|
12
|
+
@command_line_template
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end;end;end
|
16
|
+
|
3
17
|
module SSHKit
|
4
18
|
|
5
19
|
module Backend
|
@@ -15,18 +29,34 @@ module SSHKit
|
|
15
29
|
end
|
16
30
|
|
17
31
|
def checkout(*new_connection_args, &block)
|
18
|
-
|
32
|
+
entry = nil
|
19
33
|
key = new_connection_args.to_s
|
20
|
-
|
21
|
-
|
22
|
-
|
34
|
+
if idle_timeout
|
35
|
+
prune_expired
|
36
|
+
entry = find_live_entry(key)
|
37
|
+
end
|
38
|
+
entry || create_new_entry(new_connection_args, key, &block)
|
23
39
|
end
|
24
40
|
|
25
41
|
def checkin(entry)
|
26
|
-
|
42
|
+
if idle_timeout
|
43
|
+
prune_expired
|
44
|
+
entry.expires_at = Time.now + idle_timeout
|
45
|
+
@mutex.synchronize do
|
46
|
+
@pool[entry.key] ||= []
|
47
|
+
@pool[entry.key] << entry
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def close_connections
|
27
53
|
@mutex.synchronize do
|
28
|
-
@pool
|
29
|
-
|
54
|
+
@pool.values.flatten.map(&:connection).uniq.each do |conn|
|
55
|
+
if conn.respond_to?(:closed?) && conn.respond_to?(:close)
|
56
|
+
conn.close unless conn.closed?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@pool.clear
|
30
60
|
end
|
31
61
|
end
|
32
62
|
|
@@ -36,10 +66,25 @@ module SSHKit
|
|
36
66
|
|
37
67
|
private
|
38
68
|
|
69
|
+
def prune_expired
|
70
|
+
@mutex.synchronize do
|
71
|
+
@pool.each_value do |entries|
|
72
|
+
entries.collect! do |entry|
|
73
|
+
if entry.expired?
|
74
|
+
entry.close unless entry.closed?
|
75
|
+
nil
|
76
|
+
else
|
77
|
+
entry
|
78
|
+
end
|
79
|
+
end.compact!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
39
84
|
def find_live_entry(key)
|
40
85
|
@mutex.synchronize do
|
41
86
|
return nil unless @pool.key?(key)
|
42
|
-
while entry = @pool[key].shift
|
87
|
+
while (entry = @pool[key].shift)
|
43
88
|
return entry if entry.live?
|
44
89
|
end
|
45
90
|
end
|
@@ -61,6 +106,10 @@ module SSHKit
|
|
61
106
|
expires_at && Time.now > expires_at
|
62
107
|
end
|
63
108
|
|
109
|
+
def close
|
110
|
+
connection.respond_to?(:close) && connection.close
|
111
|
+
end
|
112
|
+
|
64
113
|
def closed?
|
65
114
|
connection.respond_to?(:closed?) && connection.closed?
|
66
115
|
end
|
@@ -4,35 +4,14 @@ module SSHKit
|
|
4
4
|
|
5
5
|
module Backend
|
6
6
|
|
7
|
-
class Local <
|
7
|
+
class Local < Abstract
|
8
8
|
|
9
9
|
def initialize(_ = nil, &block)
|
10
10
|
@host = Host.new(:local) # just for logging
|
11
11
|
@block = block
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
instance_exec(@host, &@block)
|
16
|
-
end
|
17
|
-
|
18
|
-
def test(*args)
|
19
|
-
options = args.extract_options!.merge(
|
20
|
-
raise_on_non_zero_exit: false,
|
21
|
-
verbosity: Logger::DEBUG
|
22
|
-
)
|
23
|
-
_execute(*[*args, options]).success?
|
24
|
-
end
|
25
|
-
|
26
|
-
def execute(*args)
|
27
|
-
_execute(*args).success?
|
28
|
-
end
|
29
|
-
|
30
|
-
def capture(*args)
|
31
|
-
options = { verbosity: Logger::DEBUG }.merge(args.extract_options!)
|
32
|
-
_execute(*[*args, options]).full_stdout
|
33
|
-
end
|
34
|
-
|
35
|
-
def upload!(local, remote, options = {})
|
14
|
+
def upload!(local, remote, _options = {})
|
36
15
|
if local.is_a?(String)
|
37
16
|
FileUtils.cp(local, remote)
|
38
17
|
else
|
@@ -42,7 +21,7 @@ module SSHKit
|
|
42
21
|
end
|
43
22
|
end
|
44
23
|
|
45
|
-
def download!(remote, local=nil,
|
24
|
+
def download!(remote, local=nil, _options = {})
|
46
25
|
if local.nil?
|
47
26
|
FileUtils.cp(remote, File.basename(remote))
|
48
27
|
else
|
@@ -54,40 +33,32 @@ module SSHKit
|
|
54
33
|
|
55
34
|
private
|
56
35
|
|
57
|
-
def
|
58
|
-
|
59
|
-
output << cmd
|
60
|
-
|
61
|
-
cmd.started = Time.now
|
36
|
+
def execute_command(cmd)
|
37
|
+
output.log_command_start(cmd)
|
62
38
|
|
63
|
-
|
64
|
-
stdout_thread = Thread.new do
|
65
|
-
while line = stdout.gets do
|
66
|
-
cmd.stdout = line
|
67
|
-
cmd.full_stdout += line
|
39
|
+
cmd.started = Time.now
|
68
40
|
|
69
|
-
|
70
|
-
|
41
|
+
Open3.popen3(cmd.to_command) do |stdin, stdout, stderr, wait_thr|
|
42
|
+
stdout_thread = Thread.new do
|
43
|
+
while (line = stdout.gets) do
|
44
|
+
cmd.on_stdout(stdin, line)
|
45
|
+
output.log_command_data(cmd, :stdout, line)
|
71
46
|
end
|
47
|
+
end
|
72
48
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
output << cmd
|
79
|
-
end
|
49
|
+
stderr_thread = Thread.new do
|
50
|
+
while (line = stderr.gets) do
|
51
|
+
cmd.on_stderr(stdin, line)
|
52
|
+
output.log_command_data(cmd, :stderr, line)
|
80
53
|
end
|
54
|
+
end
|
81
55
|
|
82
|
-
|
83
|
-
|
56
|
+
stdout_thread.join
|
57
|
+
stderr_thread.join
|
84
58
|
|
85
|
-
|
86
|
-
cmd.stdout = ''
|
87
|
-
cmd.stderr = ''
|
59
|
+
cmd.exit_status = wait_thr.value.to_i
|
88
60
|
|
89
|
-
|
90
|
-
end
|
61
|
+
output.log_command_exit(cmd)
|
91
62
|
end
|
92
63
|
end
|
93
64
|
|
@@ -15,29 +15,9 @@ end
|
|
15
15
|
|
16
16
|
module SSHKit
|
17
17
|
|
18
|
-
class Logger
|
19
|
-
|
20
|
-
class Net::SSH::LogLevelShim
|
21
|
-
attr_reader :output
|
22
|
-
def initialize(output)
|
23
|
-
@output = output
|
24
|
-
end
|
25
|
-
def debug(args)
|
26
|
-
output << LogMessage.new(Logger::TRACE, args)
|
27
|
-
end
|
28
|
-
def error(args)
|
29
|
-
output << LogMessage.new(Logger::ERROR, args)
|
30
|
-
end
|
31
|
-
def lwarn(args)
|
32
|
-
output << LogMessage.new(Logger::WARN, args)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
18
|
module Backend
|
39
19
|
|
40
|
-
class Netssh <
|
20
|
+
class Netssh < Abstract
|
41
21
|
|
42
22
|
class Configuration
|
43
23
|
attr_accessor :connection_timeout, :pty
|
@@ -48,35 +28,6 @@ module SSHKit
|
|
48
28
|
end
|
49
29
|
end
|
50
30
|
|
51
|
-
include SSHKit::CommandHelper
|
52
|
-
|
53
|
-
def run
|
54
|
-
instance_exec(host, &@block)
|
55
|
-
end
|
56
|
-
|
57
|
-
def test(*args)
|
58
|
-
options = args.extract_options!.merge(
|
59
|
-
raise_on_non_zero_exit: false,
|
60
|
-
verbosity: Logger::DEBUG
|
61
|
-
)
|
62
|
-
_execute(*[*args, options]).success?
|
63
|
-
end
|
64
|
-
|
65
|
-
def execute(*args)
|
66
|
-
_execute(*args).success?
|
67
|
-
end
|
68
|
-
|
69
|
-
def background(*args)
|
70
|
-
warn "[Deprecated] The background method is deprecated. Blame badly behaved pseudo-daemons!"
|
71
|
-
options = args.extract_options!.merge(run_in_background: true)
|
72
|
-
_execute(*[*args, options]).success?
|
73
|
-
end
|
74
|
-
|
75
|
-
def capture(*args)
|
76
|
-
options = { verbosity: Logger::DEBUG }.merge(args.extract_options!)
|
77
|
-
_execute(*[*args, options]).full_stdout.strip
|
78
|
-
end
|
79
|
-
|
80
31
|
def upload!(local, remote, options = {})
|
81
32
|
summarizer = transfer_summarizer('Uploading')
|
82
33
|
with_ssh do |ssh|
|
@@ -110,7 +61,7 @@ module SSHKit
|
|
110
61
|
def transfer_summarizer(action)
|
111
62
|
last_name = nil
|
112
63
|
last_percentage = nil
|
113
|
-
proc do |
|
64
|
+
proc do |_ch, name, transferred, total|
|
114
65
|
percentage = (transferred.to_f * 100 / total.to_f)
|
115
66
|
unless percentage.nan?
|
116
67
|
message = "#{action} #{name} #{percentage.round(2)}%"
|
@@ -129,56 +80,52 @@ module SSHKit
|
|
129
80
|
end
|
130
81
|
end
|
131
82
|
|
132
|
-
def
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
chan.
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
#
|
160
|
-
#
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
chan.on_eof do |ch|
|
169
|
-
# TODO: chan sends EOF before the exit status has been
|
170
|
-
# writtend
|
171
|
-
end
|
83
|
+
def execute_command(cmd)
|
84
|
+
output.log_command_start(cmd)
|
85
|
+
cmd.started = true
|
86
|
+
exit_status = nil
|
87
|
+
with_ssh do |ssh|
|
88
|
+
ssh.open_channel do |chan|
|
89
|
+
chan.request_pty if Netssh.config.pty
|
90
|
+
chan.exec cmd.to_command do |_ch, _success|
|
91
|
+
chan.on_data do |ch, data|
|
92
|
+
cmd.on_stdout(ch, data)
|
93
|
+
output.log_command_data(cmd, :stdout, data)
|
94
|
+
end
|
95
|
+
chan.on_extended_data do |ch, _type, data|
|
96
|
+
cmd.on_stderr(ch, data)
|
97
|
+
output.log_command_data(cmd, :stderr, data)
|
98
|
+
end
|
99
|
+
chan.on_request("exit-status") do |_ch, data|
|
100
|
+
exit_status = data.read_long
|
101
|
+
end
|
102
|
+
#chan.on_request("exit-signal") do |ch, data|
|
103
|
+
# # TODO: This gets called if the program is killed by a signal
|
104
|
+
# # might also be a worthwhile thing to report
|
105
|
+
# exit_signal = data.read_string.to_i
|
106
|
+
# warn ">>> " + exit_signal.inspect
|
107
|
+
# output.log_command_killed(cmd, exit_signal)
|
108
|
+
#end
|
109
|
+
chan.on_open_failed do |_ch|
|
110
|
+
# TODO: What do do here?
|
111
|
+
# I think we should raise something
|
112
|
+
end
|
113
|
+
chan.on_process do |_ch|
|
114
|
+
# TODO: I don't know if this is useful
|
115
|
+
end
|
116
|
+
chan.on_eof do |_ch|
|
117
|
+
# TODO: chan sends EOF before the exit status has been
|
118
|
+
# writtend
|
172
119
|
end
|
173
|
-
chan.wait
|
174
120
|
end
|
175
|
-
|
176
|
-
end
|
177
|
-
# Set exit_status and log the result upon completion
|
178
|
-
if exit_status
|
179
|
-
cmd.exit_status = exit_status
|
180
|
-
output << cmd
|
121
|
+
chan.wait
|
181
122
|
end
|
123
|
+
ssh.loop
|
124
|
+
end
|
125
|
+
# Set exit_status and log the result upon completion
|
126
|
+
if exit_status
|
127
|
+
cmd.exit_status = exit_status
|
128
|
+
output.log_command_exit(cmd)
|
182
129
|
end
|
183
130
|
end
|
184
131
|
|
@@ -1,35 +1,15 @@
|
|
1
1
|
module SSHKit
|
2
2
|
module Backend
|
3
3
|
|
4
|
+
# Printer is used to implement --dry-run in Capistrano
|
4
5
|
class Printer < Abstract
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
def run
|
9
|
-
instance_exec(host, &@block)
|
7
|
+
def execute_command(cmd)
|
8
|
+
output.log_command_start(cmd)
|
10
9
|
end
|
11
10
|
|
12
|
-
def execute(*args)
|
13
|
-
command(*args).tap do |cmd|
|
14
|
-
output << cmd
|
15
|
-
end
|
16
|
-
end
|
17
11
|
alias :upload! :execute
|
18
12
|
alias :download! :execute
|
19
|
-
alias :test :execute
|
20
|
-
|
21
|
-
def capture(*args)
|
22
|
-
String.new.tap { execute(*args) }
|
23
|
-
end
|
24
|
-
alias :capture! :capture
|
25
|
-
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def output
|
30
|
-
SSHKit.config.output
|
31
|
-
end
|
32
|
-
|
33
13
|
end
|
34
14
|
end
|
35
15
|
end
|
@@ -1,30 +1,26 @@
|
|
1
1
|
module SSHKit
|
2
2
|
module Backend
|
3
3
|
|
4
|
-
class Skipper <
|
4
|
+
class Skipper < Abstract
|
5
5
|
|
6
6
|
def initialize(&block)
|
7
7
|
@block = block
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
warn "[SKIPPING] No Matching Host for #{cmd}"
|
13
|
-
end
|
10
|
+
def execute_command(cmd)
|
11
|
+
warn "[SKIPPING] No Matching Host for #{cmd}"
|
14
12
|
end
|
15
13
|
alias :upload! :execute
|
16
14
|
alias :download! :execute
|
17
15
|
alias :test :execute
|
18
|
-
alias :invoke :execute
|
19
16
|
|
20
|
-
def info(
|
17
|
+
def info(_messages)
|
21
18
|
# suppress all messages except `warn`
|
22
19
|
end
|
23
20
|
alias :log :info
|
24
21
|
alias :fatal :info
|
25
22
|
alias :error :info
|
26
23
|
alias :debug :info
|
27
|
-
alias :trace :info
|
28
24
|
|
29
25
|
end
|
30
26
|
end
|