Skip to content

Commit

Permalink
Merge pull request #260 from robd/simplify-interaction-handler-api
Browse files Browse the repository at this point in the history
Combine on_stdout and on_stderr into on_data on interaction handler api
  • Loading branch information
leehambley committed Jul 17, 2015
2 parents c840d4f + ab5f9e4 commit 32c04ed
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 36 deletions.
15 changes: 7 additions & 8 deletions README.md
Expand Up @@ -272,18 +272,18 @@ and this can be achieved by specifying an `:interaction_handler` option when you
**It is not necessary, or desirable to enable `Netssh.config.pty` to use the `interaction_handler` option.
Only enable `Netssh.config.pty` if the command you are calling won't work without a pty.**

An `interaction_handler` is an object which responds to `on_stdout(stdout, channel, command)` or `on_stderr(stderr, channel, command)`.
The `interaction_handler`'s methods will be called once per line of `stdout` or `stderr` from the server and
An `interaction_handler` is an object which responds to `on_data(command, stream_name, data, channel)`.
The `interaction_handler`'s method will be called once per line of `stdout` or `stderr` from the server and
can send data back to the server using the `channel` parameter.
This allows scripting of command interaction by responding to `stdout` or `stderr` lines with any input required.

For example, an interaction handler to change the password of your linux user using the `passwd` command could look like this:

```ruby
class PasswdInteractionHandler
def on_stderr(channel, stderr, command)
puts stderr
case stderr
def on_data(command, stream_name, data, channel)
puts data
case data
when '(current) UNIX password: '
channel.send_data("old_pw\n")
when 'Enter new UNIX password: ', 'Retype new UNIX password: '
Expand Down Expand Up @@ -373,7 +373,7 @@ execute(:second_command, interaction_handler: ENTER_PASSWORD)
class PromptUserForPasswordAndCache
@password_cache = {}

def on_stderr(channel, stderr, command)
def on_data(command, stream_name, data, channel)
if data =~ /Sorry.*\stry\sagain/
@password_cache[command.host] = nil
end
Expand All @@ -397,8 +397,7 @@ execute(:second_command, interaction_handler: prompt_or_use_cached)

```

