stdin_responder 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/stdin_responder.rb +214 -0
  3. metadata +44 -0
@@ -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: []