net-ssh-session 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ *.swp
4
+ *.tmproj
5
+ *~
6
+ .DS_Store
7
+ .\#*
8
+ .bundle
9
+ .config
10
+ .yardoc
11
+ Gemfile.lock
12
+ InstalledFiles
13
+ \#*
14
+ _yardoc
15
+ coverage
16
+ doc/
17
+ lib/bundler/man
18
+ pkg
19
+ rdoc
20
+ spec/reports
21
+ test/tmp
22
+ test/version_tmp
23
+ tmp
24
+ tmtags
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=nested
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - ruby-head
7
+ - jruby-18mode
8
+ - jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 Dan Sosedoff.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Net::SSH::Session
2
+
3
+ A wrapper on top of `Net::SSH` and `Net::SSH::Shell` to provide a set of tools for ssh sessions
4
+
5
+ ## Install
6
+
7
+ Install with rubygems:
8
+
9
+ ```
10
+ gem install net-ssh-session
11
+ ```
12
+
13
+ Install with bundler:
14
+
15
+ ```
16
+ gem 'net-ssh-session', :github => 'sosedoff/net-ssh-session'
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Basic usage:
22
+
23
+ ```ruby
24
+ require 'net/ssh/session'
25
+
26
+ # Establish a connection
27
+ session = Net::SSH::Session.new(host, user, password)
28
+ session.open
29
+
30
+ # If you want to set a connection timeout in seconds
31
+ # it will raise Timeout::Error
32
+ session.open(10)
33
+
34
+ # Execute a remote command
35
+ result = session.run("free -m")
36
+
37
+ # Net::SSH::SessionCommand helpers
38
+ result.success? # => true
39
+ result.failure? # => false
40
+ result.exit_code # => 0
41
+ result.output # => command output text
42
+ result.duration # => execution time, seconds
43
+
44
+ # Capture command output
45
+ session.capture('cat /etc/lsb-release')
46
+
47
+ # File helpers
48
+ session.file_exists?('/path')
49
+ session.directory_exists?('/path')
50
+ session.read_file('/path')
51
+
52
+ # Process helpers
53
+ session.process_exists?(PID)
54
+ session.kill_process(PID) # => true
55
+ session.kill_process(PID, 'SIGINT') # => false
56
+ session.last_exit_code # => 1
57
+
58
+ # Environment helpers
59
+ session.env('RAILS_ENV') # => production
60
+ session.export('RAILS_ENV', 'production')
61
+ session.export_hash(
62
+ 'RAILS_ENV' => 'test',
63
+ 'RACK_ENV' => 'test'
64
+ )
65
+
66
+ # Execute a batch of commands
67
+ session.run_multiple(
68
+ 'git clone git@foobar.com:project.git',
69
+ 'cd project',
70
+ 'bundle install',
71
+ 'rake test'
72
+ )
73
+
74
+ # Execute with time limit (10s)
75
+ begin
76
+ session.with_timeout(10) do
77
+ session.run('some long job')
78
+ end
79
+ rescue Timeout::Error
80
+ puts "Operation took too long :("
81
+ end
82
+
83
+ # Execute a long command and show ongoing process
84
+ session.run("rake test") do |str|
85
+ puts str
86
+ end
87
+
88
+ # Get history, returns an array with Net::SSH::SessionCommand objects
89
+ session.history.each do |cmd|
90
+ puts cmd.to_s # => I, [2012-11-08T00:10:48.229986 #51878] INFO -- : [bundle install --path .bundle] => 10, 35 bytes
91
+ if cmd.success?
92
+ # do your thing
93
+ end
94
+ end
95
+
96
+ # Close current session
97
+ session.close
98
+ ```
99
+
100
+ ## Advanced Usage
101
+
102
+ ### Running multiple commands
103
+
104
+ By default multiple command execution will not break if one of the commands fails. If you want to break the chain on the first failure, supply `:break => true` option:
105
+
106
+ ```ruby
107
+ session.run_multiple(commands, :break => true)
108
+ ```
109
+
110
+ To get each command result after execution, you can supply a block:
111
+
112
+ ```ruby
113
+ session.run_multiple(commands) do |cmd|
114
+ puts "Original command: #{cmd.command}"
115
+ puts "Exit code: #{cmd.exit_code}"
116
+ puts "Output: #{cmd.output}"
117
+ end
118
+ ```
119
+
120
+ ### Using session logger
121
+
122
+ If you want to log command execution for the whole session you can assign a logger:
123
+
124
+ ```ruby
125
+ require 'logger'
126
+ require 'net/ssh/session'
127
+
128
+ s = Net::SSH::Session.new(host, user, password)
129
+ s.logger = Logger.new(STDOUT)
130
+ s.open
131
+
132
+ s.run("cd /tmp")
133
+ s.run("git clone git://github.com/sosedoff/net-ssh-session.git")
134
+ s.run("bundle install --path .bundle")
135
+
136
+ s.close
137
+ ```
138
+
139
+ Since the logger is set to write to `STDOUT` you'll see something like this:
140
+
141
+ ```
142
+ I, [2012-11-08T00:10:47.605916 #51878] INFO -- : [cd /tmp] => 0, 0 bytes
143
+ I, [2012-11-08T00:10:48.038294 #51878] INFO -- : [git clone git://github.com/sosedoff/net-ssh-session.git] => 0, 7795 bytes
144
+ I, [2012-11-08T00:10:48.229986 #51878] INFO -- : [bundle install --path .bundle] => 10, 35 bytes
145
+ ```
146
+
147
+ ### Execution history
148
+
149
+ By default each session command (`Net::SSH::SessionCommand`) will be recorded in
150
+ session history. Example how to skip history tracking:
151
+
152
+ ```ruby
153
+ require 'net/ssh/session'
154
+
155
+ s = Net::SSH::Session.new(host, user, password)
156
+ s.open
157
+
158
+ # Run commands with no history
159
+ s.run("export RAILS_ENV=test", :history => false)
160
+ r.run("mysqlcheck --repair mysql proc -u root", :history => false)
161
+
162
+ # Rest will be recorded
163
+ s.run("git clone git://github.com/sosedoff/net-ssh-session.git")
164
+ s.run("bundler install --path .")
165
+
166
+ s.close
167
+ ```
168
+
169
+ ## Credits
170
+
171
+ Library code was extracted and modified from multiple sources:
172
+
173
+ - Dan Sosedoff (@sosedoff)
174
+ - Mitchell Hashimoto (@mitchellh)
175
+ - Michael Klishin (@michaelklishin)
176
+ - Sven Fuchs (@svenfuchs)
177
+ - Travis-CI (@travis-ci)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ require 'bundler/gem_tasks'
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:test) do |t|
6
+ t.pattern = 'spec/*_spec.rb'
7
+ t.verbose = false
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,7 @@
1
+ module Net
2
+ module SSH
3
+ class Session
4
+ VERSION = '0.1.0'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,125 @@
1
+ require 'open3'
2
+ require 'net/ssh'
3
+ require 'net/ssh/shell'
4
+ require 'net/ssh/session_helpers'
5
+ require 'net/ssh/session_command'
6
+
7
+ module Net
8
+ module SSH
9
+ class Session
10
+ include Net::SSH::SessionHelpers
11
+
12
+ attr_reader :host, :user, :password
13
+ attr_reader :connection, :shell
14
+ attr_reader :options
15
+ attr_reader :logger
16
+ attr_reader :history
17
+
18
+ # Initialize a new ssh session
19
+ # @param host [String] remote hostname or ip address
20
+ # @param user [String] remote account username
21
+ # @param password [String] remote account password
22
+ def initialize(host, user, password='')
23
+ @host = host
24
+ @user = user
25
+ @password = password
26
+ @history = []
27
+ end
28
+
29
+ # Establish connection with remote server
30
+ # @param timeout [Integer] max timeout in seconds
31
+ # @return [Boolean]
32
+ def open(timeout=nil)
33
+ if timeout && timeout > 0
34
+ with_timeout(timeout) { establish_connection }
35
+ else
36
+ establish_connection
37
+ end
38
+ end
39
+
40
+ # Close connection with remote server
41
+ # @return [Boolean]
42
+ def close
43
+ shell.close!
44
+ end
45
+
46
+ def on_output(&block)
47
+ @on_output = block
48
+ end
49
+
50
+ # Execute command
51
+ # @param command [String] command to execute
52
+ # @param on_output [Block] output event block
53
+ # @return [Integer] command execution exit code
54
+ def exec(command, &on_output)
55
+ status = nil
56
+ shell.execute(command) do |process|
57
+ process.on_output(&on_output)
58
+ process.on_error_output(&on_output)
59
+ process.on_finish { |p| status = p.exit_status }
60
+ end
61
+ shell.session.loop(1) { status.nil? }
62
+ status
63
+ end
64
+
65
+ # Execute a single command
66
+ # @param command [String] comand to execute
67
+ # @param options [Hash] execution options
68
+ # @return [SessionCommand]
69
+ def run(command, options={})
70
+ output = ''
71
+ t_start = Time.now
72
+
73
+ exit_code = exec(command) do |process, data|
74
+ output << data
75
+ yield data if block_given?
76
+ end
77
+
78
+ t_end = Time.now
79
+
80
+ cmd = SessionCommand.new(
81
+ command, output, exit_code,
82
+ t_end - t_start
83
+ )
84
+
85
+ history << cmd unless options[:history] == false
86
+ logger.info(cmd.to_s) if logger
87
+
88
+ cmd
89
+ end
90
+
91
+ # Execute multiple commands
92
+ # @param commands [Array] set of commands to execute
93
+ # @param options [Hash] execution options
94
+ # @return [Array] set of command execution results
95
+ #
96
+ # Execution options are the following:
97
+ # options[:break] - If set to `true`, execution chain will break on first failed command
98
+ #
99
+ def run_multiple(commands=[], options={})
100
+ results = []
101
+
102
+ [commands].flatten.compact.each do |cmd|
103
+ result = run(cmd)
104
+ yield(result) if block_given?
105
+ results << result
106
+ break if results.last.failure? && options[:break] == true
107
+ end
108
+
109
+ results
110
+ end
111
+
112
+ # Set a global session logger for commands
113
+ def logger=(log)
114
+ @logger = log
115
+ end
116
+
117
+ private
118
+
119
+ def establish_connection
120
+ @connection = Net::SSH.start(host, user, :password => password)
121
+ @shell = @connection.shell
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,39 @@
1
+ module Net
2
+ module SSH
3
+ class SessionCommand
4
+ attr_reader :command, :output, :exit_code
5
+ attr_reader :duration
6
+ attr_accessor :start_time, :finish_time
7
+
8
+ # Initialize a new session command
9
+ # @param command [String] original command
10
+ # @param output [String] command execution output
11
+ # @param exit_code [Integer] command execution exit code
12
+ # @param duration [Float] execution time in seconds
13
+ def initialize(command, output, exit_code, duration=0)
14
+ @command = command
15
+ @output = output || ''
16
+ @exit_code = Integer(exit_code) rescue 1
17
+ @duration = Float(duration)
18
+ end
19
+
20
+ # Check if exit code is successful
21
+ # @return [Boolean]
22
+ def success?
23
+ exit_code == 0
24
+ end
25
+
26
+ # Check if exit code is not successful
27
+ # @return [Boolean]
28
+ def failure?
29
+ exit_code != 0
30
+ end
31
+
32
+ # Get command string representation
33
+ # @return [String]
34
+ def to_s
35
+ "[#{command}] => #{exit_code}, #{output.to_s.bytesize} bytes, #{duration} seconds"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,104 @@
1
+ require 'timeout'
2
+
3
+ module Net
4
+ module SSH
5
+ module SessionHelpers
6
+ # Swith current directory
7
+ # @param path [String] directory path
8
+ # @return [Boolean] execution result
9
+ def chdir(path)
10
+ run("cd #{path}").success?
11
+ end
12
+
13
+ # Get current directory
14
+ # @return [String]
15
+ def pwd
16
+ capture("pwd")
17
+ end
18
+
19
+ # Execute command and capture any output
20
+ # @param command [String] command to execute
21
+ # @return [String] execution result
22
+ def capture(command)
23
+ run(command).output.strip
24
+ end
25
+
26
+ # Read remote file contents
27
+ # @path [String] file path
28
+ # @return [String] file contents
29
+ def read_file(path)
30
+ result = run("cat #{path}")
31
+ result.success? ? result.output : ''
32
+ end
33
+
34
+ # Check if remote directory exists
35
+ # @param path [String] directory path
36
+ # @return [Boolean] execution result
37
+ def directory_exists?(path)
38
+ run("test -d #{path}").success?
39
+ end
40
+
41
+ # Check if remote file exists
42
+ # @param path [String] file path
43
+ # @return [Boolean] execution result
44
+ def file_exists?(path)
45
+ run("test -f #{path}").success?
46
+ end
47
+
48
+ # Check if process with PID is running
49
+ # @param pid [String] process id
50
+ # @return [Boolean] execution result
51
+ def process_exists?(pid)
52
+ run("ps -p #{pid}").success?
53
+ end
54
+
55
+ # Kill a process with the signal
56
+ # @param pid [String] process id
57
+ # @param signal [String] signal to send
58
+ # @return [Boolean] exection result
59
+ def kill_process(pid, signal='SIGTERM')
60
+ run("kill -#{signal} #{pid}")
61
+ process_exists?(pid)
62
+ end
63
+
64
+ # Export an environment variable
65
+ # @param key [String] variable name
66
+ # @param value [String] variable value
67
+ # @return [Boolean] execution result
68
+ def export(key, value)
69
+ run("export #{key}=#{value}").success?
70
+ end
71
+
72
+ # Export environment vars from hash
73
+ # @param data [Hash]
74
+ # @return [Boolean] execution result
75
+ def export_hash(data={})
76
+ data.each_pair do |k, v|
77
+ export(k, v)
78
+ end
79
+ end
80
+
81
+ # Get an environment variable
82
+ # @param key [String] variable name
83
+ # @return [String] variable value
84
+ def env(key)
85
+ capture("echo $#{key}")
86
+ end
87
+
88
+ # Get last executed command exit code
89
+ # @return [Integer] exit code
90
+ def last_exit_code
91
+ Integer(capture("echo $?"))
92
+ end
93
+
94
+ # Set a timeout context for execution
95
+ # @param time [Integer] max time for execution in seconds
96
+ # @param block [Block] block to execute
97
+ def with_timeout(time, &block)
98
+ Timeout.timeout(time) do
99
+ block.call(self)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,190 @@
1
+ require 'digest/sha1'
2
+ require 'net/ssh'
3
+ require 'net/ssh/shell/process'
4
+ require 'net/ssh/shell/subshell'
5
+
6
+ module Net
7
+ module SSH
8
+ class Shell
9
+ attr_reader :session
10
+ attr_reader :channel
11
+ attr_reader :state
12
+ attr_reader :shell
13
+ attr_reader :processes
14
+ attr_accessor :default_process_class
15
+
16
+ def initialize(session, shell=:default)
17
+ @session = session
18
+ @shell = shell
19
+ @state = :closed
20
+ @processes = []
21
+ @when_open = []
22
+ @on_process_run = nil
23
+ @default_process_class = Net::SSH::Shell::Process
24
+ open
25
+ end
26
+
27
+ def open(&callback)
28
+ if closed?
29
+ @state = :opening
30
+ @channel = session.open_channel(&method(:open_succeeded))
31
+ @channel.on_open_failed(&method(:open_failed))
32
+ @channel.on_request('exit-status', &method(:on_exit_status))
33
+ end
34
+ when_open(&callback) if callback
35
+ self
36
+ end
37
+
38
+ def open!
39
+ if !open?
40
+ open if closed?
41
+ session.loop { opening? }
42
+ end
43
+ self
44
+ end
45
+
46
+ def when_open(&callback)
47
+ if open?
48
+ yield self
49
+ else
50
+ @when_open << callback
51
+ end
52
+ self
53
+ end
54
+
55
+ def open?
56
+ state == :open
57
+ end
58
+
59
+ def closed?
60
+ state == :closed
61
+ end
62
+
63
+ def opening?
64
+ !open? && !closed?
65
+ end
66
+
67
+ def on_process_run(&callback)
68
+ @on_process_run = callback
69
+ end
70
+
71
+ def execute(command, *args, &callback)
72
+ # The class is an optional second argument.
73
+ klass = default_process_class
74
+ klass = args.shift if args.first.is_a?(Class)
75
+
76
+ # The properties are expected to be the next argument.
77
+ props = {}
78
+ props = args.shift if args.first.is_a?(Hash)
79
+
80
+ process = klass.new(self, command, props, callback)
81
+ processes << process
82
+ run_next_process if processes.length == 1
83
+ process
84
+ end
85
+
86
+ def subshell(command, &callback)
87
+ execute(command, Net::SSH::Shell::Subshell, &callback)
88
+ end
89
+
90
+ def execute!(command, &callback)
91
+ process = execute(command, &callback)
92
+ wait!
93
+ process
94
+ end
95
+
96
+ def busy?
97
+ opening? || processes.any?
98
+ end
99
+
100
+ def wait!
101
+ session.loop { busy? }
102
+ end
103
+
104
+ def close!
105
+ channel.close if channel
106
+ end
107
+
108
+ def child_finished(child)
109
+ channel.on_close(&method(:on_channel_close)) if !channel.nil?
110
+ processes.delete(child)
111
+ run_next_process
112
+ end
113
+
114
+ def separator
115
+ @separator ||= begin
116
+ s = Digest::SHA1.hexdigest([session.object_id, object_id, Time.now.to_i, Time.now.usec, rand(0xFFFFFFFF)].join(":"))
117
+ s << Digest::SHA1.hexdigest(s)
118
+ end
119
+ end
120
+
121
+ def on_channel_close(channel)
122
+ @state = :closed
123
+ @channel = nil
124
+ end
125
+
126
+ private
127
+
128
+ def run_next_process
129
+ if processes.any?
130
+ process = processes.first
131
+ @on_process_run.call(self, process) if @on_process_run
132
+ process.run
133
+ end
134
+ end
135
+
136
+ def open_succeeded(channel)
137
+ @state = :pty
138
+ channel.on_close(&method(:on_channel_close))
139
+ channel.request_pty(:modes => { Net::SSH::Connection::Term::ECHO => 0 }, &method(:pty_requested))
140
+ end
141
+
142
+ def open_failed(channel, code, description)
143
+ @state = :closed
144
+ raise "could not open channel for process manager (#{description}, ##{code})"
145
+ end
146
+
147
+ def on_exit_status(channel, data)
148
+ unless data.read_long == 0
149
+ raise "the shell exited unexpectedly"
150
+ end
151
+ end
152
+
153
+ def pty_requested(channel, success)
154
+ @state = :shell
155
+ raise "could not request pty for process manager" unless success
156
+ if shell == :default
157
+ channel.send_channel_request("shell", &method(:shell_requested))
158
+ else
159
+ channel.exec(shell, &method(:shell_requested))
160
+ end
161
+ end
162
+
163
+ def shell_requested(channel, success)
164
+ @state = :initializing
165
+ raise "could not request shell for process manager" unless success
166
+ channel.on_data(&method(:look_for_initialization_done))
167
+ channel.send_data "export PS1=; echo #{separator} $?\n"
168
+ end
169
+
170
+ def look_for_initialization_done(channel, data)
171
+ if data.include?(separator)
172
+ @state = :open
173
+ @when_open.each { |callback| callback.call(self) }
174
+ @when_open.clear
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ class Net::SSH::Connection::Session
182
+ # Provides a convenient way to initialize a shell given a Net::SSH
183
+ # session. Yields the new shell if a block is given. Returns the shell
184
+ # instance.
185
+ def shell(*args)
186
+ shell = Net::SSH::Shell.new(self, *args)
187
+ yield shell if block_given?
188
+ shell
189
+ end
190
+ end
@@ -0,0 +1,120 @@
1
+ module Net; module SSH; class Shell
2
+ class Process
3
+ attr_reader :state
4
+ attr_reader :command
5
+ attr_reader :manager
6
+ attr_reader :callback
7
+ attr_reader :exit_status
8
+ attr_reader :properties
9
+
10
+ def initialize(manager, command, properties, callback)
11
+ @command = command
12
+ @manager = manager
13
+ @callback = callback
14
+ @properties = properties
15
+ @on_output = nil
16
+ @on_error_output = nil
17
+ @on_finish = nil
18
+ @state = :new
19
+ end
20
+
21
+ def [](key)
22
+ @properties[key]
23
+ end
24
+
25
+ def []=(key, value)
26
+ @properties[key] = value
27
+ end
28
+
29
+ def send_data(data)
30
+ manager.channel.send_data(data)
31
+ end
32
+
33
+ def run
34
+ if state == :new
35
+ state = :starting
36
+ manager.open do
37
+ state = :running
38
+ manager.channel.on_data(&method(:on_stdout))
39
+ manager.channel.on_extended_data(&method(:on_stderr))
40
+ manager.channel.on_close(&method(:on_close))
41
+
42
+ callback.call(self) if callback
43
+
44
+ cmd = command.dup
45
+ cmd << ";" if cmd !~ /[;&]$/
46
+ cmd << " DONTEVERUSETHIS=$?; echo #{manager.separator} $DONTEVERUSETHIS; echo \"exit $DONTEVERUSETHIS\"|sh"
47
+
48
+ send_data(cmd + "\n")
49
+ end
50
+ end
51
+
52
+ self
53
+ end
54
+
55
+ def starting?
56
+ state == :starting
57
+ end
58
+
59
+ def running?
60
+ state == :running
61
+ end
62
+
63
+ def finished?
64
+ state == :finished
65
+ end
66
+
67
+ def busy?
68
+ starting? || running?
69
+ end
70
+
71
+ def wait!
72
+ manager.session.loop { busy? }
73
+ self
74
+ end
75
+
76
+ def on_output(&callback)
77
+ @on_output = callback
78
+ end
79
+
80
+ def on_error_output(&callback)
81
+ @on_error_output = callback
82
+ end
83
+
84
+ def on_finish(&callback)
85
+ @on_finish = callback
86
+ end
87
+
88
+ protected
89
+
90
+ def output!(data)
91
+ @on_output.call(self, data) if @on_output
92
+ end
93
+
94
+ def on_stdout(ch, data)
95
+ if data.strip =~ /#{manager.separator} (\d+)$/
96
+ before = $`
97
+ output!(before) unless before.empty?
98
+ finished!($1)
99
+ else
100
+ output!(data)
101
+ end
102
+ end
103
+
104
+ def on_stderr(ch, type, data)
105
+ @on_error_output.call(self, data) if @on_error_output
106
+ end
107
+
108
+ def on_close(ch)
109
+ manager.on_channel_close(ch)
110
+ finished!(-1)
111
+ end
112
+
113
+ def finished!(status)
114
+ @state = :finished
115
+ @exit_status = status.to_i
116
+ @on_finish.call(self) if @on_finish
117
+ manager.child_finished(self)
118
+ end
119
+ end
120
+ end; end; end
@@ -0,0 +1,21 @@
1
+ require 'net/ssh/shell/process'
2
+
3
+ module Net; module SSH; class Shell
4
+ class Subshell < Process
5
+ protected
6
+
7
+ def on_stdout(ch, data)
8
+ if !output!(data)
9
+ ch.on_data(&method(:look_for_finalize_initializer))
10
+ ch.send_data("export PS1=; echo #{manager.separator} $?\n")
11
+ end
12
+ end
13
+
14
+ def look_for_finalize_initializer(ch, data)
15
+ if data =~ /#{manager.separator} (\d+)/
16
+ ch.on_close(&@master_onclose)
17
+ finished!($1)
18
+ end
19
+ end
20
+ end
21
+ end; end; end
@@ -0,0 +1,7 @@
1
+ module Net
2
+ module SSH
3
+ class Shell
4
+ VERSION = "0.3.0.dev"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path('../lib/net-ssh-session/version', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "net-ssh-session"
5
+ s.version = Net::SSH::Session::VERSION
6
+ s.summary = "Shell session for Net::SSH connections"
7
+ s.description = "Shell interface with helper methods to work with Net::SSH connections"
8
+ s.homepage = "https://github.com/sosedoff/net-ssh-session"
9
+ s.authors = ["Dan Sosedoff"]
10
+ s.email = ["dan.sosedoff@gmail.com"]
11
+
12
+ s.add_development_dependency 'rake'
13
+ s.add_development_dependency 'rspec', '~> 2.11'
14
+ s.add_development_dependency 'simplecov', '~> 0.4'
15
+
16
+ s.add_dependency 'net-ssh', '~> 2.6'
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
21
+ s.require_paths = ["lib"]
22
+ end
File without changes
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe Net::SSH::SessionCommand do
4
+ describe '#initialize' do
5
+ it 'assigns attributes' do
6
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '128', 1.5)
7
+
8
+ cmd.command.should be_a String
9
+ cmd.output.should be_a String
10
+ cmd.exit_code.should be_a Fixnum
11
+ cmd.duration.should be_a Float
12
+ end
13
+
14
+ it 'sets exit code to 1 on invalid value' do
15
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', 'ohai')
16
+ cmd.exit_code.should eq(1)
17
+ end
18
+ end
19
+
20
+ describe '#success?' do
21
+ it 'returns true for successful exit codes' do
22
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '0')
23
+ cmd.success?.should be_true
24
+ end
25
+
26
+ it 'returns false for failed exit codes' do
27
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '1')
28
+ cmd.success?.should be_false
29
+
30
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '128')
31
+ cmd.success?.should be_false
32
+ end
33
+ end
34
+
35
+ describe '#failure?' do
36
+ it 'returns true for non-zero exit codes' do
37
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '1')
38
+ cmd.failure?.should be_true
39
+
40
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '128')
41
+ cmd.failure?.should be_true
42
+ end
43
+
44
+ it 'returns false for successful exit codes' do
45
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '0')
46
+ cmd.failure?.should be_false
47
+ end
48
+ end
49
+
50
+ describe '#to_s' do
51
+ it 'returns command string representation' do
52
+ cmd = Net::SSH::SessionCommand.new('cmd', 'output', '0', 1.234)
53
+ cmd.to_s.should eq("[cmd] => 0, 6 bytes, 1.234 seconds")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+
3
+ describe Net::SSH::SessionHelpers do
4
+ let(:session) do
5
+ class Session
6
+ include Net::SSH::SessionHelpers
7
+ end
8
+ Session.new
9
+ end
10
+
11
+ describe '#chdir' do
12
+ before do
13
+ session.stub(:run).with("cd /tmp").
14
+ and_return(fake_run("cd /tmp", "", 0))
15
+
16
+ session.stub(:run).with("cd /foo/bar").
17
+ and_return(fake_run("cd /foo/bar", "-bash: cd: /foo/bar: No such file or directory\r\n", 1))
18
+ end
19
+
20
+ it 'returns true if directory was changed' do
21
+ session.chdir("/tmp").should eq(true)
22
+ end
23
+
24
+ it 'returns false if unable to change directory' do
25
+ session.chdir("/foo/bar").should eq(false)
26
+ end
27
+ end
28
+
29
+ describe '#pwd' do
30
+ before do
31
+ session.stub(:run).with("pwd").and_return(fake_run("pwd", "/tmp\r\n", 0))
32
+ end
33
+
34
+ it 'returns current work directory' do
35
+ session.pwd.should eq("/tmp")
36
+ end
37
+ end
38
+
39
+ describe '#capture' do
40
+ before do
41
+ session.stub(:run).with("uname").and_return(fake_run("uname", "Linux\r\n"))
42
+ session.stub(:run).with("ls").and_return(fake_run("ls", "\r\n", 1))
43
+ session.stub(:run).with("date").and_return(fake_run("date", nil, 1))
44
+ end
45
+
46
+ it 'returns command output' do
47
+ session.capture("uname").should eq("Linux")
48
+ session.capture("ls").should eq("")
49
+ session.capture("date").should eq("")
50
+ end
51
+ end
52
+
53
+ describe '#read_file' do
54
+ before do
55
+ session.stub(:run).with('cat /tmp/file').
56
+ and_return(fake_run('cat /tmp/file', "Hello\n\n", 0))
57
+
58
+ session.stub(:run).with('cat /foo').
59
+ and_return(fake_run('cat /foo', nil, 1))
60
+ end
61
+
62
+ it 'returns file contents as is' do
63
+ session.read_file("/tmp/file").should eq("Hello\n\n")
64
+ end
65
+
66
+ it 'returns empty string if files does not exist' do
67
+ session.read_file("/foo").should eq("")
68
+ end
69
+ end
70
+
71
+ describe '#directory_exists?' do
72
+ before do
73
+ session.stub(:run).with('test -d /tmp').and_return(fake_run('test -d /tmp', nil, 0))
74
+ session.stub(:run).with('test -d /foo').and_return(fake_run('test -d /foo', nil, 1))
75
+ end
76
+
77
+ it 'returns true if directory exists' do
78
+ session.directory_exists?('/tmp').should be_true
79
+ end
80
+
81
+ it 'returns false if directory does not exist' do
82
+ session.directory_exists?('/foo').should be_false
83
+ end
84
+ end
85
+
86
+ describe '#file_exists?' do
87
+ before do
88
+ session.stub(:run).with('test -f /f1').and_return(fake_run('test -f /f1', nil, 0))
89
+ session.stub(:run).with('test -f /f2').and_return(fake_run('test -f /f2', nil, 1))
90
+ end
91
+
92
+ it 'returns true if file exists' do
93
+ session.file_exists?('/f1').should be_true
94
+ end
95
+
96
+ it 'returns false if file does not exist' do
97
+ session.file_exists?('/f2').should be_false
98
+ end
99
+ end
100
+
101
+ describe '#process_exists?' do
102
+ before do
103
+ session.stub(:run).with('ps -p 123').and_return(fake_run('ps -p 123', nil, 0))
104
+ session.stub(:run).with('ps -p 345').and_return(fake_run('ps -p 345', nil, 1))
105
+ end
106
+
107
+ it 'returns true if process exists' do
108
+ session.process_exists?(123).should be_true
109
+ end
110
+
111
+ it 'returns true if process does not exist' do
112
+ session.process_exists?(345).should be_false
113
+ end
114
+ end
115
+
116
+ describe '#kill_process' do
117
+ before do
118
+ session.stub(:run).with('kill -SIGTERM 123').and_return(fake_run('kill -SIGTERM 123', nil, 0))
119
+ session.stub(:run).with('ps -p 123').and_return(fake_run('ps -p 123', nil, 0))
120
+
121
+ session.stub(:run).with('kill -SIGTERM 345').and_return(fake_run('kill -SIGTERM 345', nil, 0))
122
+ session.stub(:run).with('ps -p 345').and_return(fake_run('ps -p 345', nil, 1))
123
+ end
124
+
125
+ it 'returns true if process was killed and does not exist' do
126
+ session.kill_process(123).should be_true
127
+ end
128
+
129
+ it 'returns false if process is still alive' do
130
+ session.kill_process(345).should be_false
131
+ end
132
+ end
133
+
134
+ describe '#with_timeout' do
135
+ let(:worker) do
136
+ proc { sleep 1 }
137
+ end
138
+
139
+ it 'raises timeout error if exceded' do
140
+ expect { session.with_timeout(0.5, &worker) }.to raise_error Timeout::Error
141
+ end
142
+
143
+ it 'runs normally' do
144
+ expect { session.with_timeout(2, &worker) }.not_to raise_error Timeout::Error
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Net::SSH::Session do
4
+ # TODO
5
+ end
@@ -0,0 +1,16 @@
1
+ $:.unshift File.expand_path("../..", __FILE__)
2
+
3
+ require 'net/ssh/session'
4
+
5
+ def fixture_path(filename=nil)
6
+ path = File.expand_path("../fixtures", __FILE__)
7
+ filename.nil? ? path : File.join(path, filename)
8
+ end
9
+
10
+ def fixture(file)
11
+ File.read(File.join(fixture_path, file))
12
+ end
13
+
14
+ def fake_run(command, output, exit_code=0, duration=0)
15
+ Net::SSH::SessionCommand.new(command, output, exit_code, duration)
16
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-ssh-session
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Dan Sosedoff
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-12-06 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :development
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: "2.11"
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: simplecov
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: "0.4"
46
+ type: :development
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: net-ssh
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: "2.6"
57
+ type: :runtime
58
+ version_requirements: *id004
59
+ description: Shell interface with helper methods to work with Net::SSH connections
60
+ email:
61
+ - dan.sosedoff@gmail.com
62
+ executables: []
63
+
64
+ extensions: []
65
+
66
+ extra_rdoc_files: []
67
+
68
+ files:
69
+ - .gitignore
70
+ - .rspec
71
+ - .travis.yml
72
+ - Gemfile
73
+ - LICENSE
74
+ - README.md
75
+ - Rakefile
76
+ - lib/net-ssh-session/version.rb
77
+ - lib/net/ssh/session.rb
78
+ - lib/net/ssh/session_command.rb
79
+ - lib/net/ssh/session_helpers.rb
80
+ - lib/net/ssh/shell.rb
81
+ - lib/net/ssh/shell/process.rb
82
+ - lib/net/ssh/shell/subshell.rb
83
+ - lib/net/ssh/shell/version.rb
84
+ - net-ssh-session.gemspec
85
+ - spec/fixtures/.gitkeep
86
+ - spec/session_command_spec.rb
87
+ - spec/session_helpers_spec.rb
88
+ - spec/session_spec.rb
89
+ - spec/spec_helper.rb
90
+ homepage: https://github.com/sosedoff/net-ssh-session
91
+ licenses: []
92
+
93
+ post_install_message:
94
+ rdoc_options: []
95
+
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: "0"
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: "0"
110
+ requirements: []
111
+
112
+ rubyforge_project:
113
+ rubygems_version: 1.8.24
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Shell session for Net::SSH connections
117
+ test_files:
118
+ - spec/fixtures/.gitkeep
119
+ - spec/session_command_spec.rb
120
+ - spec/session_helpers_spec.rb
121
+ - spec/session_spec.rb
122
+ - spec/spec_helper.rb
123
+ has_rdoc: