shells 0.1.23 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ module Shells
2
+ class ShellBase
3
+
4
+ ##
5
+ # Sets the code to be run when debug messages are processed.
6
+ #
7
+ # The code will receive the debug message as an argument.
8
+ #
9
+ # on_debug do |msg|
10
+ # puts msg
11
+ # end
12
+ #
13
+ def self.on_debug(proc = nil, &block)
14
+ add_hook :on_debug, proc, &block
15
+ end
16
+
17
+ protected
18
+
19
+ ##
20
+ # Processes a debug message.
21
+ def self.debug(msg) #:doc:
22
+ run_static_hook :on_debug, msg
23
+ end
24
+
25
+ ##
26
+ # Processes a debug message for an instance.
27
+ #
28
+ # This is processed synchronously.
29
+ def debug(msg) #:doc:
30
+ if have_hook?(:on_debug)
31
+ sync { self.class.debug msg }
32
+ end
33
+ end
34
+
35
+
36
+ end
37
+ end
@@ -0,0 +1,175 @@
1
+ module Shells
2
+ class ShellBase
3
+
4
+ ##
5
+ # Gets the exit code from the last command if it was retrieved.
6
+ attr_accessor :last_exit_code
7
+ private :last_exit_code=
8
+
9
+ add_hook :on_before_run do |sh|
10
+ sh.instance_eval do
11
+ self.last_exit_code = nil
12
+ end
13
+ end
14
+
15
+ add_hook :on_after_run do |sh|
16
+ sh.instance_eval do
17
+ self.last_exit_code = nil
18
+ end
19
+ end
20
+
21
+ ##
22
+ # Executes a command during the shell session.
23
+ #
24
+ # If called outside of the +new+ block, this will raise an error.
25
+ #
26
+ # The +command+ is the command to execute in the shell.
27
+ #
28
+ # The +options+ can be used to override the exit code behavior.
29
+ # In all cases, the :default option is the same as not providing the option and will cause +exec+
30
+ # to inherit the option from the shell's options.
31
+ #
32
+ # +retrieve_exit_code+::
33
+ # This can be one of :default, true, or false.
34
+ # +on_non_zero_exit_code+::
35
+ # This can be on ot :default, :ignore, or :raise.
36
+ # +silence_timeout+::
37
+ # This can be :default or the number of seconds to wait in silence before timing out.
38
+ # +command_timeout+::
39
+ # This can be :default or the maximum number of seconds to wait for a command to finish before timing out.
40
+ #
41
+ # If provided, the +block+ is a chunk of code that will be processed every time the
42
+ # shell receives output from the program. If the block returns a string, the string
43
+ # will be sent to the shell. This can be used to monitor processes or monitor and
44
+ # interact with processes. The +block+ is optional.
45
+ #
46
+ # shell.exec('sudo -p "password:" nginx restart') do |data,type|
47
+ # return 'super-secret' if /password:$/.match(data)
48
+ # nil
49
+ # end
50
+ #
51
+ def exec(command, options = {}, &block)
52
+ raise Shells::NotRunning unless running?
53
+
54
+ options ||= {}
55
+ options = { timeout_error: true, get_output: true }.merge(options)
56
+ options = self.options.merge(options.inject({}) { |m,(k,v)| m[k.to_sym] = v; m })
57
+ options[:retrieve_exit_code] = self.options[:retrieve_exit_code] if options[:retrieve_exit_code] == :default
58
+ options[:on_non_zero_exit_code] = self.options[:on_non_zero_exit_code] unless [:raise, :ignore].include?(options[:on_non_zero_exit_code])
59
+ options[:silence_timeout] = self.options[:silence_timeout] if options[:silence_timeout] == :default
60
+ options[:command_timeout] = self.options[:command_timeout] if options[:command_timeout] == :default
61
+ options[:command_is_echoed] = true if options[:command_is_echoed].nil?
62
+ ret = ''
63
+
64
+ merge_local_buffer do
65
+ begin
66
+ # buffer while also passing data to the supplied block.
67
+ if block_given?
68
+ buffer_output(&block)
69
+ end
70
+
71
+ command = command.to_s
72
+
73
+ # send the command and wait for the prompt to return.
74
+ debug 'Queueing command: ' + command
75
+ queue_input command + line_ending
76
+ if wait_for_prompt(options[:silence_timeout], options[:command_timeout], options[:timeout_error])
77
+ # get the output of the command, minus the trailing prompt.
78
+ ret =
79
+ if options[:get_output]
80
+ debug 'Reading output of command...'
81
+ command_output command, options[:command_is_echoed]
82
+ else
83
+ ''
84
+ end
85
+
86
+ if options[:retrieve_exit_code]
87
+ self.last_exit_code = get_exit_code
88
+ if options[:on_non_zero_exit_code] == :raise
89
+ raise NonZeroExitCode.new(last_exit_code) unless last_exit_code == 0 || last_exit_code == :undefined
90
+ end
91
+ else
92
+ self.last_exit_code = nil
93
+ end
94
+ else
95
+ # A timeout occurred and timeout_error was set to false.
96
+ self.last_exit_code = :timeout
97
+ ret = output
98
+ end
99
+
100
+ ensure
101
+ # return buffering to normal.
102
+ if block_given?
103
+ buffer_output
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ ret
110
+ end
111
+
112
+ ##
113
+ # Executes a command specifically for the exit code.
114
+ #
115
+ # Does not return the output of the command, only the exit code.
116
+ def exec_for_code(command, options = {}, &block)
117
+ options = (options || {}).merge(retrieve_exit_code: true, on_non_zero_exit_code: :ignore)
118
+ exec command, options, &block
119
+ last_exit_code
120
+ end
121
+
122
+ ##
123
+ # Executes a command ignoring any exit code.
124
+ #
125
+ # Returns the output of the command and does not even retrieve the exit code.
126
+ def exec_ignore_code(command, options = {}, &block)
127
+ options = (options || {}).merge(retrieve_exit_code: false, on_non_zero_exit_code: :ignore)
128
+ exec command, options, &block
129
+ end
130
+
131
+ protected
132
+
133
+ ##
134
+ # Gets the output from a command.
135
+ def command_output(command, expect_command = true) #:doc:
136
+ # get everything except for the ending prompt.
137
+ ret =
138
+ if (prompt_pos = (output =~ prompt_match))
139
+ output[0...prompt_pos]
140
+ else
141
+ output
142
+ end
143
+
144
+ if expect_command
145
+ command_regex = command_match(command)
146
+
147
+ # Go until we run out of data or we find one of the possible command starts.
148
+ # Note that we EXPECT the command to the first line of the output from the command because we expect the
149
+ # shell to echo it back to us.
150
+ result_cmd,_,result_data = ret.partition("\n")
151
+ until result_data.to_s.strip == '' || result_cmd.strip =~ command_regex
152
+ result_cmd,_,result_data = result_data.partition("\n")
153
+ end
154
+
155
+ if result_cmd.nil? || !(result_cmd =~ command_regex)
156
+ STDERR.puts "SHELL WARNING: Failed to match #{command_regex.inspect}."
157
+ end
158
+
159
+ result_data
160
+ else
161
+ ret
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def command_match(command)
168
+ p = regex_escape options[:prompt]
169
+ c = regex_escape command
170
+ /\A(?:#{p}\s*)?#{c}[ \t]*\z/
171
+ end
172
+
173
+
174
+ end
175
+ end
@@ -0,0 +1,83 @@
1
+ module Shells
2
+ class ShellBase
3
+
4
+ private
5
+
6
+ def self.hooks
7
+ @hooks ||= {}
8
+ end
9
+
10
+ def self.parent_hooks(name)
11
+ superclass.respond_to?(:all_hooks, true) ? superclass.send(:all_hooks, name) : []
12
+ end
13
+
14
+ def self.all_hooks(name)
15
+ parent_hooks(name) + (hooks[name] || [])
16
+ end
17
+
18
+ protected
19
+
20
+ ##
21
+ # Adds a hook method to the class.
22
+ #
23
+ # A hook method should return :break if it wants to cancel executing any other hook methods in the chain.
24
+ def self.add_hook(hook_name, proc = nil, &block) #:doc:
25
+ hooks[hook_name] ||= []
26
+
27
+ if proc.respond_to?(:call)
28
+ hooks[hook_name] << proc
29
+ elsif proc.is_a?(Symbol) || proc.is_a?(String)
30
+ if self.respond_to?(proc, true)
31
+ hooks[hook_name] << method(proc.to_sym)
32
+ end
33
+ elsif proc
34
+ raise ArgumentError, 'proc must respond to :call method or be the name of a static method in this class'
35
+ end
36
+
37
+ if block
38
+ hooks[hook_name] << block
39
+ end
40
+
41
+ end
42
+
43
+
44
+
45
+ ##
46
+ # Runs a hook statically.
47
+ #
48
+ # The arguments supplied are passed to the hook methods directly.
49
+ #
50
+ # Return false unless the hook was executed. Returns :break if one of the hook methods returns :break.
51
+ def self.run_static_hook(hook_name, *args)
52
+ list = all_hooks(hook_name)
53
+ list.each do |hook|
54
+ result = hook.call(*args)
55
+ return :break if result == :break
56
+ end
57
+ list.any?
58
+ end
59
+
60
+ ##
61
+ # Runs a hook in the current shell instance.
62
+ #
63
+ # The hook method is passed the shell as the first argument then the arguments passed to this method.
64
+ #
65
+ # Return false unless the hook was executed. Returns :break if one of the hook methods returns :break.
66
+ def run_hook(hook_name, *args)
67
+ list = self.class.all_hooks(hook_name)
68
+ shell = self
69
+ list.each do |hook|
70
+ result = hook.call(shell, *args)
71
+ return :break if result == :break
72
+ end
73
+ list.any?
74
+ end
75
+
76
+ ##
77
+ # Returns true if there are any hooks to run.
78
+ def have_hook?(hook_name)
79
+ self.class.all_hooks(hook_name).any?
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,50 @@
1
+ module Shells
2
+ class ShellBase
3
+
4
+ attr_accessor :input_fifo
5
+ private :input_fifo, :input_fifo
6
+
7
+ add_hook :on_before_run do |sh|
8
+ sh.instance_eval do
9
+ self.input_fifo = []
10
+ end
11
+ end
12
+
13
+ add_hook :on_after_run do |sh|
14
+ sh.instance_eval do
15
+ self.input_fifo = nil
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Defines the line ending used to terminate commands sent to the shell.
21
+ #
22
+ # The default is "\n". If you need "\r\n", "\r", or some other value, simply override this function.
23
+ def line_ending
24
+ "\n"
25
+ end
26
+
27
+ protected
28
+
29
+ ##
30
+ # Adds input to be sent to the shell.
31
+ def queue_input(data) #:doc:
32
+ sync do
33
+ if options[:unbuffered_input]
34
+ data = data.chars
35
+ input_fifo.push *data
36
+ else
37
+ input_fifo.push data
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def next_input
45
+ sync { input_fifo.shift }
46
+ end
47
+
48
+
49
+ end
50
+ end
@@ -0,0 +1,149 @@
1
+ module Shells
2
+ class ShellBase
3
+
4
+ protected
5
+
6
+ ##
7
+ # Sets up the shell session.
8
+ #
9
+ # This method is called after connecting the shell before the session block is run.
10
+ #
11
+ # By default this method will wait for the prompt to appear in the output.
12
+ #
13
+ # If you need to set the prompt, you would want to do it here.
14
+ def setup #:doc:
15
+ setup_prompt
16
+ end
17
+
18
+ ##
19
+ # Sets up the prompt for the shell session.
20
+ #
21
+ # By default this method will wait for the prompt to appear in the output.
22
+ #
23
+ # If you need to set the prompt, you would want to do it here.
24
+ def setup_prompt #:doc:
25
+ wait_for_prompt 30, 30, true
26
+ end
27
+
28
+
29
+ ##
30
+ # Tears down the shell session.
31
+ #
32
+ # This method is called after the session block is run before disconnecting the shell.
33
+ #
34
+ # The default implementation simply sends the quit command to the shell and waits up to 1 second for a result.
35
+ #
36
+ # This method will be called even if an exception is raised during the session.
37
+ def teardown #:doc:
38
+ unless options[:quit].to_s.strip == ''
39
+ self.ignore_io_error = true
40
+ exec_ignore_code options[:quit], command_timeout: 1, timeout_error: false
41
+ end
42
+ end
43
+
44
+ ##
45
+ # Connects to the shell.
46
+ #
47
+ # You must define this method in your subclass.
48
+ def connect #:doc:
49
+ raise ::NotImplementedError
50
+ end
51
+
52
+ ##
53
+ # Disconnects from the shell.
54
+ #
55
+ # You must define this method in your subclass.
56
+ # This method will always be called, even if an exception occurs during the session.
57
+ def disconnect #:doc:
58
+ raise ::NotImplementedError
59
+ end
60
+
61
+ ##
62
+ # Determines if the shell is currently active.
63
+ #
64
+ # You must define this method in your subclass.
65
+ def active? #:doc:
66
+ raise ::NotImplementedError
67
+ end
68
+
69
+ ##
70
+ # Runs the IO loop on the shell while the block returns true.
71
+ #
72
+ # You must define this method in your subclass.
73
+ # It should block for as little time as necessary before yielding to the block.
74
+ def io_loop(&block) #:doc:
75
+ raise ::NotImplementedError
76
+ end
77
+
78
+ ##
79
+ # Sends data to the shell.
80
+ #
81
+ # You must define this method in your subclass.
82
+ #
83
+ # It is important that this method not be called directly outside of the +run+ method.
84
+ # Use +queue_input+ to send data to the shell so that it can be handled in a synchronous manner.
85
+ def send_data(data) #:doc:
86
+ raise ::NotImplementedError
87
+ end
88
+
89
+ ##
90
+ # Register a callback to run when stdout data is received.
91
+ #
92
+ # The block will be passed the data received.
93
+ #
94
+ # You must define this method in your subclass and it should set a hook to be called when data is received.
95
+ #
96
+ # def stdout_received
97
+ # @conn.on_stdout do |data|
98
+ # yield data
99
+ # end
100
+ # end
101
+ #
102
+ def stdout_received(&block) #:doc:
103
+ raise ::NotImplementedError
104
+ end
105
+
106
+ ##
107
+ # Register a callback to run when stderr data is received.
108
+ #
109
+ # The block will be passed the data received.
110
+ #
111
+ # You must define this method in your subclass and it should set a hook to be called when data is received.
112
+ #
113
+ # def stderr_received
114
+ # @conn.on_stderr do |data|
115
+ # yield data
116
+ # end
117
+ # end
118
+ #
119
+ def stderr_received(&block) #:doc:
120
+ raise ::NotImplementedError
121
+ end
122
+
123
+ ##
124
+ # Gets the exit code from the last command.
125
+ #
126
+ # You must define this method in your subclass to utilize exit codes.
127
+ def get_exit_code #:doc:
128
+ self.last_exit_code = :undefined
129
+ end
130
+
131
+ public
132
+
133
+ ##
134
+ # Reads from a file on the device.
135
+ def read_file(path)
136
+ nil
137
+ end
138
+
139
+ ##
140
+ # Writes to a file on the device.
141
+ def write_file(path, data)
142
+ false
143
+ end
144
+
145
+
146
+
147
+
148
+ end
149
+ end