sshkit 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -2
  3. data/BREAKING_API_WISHLIST.md +14 -0
  4. data/CHANGELOG.md +74 -0
  5. data/CONTRIBUTING.md +43 -0
  6. data/EXAMPLES.md +265 -169
  7. data/Gemfile +7 -0
  8. data/README.md +274 -9
  9. data/RELEASING.md +16 -8
  10. data/Rakefile +8 -0
  11. data/lib/sshkit.rb +0 -9
  12. data/lib/sshkit/all.rb +6 -4
  13. data/lib/sshkit/backends/abstract.rb +42 -42
  14. data/lib/sshkit/backends/connection_pool.rb +57 -8
  15. data/lib/sshkit/backends/local.rb +21 -50
  16. data/lib/sshkit/backends/netssh.rb +45 -98
  17. data/lib/sshkit/backends/printer.rb +3 -23
  18. data/lib/sshkit/backends/skipper.rb +4 -8
  19. data/lib/sshkit/color.rb +51 -20
  20. data/lib/sshkit/command.rb +68 -47
  21. data/lib/sshkit/configuration.rb +38 -5
  22. data/lib/sshkit/deprecation_logger.rb +17 -0
  23. data/lib/sshkit/formatters/abstract.rb +28 -4
  24. data/lib/sshkit/formatters/black_hole.rb +1 -2
  25. data/lib/sshkit/formatters/dot.rb +3 -10
  26. data/lib/sshkit/formatters/pretty.rb +31 -56
  27. data/lib/sshkit/formatters/simple_text.rb +6 -44
  28. data/lib/sshkit/host.rb +5 -6
  29. data/lib/sshkit/logger.rb +0 -1
  30. data/lib/sshkit/mapping_interaction_handler.rb +47 -0
  31. data/lib/sshkit/runners/parallel.rb +1 -1
  32. data/lib/sshkit/runners/sequential.rb +1 -1
  33. data/lib/sshkit/version.rb +1 -1
  34. data/sshkit.gemspec +0 -1
  35. data/test/functional/backends/test_local.rb +14 -1
  36. data/test/functional/backends/test_netssh.rb +58 -50
  37. data/test/helper.rb +2 -2
  38. data/test/unit/backends/test_abstract.rb +145 -0
  39. data/test/unit/backends/test_connection_pool.rb +27 -2
  40. data/test/unit/backends/test_printer.rb +47 -47
  41. data/test/unit/formatters/test_custom.rb +65 -0
  42. data/test/unit/formatters/test_dot.rb +25 -32
  43. data/test/unit/formatters/test_pretty.rb +114 -22
  44. data/test/unit/formatters/test_simple_text.rb +83 -0
  45. data/test/unit/test_color.rb +69 -5
  46. data/test/unit/test_command.rb +53 -18
  47. data/test/unit/test_command_map.rb +0 -4
  48. data/test/unit/test_configuration.rb +47 -7
  49. data/test/unit/test_coordinator.rb +45 -52
  50. data/test/unit/test_deprecation_logger.rb +38 -0
  51. data/test/unit/test_host.rb +3 -4
  52. data/test/unit/test_logger.rb +0 -1
  53. data/test/unit/test_mapping_interaction_handler.rb +101 -0
  54. metadata +37 -41
  55. data/lib/sshkit/utils/capture_output_methods.rb +0 -13
  56. 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
- # Optimization: completely bypass the pool if idle_timeout is zero.
32
+ entry = nil
19
33
  key = new_connection_args.to_s
20
- return create_new_entry(new_connection_args, key, &block) if idle_timeout == 0
21
-
22
- find_live_entry(key) || create_new_entry(new_connection_args, key, &block)
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
- entry.expires_at = Time.now + idle_timeout if idle_timeout
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[entry.key] ||= []
29
- @pool[entry.key] << entry
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 < Printer
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 run
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, options = {})
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 _execute(*args)
58
- command(*args).tap do |cmd|
59
- output << cmd
60
-
61
- cmd.started = Time.now
36
+ def execute_command(cmd)
37
+ output.log_command_start(cmd)
62
38
 
63
- Open3.popen3(cmd.to_command) do |stdin, stdout, stderr, wait_thr|
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
- output << cmd
70
- end
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
- stderr_thread = Thread.new do
74
- while line = stderr.gets do
75
- cmd.stderr = line
76
- cmd.full_stderr += line
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
- stdout_thread.join
83
- stderr_thread.join
56
+ stdout_thread.join
57
+ stderr_thread.join
84
58
 
85
- cmd.exit_status = wait_thr.value.to_i
86
- cmd.stdout = ''
87
- cmd.stderr = ''
59
+ cmd.exit_status = wait_thr.value.to_i
88
60
 
89
- output << cmd
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 < Printer
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 |ch, name, transferred, total|
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 _execute(*args)
133
- command(*args).tap do |cmd|
134
- output << cmd
135
- cmd.started = true
136
- exit_status = nil
137
- with_ssh do |ssh|
138
- ssh.open_channel do |chan|
139
- chan.request_pty if Netssh.config.pty
140
- chan.exec cmd.to_command do |ch, success|
141
- chan.on_data do |ch, data|
142
- cmd.stdout = data
143
- cmd.full_stdout += data
144
- output << cmd
145
- end
146
- chan.on_extended_data do |ch, type, data|
147
- cmd.stderr = data
148
- cmd.full_stderr += data
149
- output << cmd
150
- end
151
- chan.on_request("exit-status") do |ch, data|
152
- exit_status = data.read_long
153
- end
154
- #chan.on_request("exit-signal") do |ch, data|
155
- # # TODO: This gets called if the program is killed by a signal
156
- # # might also be a worthwhile thing to report
157
- # exit_signal = data.read_string.to_i
158
- # warn ">>> " + exit_signal.inspect
159
- # output << cmd
160
- #end
161
- chan.on_open_failed do |ch|
162
- # TODO: What do do here?
163
- # I think we should raise something
164
- end
165
- chan.on_process do |ch|
166
- # TODO: I don't know if this is useful
167
- end
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
- ssh.loop
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
- include SSHKit::CommandHelper
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 < Printer
4
+ class Skipper < Abstract
5
5
 
6
6
  def initialize(&block)
7
7
  @block = block
8
8
  end
9
9
 
10
- def execute(*args)
11
- command(*args).tap do |cmd|
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(messages)
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