cheetah 0.1.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 (6) hide show
  1. data/CHANGELOG +4 -0
  2. data/LICENSE +22 -0
  3. data/README.md +6 -0
  4. data/VERSION +1 -0
  5. data/lib/cheetah.rb +222 -0
  6. metadata +98 -0
@@ -0,0 +1,4 @@
1
+ 0.1.0 (2012-03-23)
2
+ ------------------
3
+
4
+ * Initial release.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 SUSE
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,6 @@
1
+ Cheetah
2
+ =======
3
+
4
+ Cheetah is a simple library for executing external commands safely and conveniently. It is meant as a safe replacement of `backticks`, Kernel#system and similar methods, which are often used in unsecure way (they allow shell expansion of commands).
5
+
6
+ Proper documentation is coming soon.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,222 @@
1
+ # Contains methods for executing external commands safely and conveniently.
2
+ module Cheetah
3
+ VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip
4
+
5
+ # Exception raised when a command execution fails.
6
+ class ExecutionFailed < StandardError
7
+ attr_reader :command, :args, :status, :stdout, :stderr
8
+
9
+ def initialize(command, args, status, stdout, stderr, message = nil)
10
+ super(message)
11
+ @command = command
12
+ @args = args
13
+ @status = status
14
+ @stdout = stdout
15
+ @stderr = stderr
16
+ end
17
+ end
18
+
19
+ # Runs an external command, optionally capturing its output. Meant as a safe
20
+ # replacement of `backticks`, Kernel#system and similar methods, which are
21
+ # often used in unsecure way. (They allow shell expansion of commands, which
22
+ # often means their arguments need proper escaping. The problem is that people
23
+ # forget to do it or do it badly, causing serious security issues.)
24
+ #
25
+ # Examples:
26
+ #
27
+ # # Run a command, grab its output and handle failures.
28
+ # files = nil
29
+ # begin
30
+ # files = Cheetah.run("ls", "-la", :capture => :stdout)
31
+ # rescue Cheetah::ExecutionFailed => e
32
+ # puts "Command #{e.command} failed with status #{e.status}."
33
+ # end
34
+ #
35
+ # # Log the executed command, it's status, input and both outputs into
36
+ # # user-supplied logger.
37
+ # Cheetah.run("qemu-kvm", "foo.raw", :logger => my_logger)
38
+ #
39
+ # The first parameter specifies the command to run, the remaining parameters
40
+ # specify its arguments. It is also possible to specify both the command and
41
+ # arguments in the first parameter using an array. If the last parameter is a
42
+ # hash, it specifies options.
43
+ #
44
+ # For security reasons, the command never goes through shell expansion even if
45
+ # only one parameter is specified (i.e. the method does do not adhere to the
46
+ # convention used by other Ruby methods for launching external commands, e.g.
47
+ # Kernel#system).
48
+ #
49
+ # If the command execution succeeds, the returned value depends on the
50
+ # value of the :capture option (see below). If it fails (the command is not
51
+ # executed for some reason or returns a non-zero exit status), the method
52
+ # raises a ExecutionFailed exception with detailed information about the
53
+ # failure.
54
+ #
55
+ # Options:
56
+ #
57
+ # :capture - configures which output(s) the method captures and returns, the
58
+ # valid values are:
59
+ #
60
+ # - nil - no output is captured and returned
61
+ # (the default)
62
+ # - :stdout - standard output is captured and
63
+ # returned as a string
64
+ # - :stderr - error output is captured and returned
65
+ # as a string
66
+ # - [:stdout, :stderr] - both outputs are captured and returned
67
+ # as a two-element array of strings
68
+ #
69
+ # :stdin - if specified, it is a string sent to command's standard input
70
+ #
71
+ # :logger - if specified, the method will log the command, its status, input
72
+ # and both outputs to passed logger at the "debug" level
73
+ #
74
+ def self.run(command, *args)
75
+ options = args.last.is_a?(Hash) ? args.pop : {}
76
+
77
+ capture = options[:capture]
78
+ stdin = options[:stdin] || ""
79
+ logger = options[:logger]
80
+
81
+ if command.is_a?(Array)
82
+ args = command[1..-1]
83
+ command = command.first
84
+ end
85
+
86
+ pass_stdin = !stdin.empty?
87
+ pipe_stdin_read, pipe_stdin_write = pass_stdin ? IO.pipe : [nil, nil]
88
+
89
+ capture_stdout = [:stdout, [:stdout, :stderr]].include?(capture) || logger
90
+ pipe_stdout_read, pipe_stdout_write = capture_stdout ? IO.pipe : [nil, nil]
91
+
92
+ capture_stderr = [:stderr, [:stdout, :stderr]].include?(capture) || logger
93
+ pipe_stderr_read, pipe_stderr_write = capture_stderr ? IO.pipe : [nil, nil]
94
+
95
+ if logger
96
+ logger.debug "Executing command #{command.inspect} with #{describe_args(args)}."
97
+ logger.debug "Standard input: " + (stdin.empty? ? "(none)" : stdin)
98
+ end
99
+
100
+ pid = fork do
101
+ begin
102
+ if pass_stdin
103
+ pipe_stdin_write.close
104
+ STDIN.reopen(pipe_stdin_read)
105
+ pipe_stdin_read.close
106
+ else
107
+ STDIN.reopen("/dev/null", "r")
108
+ end
109
+
110
+ if capture_stdout
111
+ pipe_stdout_read.close
112
+ STDOUT.reopen(pipe_stdout_write)
113
+ pipe_stdout_write.close
114
+ else
115
+ STDOUT.reopen("/dev/null", "w")
116
+ end
117
+
118
+ if capture_stderr
119
+ pipe_stderr_read.close
120
+ STDERR.reopen(pipe_stderr_write)
121
+ pipe_stderr_write.close
122
+ else
123
+ STDERR.reopen("/dev/null", "w")
124
+ end
125
+
126
+ # All file descriptors from 3 above should be closed here, but since I
127
+ # don't know about any way how to detect the maximum file descriptor
128
+ # number portably in Ruby, I didn't implement it. Patches welcome.
129
+
130
+ exec([command, command], *args)
131
+ rescue SystemCallError => e
132
+ exit!(127)
133
+ end
134
+ end
135
+
136
+ [pipe_stdin_read, pipe_stdout_write, pipe_stderr_write].each { |p| p.close if p }
137
+
138
+ # We write the command's input and read its output using a select loop. Why?
139
+ # Because otherwise we could end up with a deadlock.
140
+ #
141
+ # Imagine if we first read the whole standard output and then the whole
142
+ # error output, but the executed command would write lot of data but only to
143
+ # the error output. Sooner or later it would fill the buffer and block,
144
+ # while we would be blocked on reading the standard output -- classic
145
+ # deadlock.
146
+ #
147
+ # Similar issues can happen with standard input vs. one of the outputs.
148
+ if pass_stdin || capture_stdout || capture_stderr
149
+ stdout = ""
150
+ stderr = ""
151
+
152
+ loop do
153
+ pipes_readable = [pipe_stdout_read, pipe_stderr_read].compact.select { |p| !p.closed? }
154
+ pipes_writable = [pipe_stdin_write].compact.select { |p| !p.closed? }
155
+
156
+ break if pipes_readable.empty? && pipes_writable.empty?
157
+
158
+ ios_read, ios_write, ios_error = select(pipes_readable, pipes_writable,
159
+ pipes_readable + pipes_writable)
160
+
161
+ if !ios_error.empty?
162
+ raise IOError, "Error when communicating with executed program."
163
+ end
164
+
165
+ if ios_read.include?(pipe_stdout_read)
166
+ begin
167
+ stdout += pipe_stdout_read.readpartial(4096)
168
+ rescue EOFError
169
+ pipe_stdout_read.close
170
+ end
171
+ end
172
+
173
+ if ios_read.include?(pipe_stderr_read)
174
+ begin
175
+ stderr += pipe_stderr_read.readpartial(4096)
176
+ rescue EOFError
177
+ pipe_stderr_read.close
178
+ end
179
+ end
180
+
181
+ if ios_write.include?(pipe_stdin_write)
182
+ n = pipe_stdin_write.syswrite(stdin)
183
+ stdin = stdin[n..-1]
184
+ pipe_stdin_write.close if stdin.empty?
185
+ end
186
+ end
187
+ end
188
+
189
+ pid, status = Process.wait2(pid)
190
+ begin
191
+ if !status.success?
192
+ raise ExecutionFailed.new(command, args, status,
193
+ capture_stdout ? stdout : nil,
194
+ capture_stderr ? stderr : nil,
195
+ "Execution of command #{command.inspect} " +
196
+ "with #{describe_args(args)} " +
197
+ "failed with status #{status.exitstatus}.")
198
+ end
199
+ ensure
200
+ if logger
201
+ logger.debug "Status: #{status.exitstatus}"
202
+ logger.debug "Standard output: " + (stdout.empty? ? "(none)" : stdout)
203
+ logger.debug "Error output: " + (stderr.empty? ? "(none)" : stderr)
204
+ end
205
+ end
206
+
207
+ case capture
208
+ when nil
209
+ nil
210
+ when :stdout
211
+ stdout
212
+ when :stderr
213
+ stderr
214
+ when [:stdout, :stderr]
215
+ [stdout, stderr]
216
+ end
217
+ end
218
+
219
+ def self.describe_args(args)
220
+ args.empty? ? "no arguments" : "arguments #{args.map(&:inspect).join(", ")}"
221
+ end
222
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cheetah
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - David Majda
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-23 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: shoulda-context
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: mocha
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: Cheetah is a simple library for executing external commands safely and conveniently. It is meant as a safe replacement of `backticks`, Kernel#system and similar methods, which are often used in unsecure way (they allow shell expansion of commands).
50
+ email: dmajda@suse.de
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - CHANGELOG
59
+ - LICENSE
60
+ - README.md
61
+ - VERSION
62
+ - lib/cheetah.rb
63
+ has_rdoc: true
64
+ homepage: https://github.com/openSUSE/cheetah
65
+ licenses:
66
+ - MIT
67
+ post_install_message:
68
+ rdoc_options: []
69
+
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ requirements: []
91
+
92
+ rubyforge_project:
93
+ rubygems_version: 1.6.2
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Simple library for executing external commands safely and conveniently
97
+ test_files: []
98
+