shells 0.1.23 → 0.2.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.
@@ -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