cheetah 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+