sshkit 1.7.1 → 1.8.0

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 (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