When using the `Netssh` backend, the `channel` parameter of `on_stdout(channel, stdout, command)` or
`on_stderr(channel, stderr, command)` is a
When using the `Netssh` backend, the `channel` parameter of `on_data(command, stream_name, data, channel)` is a
[Net::SSH Channel](http://net-ssh.github.io/ssh/v2/api/classes/Net/SSH/Connection/Channel.html).
When using the `Local` backend, it is a [ruby IO](http://ruby-doc.org/core/IO.html) object.
If you need to support both sorts of backends with the same interaction handler,
Expand Down
8 changes: 4 additions & 4 deletions lib/sshkit/command.rb
Expand Up @@ -79,13 +79,13 @@ def stderr=(new_value)
def on_stdout(channel, data)
@stdout = data
@full_stdout += data
call_interaction_handler(channel, data, :on_stdout)
call_interaction_handler(:stdout, data, channel)
end

def on_stderr(channel, data)
@stderr = data
@full_stderr += data
call_interaction_handler(channel, data, :on_stderr)
call_interaction_handler(:stderr, data, channel)
end

def exit_status=(new_exit_status)
Expand Down Expand Up @@ -230,10 +230,10 @@ def sanitize_command!
end
end

def call_interaction_handler(channel, data, callback_name)
def call_interaction_handler(stream_name, data, channel)
interaction_handler = options[:interaction_handler]
interaction_handler = MappingInteractionHandler.new(interaction_handler) if interaction_handler.kind_of?(Hash)
interaction_handler.send(callback_name, channel, data, self) if interaction_handler.respond_to?(callback_name)
interaction_handler.on_data(self, stream_name, data, channel) if interaction_handler.respond_to?(:on_data)
end

def log_reader_deprecation(stream)
Expand Down
14 changes: 3 additions & 11 deletions lib/sshkit/mapping_interaction_handler.rb
Expand Up @@ -17,17 +17,7 @@ def initialize(mapping, log_level=nil)
end
end

def on_stdout(channel, data, command)
on_data(channel, data, 'stdout')
end

def on_stderr(channel, data, command)
on_data(channel, data, 'stderr')
end

private

def on_data(channel, data, stream_name)
def on_data(command, stream_name, data, channel)
log("Looking up response for #{stream_name} message #{data.inspect}")

response_data = @mapping_proc.call(data)
Expand All @@ -46,6 +36,8 @@ def on_data(channel, data, stream_name)
end
end

private

def log(message)
SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
end
Expand Down
26 changes: 13 additions & 13 deletions test/unit/test_mapping_interaction_handler.rb
Expand Up @@ -15,37 +15,37 @@ def setup

def test_calls_send_data_with_mapped_input_when_stdout_matches
handler = MappingInteractionHandler.new('Server output' => "some input\n")

channel.expects(:send_data).with("some input\n")

handler.on_stdout(channel, 'Server output', nil)
handler.on_data(nil, :stdout, 'Server output', channel)
end

def test_calls_send_data_with_mapped_input_when_stderr_matches
handler = MappingInteractionHandler.new('Server output' => "some input\n")
channel.expects(:send_data).with("some input\n")

MappingInteractionHandler.new('Server output' => "some input\n").on_stderr(channel, 'Server output', nil)
handler.on_data(nil, :stderr, 'Server output', channel)
end

def test_logs_unmatched_interaction_if_constructed_with_a_log_level
@output.expects(:debug).with('Looking up response for stdout message "Server output\n"')
@output.expects(:debug).with('Unable to find interaction handler mapping for stdout: "Server output\n" so no response was sent')

MappingInteractionHandler.new({}, :debug).on_stdout(channel, "Server output\n", nil)
MappingInteractionHandler.new({}, :debug).on_data(nil, :stdout, "Server output\n", channel)
end

def test_logs_matched_interaction_if_constructed_with_a_log_level
handler = MappingInteractionHandler.new({"Server output\n" => "Some input\n"}, :debug)

channel.stubs(:send_data)
@output.expects(:debug).with('Looking up response for stdout message "Server output\n"')
@output.expects(:debug).with('Sending "Some input\n"')

MappingInteractionHandler.new({"Server output\n" => "Some input\n"}, :debug).on_stdout(channel, "Server output\n", nil)
handler.on_data(nil, :stdout, "Server output\n", channel)
end

def test_supports_regex_keys
handler = MappingInteractionHandler.new({/Some \w+ output\n/ => "Input\n"})
channel.expects(:send_data).with("Input\n")

MappingInteractionHandler.new({ /Some \w+ output\n/ => "Input\n"}).on_stdout(channel, "Some lovely output\n", nil)
handler.on_data(nil, :stdout, "Some lovely output\n", channel)
end

def test_supports_lambda_mapping
Expand All @@ -58,7 +58,7 @@ def test_supports_lambda_mapping
end
end

MappingInteractionHandler.new(mapping).on_stdout(channel, "Some great output\n", nil)
MappingInteractionHandler.new(mapping).on_data(nil, :stdout, "Some great output\n", channel)
end


Expand All @@ -69,7 +69,7 @@ def test_matches_keys_in_ofer
})

channel.expects(:send_data).with("Specific Input\n")
interaction_handler.on_stdout(channel, "Specific output\n", nil)
interaction_handler.on_data(nil, :stdout, "Specific output\n", channel)
end

def test_supports_default_mapping
Expand All @@ -79,7 +79,7 @@ def test_supports_default_mapping
})

channel.expects(:send_data).with("Specific Input\n")
interaction_handler.on_stdout(channel, "Specific output\n", nil)
interaction_handler.on_data(nil, :stdout, "Specific output\n", channel)
end

def test_raises_for_unsupported_mapping_type
Expand All @@ -92,7 +92,7 @@ def test_raises_for_unsupported_mapping_type
def test_raises_for_unsupported_channel_type
handler = MappingInteractionHandler.new({"Some output\n" => "Whatever"})
raised_error = assert_raises RuntimeError do
handler.on_stdout(Object.new, "Some output\n", nil)
handler.on_data(nil, :stdout, "Some output\n", Object.new)
end
assert_match(/Unable to write response data to channel #<Object:.*> - does not support 'send_data' or 'write'/, raised_error.message)
end
Expand Down

0 comments on commit 32c04ed

Please sign in to comment.