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 +24 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +2 -0
- data/LICENSE +7 -0
- data/README.md +177 -0
- data/Rakefile +10 -0
- data/lib/net-ssh-session/version.rb +7 -0
- data/lib/net/ssh/session.rb +125 -0
- data/lib/net/ssh/session_command.rb +39 -0
- data/lib/net/ssh/session_helpers.rb +104 -0
- data/lib/net/ssh/shell.rb +190 -0
- data/lib/net/ssh/shell/process.rb +120 -0
- data/lib/net/ssh/shell/subshell.rb +21 -0
- data/lib/net/ssh/shell/version.rb +7 -0
- data/net-ssh-session.gemspec +22 -0
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/session_command_spec.rb +56 -0
- data/spec/session_helpers_spec.rb +147 -0
- data/spec/session_spec.rb +5 -0
- data/spec/spec_helper.rb +16 -0
- metadata +123 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|