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.
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: []