stdin_responder 0.0.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.
- checksums.yaml +7 -0
- data/lib/stdin_responder.rb +214 -0
- metadata +44 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: ad504308d5627cec90e9bdb15d4a0d4fbfcf120f
|
|
4
|
+
data.tar.gz: 66623fcafcd68c33027433d13cdbb6683006f6e2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f92c5d050248fe71d36bc5ec2703a0931b3f994888bf1fc2f57f6947a754d89c5467cddce1bf4b13b1dcd0fadff7b6cc5d480e443f86508a5963095eea6eadd2
|
|
7
|
+
data.tar.gz: 1d3670268abf62c6a64dc121413b27eb6761581dd1f1d305297d5b169ea37074f1e5f24650bdcba2ce54328cdf553c38d5d5448d2b14e776bcd53d8a0d2d9d0b
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# StdinResponder
|
|
2
|
+
# Monitor stdout and/or stderr, and feed responses to stdin based on rules
|
|
3
|
+
#
|
|
4
|
+
# EXAMPLE
|
|
5
|
+
# require "./stdin_responder.rb"
|
|
6
|
+
# r = StdinResponder.new
|
|
7
|
+
# r.add_rule /sudo/ => "Okay.", default: "What? Make it yourself.", repeat: Float::INFINITY
|
|
8
|
+
# r.run("./sandwich")
|
|
9
|
+
#
|
|
10
|
+
# RULES:
|
|
11
|
+
# Each rule is a Hash. Keys added to the Rule determine how the Rule
|
|
12
|
+
# behaves given a particular stdout buffer. Keys can be:
|
|
13
|
+
# Regexp: if stdout buffer matches the regexp, the value is used
|
|
14
|
+
# Proc / Lambda: till be called with the stdout buffer as an argument.
|
|
15
|
+
# if the Proc return a truthy value, the value is used
|
|
16
|
+
# String: If the stdout buffer's last non-empty line matches the
|
|
17
|
+
# String exactly, the value is used
|
|
18
|
+
#
|
|
19
|
+
# The first key that results in a match will be the only key used.
|
|
20
|
+
#
|
|
21
|
+
# Each Rule also has two command symbols as keys, :default and :repeat
|
|
22
|
+
# :default gives the value to be used if no other keys match
|
|
23
|
+
# :repeat is the number of times to re-use this rule before discarding it
|
|
24
|
+
#
|
|
25
|
+
# A Rule value gets used depending on its type.
|
|
26
|
+
# String: puts the value to stdin
|
|
27
|
+
# Proc: call with the current stdout buffer as an arg. Results is
|
|
28
|
+
# puts'd to stdin
|
|
29
|
+
# Three command symbols may be used as values:
|
|
30
|
+
# :wait - put the rule back on the stack and wait
|
|
31
|
+
# :skip - discard the rule and immediately proceed to the next one
|
|
32
|
+
# :abort - terminate execution
|
|
33
|
+
# Any other value will be converted to a String and puts'd to stdin
|
|
34
|
+
#
|
|
35
|
+
# Other rule examples:
|
|
36
|
+
# r.add_rule /connecting/ => :wait, /access.*denied/i => :abort, /access.*granted/i => "echo 'hello, world'"
|
|
37
|
+
# r.add_rule /do you want to save/i => 'y', default: :skip
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
require 'open4'
|
|
41
|
+
|
|
42
|
+
class StdinResponder
|
|
43
|
+
|
|
44
|
+
attr_reader :rules
|
|
45
|
+
|
|
46
|
+
def initialize(merge_stderr: false, prompt_delay_threshold: 1.0, timeout: 120, verbose: true, debug: false)
|
|
47
|
+
@rules = []
|
|
48
|
+
@stdout_buffer = ""
|
|
49
|
+
@stderr_buffer = merge_stderr ? @stdout_buffer : ""
|
|
50
|
+
@stdin_buffer = ""
|
|
51
|
+
@prompt_delay_threshold = prompt_delay_threshold
|
|
52
|
+
@timeout = timeout
|
|
53
|
+
@verbose = verbose
|
|
54
|
+
@debug = debug
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_rule(rule)
|
|
58
|
+
@rules << {default: "", repeat: 0}.merge(rule)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run(command)
|
|
62
|
+
@session_rules = @rules.dup
|
|
63
|
+
pid, stdin, stdout, stderr = Open4.popen4(command)
|
|
64
|
+
|
|
65
|
+
out_buffer = ""
|
|
66
|
+
prompt_threshold = 1
|
|
67
|
+
|
|
68
|
+
@stdout_thread = Thread.start do
|
|
69
|
+
monitor_stdout(stdout)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@stderr_thread = Thread.start do
|
|
73
|
+
monitor_stderr(stderr)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@stdin_thread = Thread.start do
|
|
77
|
+
generate_responses(stdin)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
master_thread = Thread.start do
|
|
81
|
+
monitor_threads
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@stdout_thread.abort_on_exception = true
|
|
85
|
+
@stderr_thread.abort_on_exception = true
|
|
86
|
+
@stdin_thread.abort_on_exception = true
|
|
87
|
+
master_thread.abort_on_exception = true
|
|
88
|
+
|
|
89
|
+
@stdout_thread.join
|
|
90
|
+
@stderr_thread.join
|
|
91
|
+
stdout.close
|
|
92
|
+
stderr.close
|
|
93
|
+
|
|
94
|
+
@stdin_thread.join
|
|
95
|
+
stdin.close
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def monitor_stdout(outstream)
|
|
101
|
+
outstream.each_char do |c|
|
|
102
|
+
@stdout_buffer << c
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def monitor_stderr(outstream)
|
|
107
|
+
outstream.each_char do |c|
|
|
108
|
+
@stderr_buffer << c
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def monitor_threads
|
|
113
|
+
if ![@stdout_thread, @stderr_thread, @stdin_thread].all?(&:alive?)
|
|
114
|
+
abort_threads
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def generate_responses(instream)
|
|
119
|
+
t0 = Time.now
|
|
120
|
+
read_buffer = ""
|
|
121
|
+
|
|
122
|
+
while @stdout_thread.alive? do
|
|
123
|
+
current_output = consume_stdout
|
|
124
|
+
if !current_output.empty?
|
|
125
|
+
# Found new output, put it in our read_buffer and reset the timer
|
|
126
|
+
t0 = Time.now
|
|
127
|
+
print current_output if @verbose
|
|
128
|
+
read_buffer << current_output
|
|
129
|
+
elsif Time.now - t0 > @prompt_delay_threshold
|
|
130
|
+
# No new input and we've been waiting long enough to respond
|
|
131
|
+
rule = next_rule
|
|
132
|
+
puts "dt = #{Time.now - t0}, applying #{rule}" if @debug
|
|
133
|
+
response = determine_response(rule, read_buffer)
|
|
134
|
+
puts "Response: #{response.inspect}" if @debug
|
|
135
|
+
case response
|
|
136
|
+
when nil then nil
|
|
137
|
+
when :skip then next
|
|
138
|
+
when :wait
|
|
139
|
+
t0 = Time.now
|
|
140
|
+
@session_rules.unshift(rule.merge(repeat: 0))
|
|
141
|
+
when :abort
|
|
142
|
+
$stderr.puts "^Abort!" if @verbose
|
|
143
|
+
abort_threads
|
|
144
|
+
break
|
|
145
|
+
else
|
|
146
|
+
t0 = Time.now
|
|
147
|
+
puts "#{response}" if @verbose
|
|
148
|
+
instream.puts response
|
|
149
|
+
read_buffer = ""
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
nil # Be patient, wait longer.
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Abort if we exceed the timeout
|
|
156
|
+
if Time.now - t0 > @timeout
|
|
157
|
+
$stderr.puts "Timeout: abort!" if @verbose
|
|
158
|
+
abort_threads
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Wait a little
|
|
162
|
+
sleep(0.1)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# For completion, read the rest of the output
|
|
166
|
+
read_buffer << consume_stdout
|
|
167
|
+
puts read_buffer
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def next_rule
|
|
171
|
+
rule = @session_rules.first or return
|
|
172
|
+
rule = rule.dup
|
|
173
|
+
rule[:repeat] <= 0 ? @session_rules.shift : @session_rules.first[:repeat] -= 1
|
|
174
|
+
return rule
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def abort_threads
|
|
178
|
+
@stdout_thread.kill
|
|
179
|
+
@stderr_thread.kill
|
|
180
|
+
@stdin_thread.kill
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def determine_response(rule, read_buffer)
|
|
184
|
+
# determine which rule key, if any match the current read_buffer
|
|
185
|
+
return if rule.nil?
|
|
186
|
+
matcher, responder = rule.find do |matcher, responder|
|
|
187
|
+
case matcher
|
|
188
|
+
when Regexp then read_buffer =~ matcher
|
|
189
|
+
when Proc then matcher.call(read_buffer)
|
|
190
|
+
when String then read_buffer.split("\n").last == matcher
|
|
191
|
+
end
|
|
192
|
+
end || [:default, rule[:default]]
|
|
193
|
+
|
|
194
|
+
# and generate the response accordingly
|
|
195
|
+
response = case responder
|
|
196
|
+
when Proc then responder.call(read_buffer)
|
|
197
|
+
when :wait, :skip, :abort then responder
|
|
198
|
+
else responder.to_s
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
return response
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def consume_stdout
|
|
205
|
+
if !@stdout_buffer.empty?
|
|
206
|
+
output = @stdout_buffer.dup
|
|
207
|
+
@stdout_buffer.clear
|
|
208
|
+
return output
|
|
209
|
+
else
|
|
210
|
+
return ""
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: stdin_responder
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Schwartz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-10-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Monitor stdout and/or stderr, and feed responses to stdin based on rules
|
|
14
|
+
email: the.andrew.h.schwartz@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/stdin_responder.rb
|
|
20
|
+
homepage:
|
|
21
|
+
licenses:
|
|
22
|
+
- MIT
|
|
23
|
+
metadata: {}
|
|
24
|
+
post_install_message:
|
|
25
|
+
rdoc_options: []
|
|
26
|
+
require_paths:
|
|
27
|
+
- lib
|
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
34
|
+
requirements:
|
|
35
|
+
- - ">="
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
requirements: []
|
|
39
|
+
rubyforge_project:
|
|
40
|
+
rubygems_version: 2.4.8
|
|
41
|
+
signing_key:
|
|
42
|
+
specification_version: 4
|
|
43
|
+
summary: Automated interaction with shell
|
|
44
|
+
test_files: []
|