dia 1.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -0,0 +1,67 @@
1
+ # Dia
2
+ Through the use of technology found on Apple's Leopard and Snow Leopard
3
+ operating systems, Dia can create dynamic and robust sandbox environments
4
+ for applications and for blocks of ruby code. The Ruby API was designed to be
5
+ simple, and a joy to use. I hope you feel the same way :-)
6
+
7
+ ## Quick Example
8
+
9
+ * RubyBlock
10
+
11
+ require('rubygems')
12
+ require('dia')
13
+ require('open-uri')
14
+
15
+ sandbox = Dia::RubyBlock.new(Dia::Profiles::NO_INTERNET) do
16
+ open('http://www.google.com')
17
+ end
18
+
19
+ sandbox.rescue_exception = true
20
+ sandbox.run
21
+ puts "Exception : #{sandbox.exception.klass}"
22
+ puts "Message : #{sandbox.exception.message}"
23
+
24
+ * Application
25
+
26
+ require('rubygems')
27
+ require('dia')
28
+
29
+ sandbox = Dia::Application.new(Dia::Profiles::NO_INTERNET,
30
+ '/path/to/firefox')
31
+
32
+ sandbox.run_nonblock
33
+ sandbox.terminate
34
+
35
+ ## Documentation
36
+
37
+ * [API Documentation](http://yardoc.org/docs/robgleeson-Dia/)
38
+ Written using YARD, the API documentation makes a great reference.
39
+ *The API documentation linked is for the latest stable release*
40
+
41
+ * [Mailing list](http://groups.google.com/group/ruby-dia)
42
+ Troubleshoot your problems with other Dia users on the Google Groups mailing list.
43
+
44
+ * Wiki documentation
45
+ *Work in progress*
46
+
47
+ ## Supported Rubies.
48
+
49
+ The following Ruby implementations have had the test suite run against them, and
50
+ reported a 100% success rate.
51
+
52
+ * MRI
53
+ * 1.8.7-p299
54
+ * 1.9.1-p378
55
+ * 1.9.2-rc1
56
+ * REE
57
+ * Ruby Enterprise Edition 2010.02 (1.8.7-p253)
58
+
59
+ MacRuby is not supported because it does not support Kernel.fork, and it won't add support
60
+ for fork anytime soon(if ever).
61
+ JRuby has experimental support for fork, but I haven't tried it.
62
+
63
+ ## Bugs
64
+ Bug reports are _very_ welcome, and can be reported through the
65
+ [issue tracker](http://github.com/robgleeson/dia/issues).
66
+
67
+
@@ -0,0 +1,49 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require('./lib/dia')
3
+
4
+ Gem::Specification.new do |s|
5
+
6
+ s.name = %q{dia}
7
+ s.version = Dia::VERSION
8
+ s.authors = ["Robert Gleeson"]
9
+ s.email = %q{rob@flowof.info}
10
+ s.date = Time.now.strftime("%Y-%m-%d")
11
+
12
+ s.description = %q{Through the use of technology found on Apple's Leopard and Snow Leopard
13
+ operating systems, Dia can create dynamic and robust sandbox environments
14
+ for applications and for blocks of ruby code.
15
+ The Ruby API was designed to be simple, and a joy to use.
16
+ I hope you feel the same way :-)}
17
+
18
+ s.summary = %q{Through the use of technology found on Apple's Leopard and Snow Leopard
19
+ operating systems, Dia can create dynamic and robust sandbox environments
20
+ for applications and for blocks of ruby code.
21
+ The Ruby API was designed to be simple, and a joy to use.
22
+ I hope you feel the same way :-)}
23
+
24
+
25
+ s.require_paths = ["lib"]
26
+ s.files = Dir["lib/**/*.rb"] + Dir["test/**/*.rb"] + %w(COPYING README.mkd dia.gemspec)
27
+ s.test_files = Dir["test/**/*.rb"]
28
+
29
+
30
+ s.add_runtime_dependency(%q<ffi>, ["= 0.6.2"])
31
+ s.add_development_dependency(%q<yard>, [">= 0"])
32
+ s.has_rdoc = %q{yard}
33
+
34
+ s.post_install_message = <<-MESSAGE
35
+ --------------------------------------------------------------------
36
+ Dia (#{Dia::VERSION})
37
+
38
+ Thanks for installing Dia, #{Dia::VERSION}!
39
+
40
+ >=2.0.0 releases include public API changes that are not backward
41
+ compatiable with older releases. Be sure to check the docs!
42
+
43
+ [Github] http://github.com/robgleeson/dia
44
+ [API Documentation] http://yardoc.org/docs/robgleeson-dia/
45
+ [Mailing List (new)] http://groups.google.com/group/ruby-dia
46
+ --------------------------------------------------------------------
47
+ MESSAGE
48
+
49
+ end
data/lib/dia.rb CHANGED
@@ -1,11 +1,14 @@
1
- gem 'ffi', '= 0.6.2'
2
- require 'ffi'
3
- require File.join(File.dirname(__FILE__), 'dia/profiles.rb')
4
- require File.join(File.dirname(__FILE__), 'dia/commonapi.rb')
5
- require File.join(File.dirname(__FILE__), 'dia/sandbox.rb')
1
+ gem('ffi', '0.6.2')
2
+ require('ffi')
3
+ require('ostruct')
4
+ require(File.expand_path('dia/shared_features' , File.dirname(__FILE__)))
5
+ require(File.expand_path('dia/functions' , File.dirname(__FILE__)))
6
+ require(File.expand_path('dia/profiles' , File.dirname(__FILE__)))
7
+ require(File.expand_path('dia/ruby_block' , File.dirname(__FILE__)))
8
+ require(File.expand_path('dia/application' , File.dirname(__FILE__)))
9
+ require(File.expand_path('dia/exceptions' , File.dirname(__FILE__)))
10
+ require(File.expand_path('dia/exception_struct' , File.dirname(__FILE__)))
6
11
 
7
12
  module Dia
8
- VERSION = '1.5'
9
- class SandboxException < StandardError; end
13
+ VERSION = '2.0.0'
10
14
  end
11
-
@@ -0,0 +1,49 @@
1
+ module Dia
2
+
3
+ class Application
4
+ include Dia::SharedFeatures
5
+
6
+
7
+ # @param [String] Profile Accepts one of five profiles found under the {Dia::Profiles}
8
+ # module.
9
+ #
10
+ # @param [String] Application Accepts a path to an application.
11
+ #
12
+ # @raise [ArgumentError] It may raise an ArgumentError if it isn't possible to use a
13
+ # particular profile with this type of sandbox.
14
+ def initialize(profile, app)
15
+ @profile = profile
16
+ @app = app
17
+ raise(ArgumentError, "Dia::Profiles::NO_OS_SERVICES is not applicable to the Application " \
18
+ "sandbox.") if Dia::Profiles::NO_OS_SERVICES == @profile
19
+ end
20
+
21
+ # This method will spawn a child process, initialize a sandbox environment, and call exec()
22
+ # to start your application.
23
+ #
24
+ # @return [Fixnum] Returns the Process ID(PID) of the child process used to execute an
25
+ # application in a sandbox.
26
+ def run
27
+ @pid = fork do
28
+ initialize_sandbox
29
+ exec(@app)
30
+ end
31
+
32
+ _, @exit_status = Process.wait2(@pid)
33
+ @pid
34
+ end
35
+
36
+ # An identical but non-blocking form of {#run}.
37
+ def run_nonblock
38
+ @pid = fork do
39
+ initialize_sandbox
40
+ exec(@app)
41
+ end
42
+
43
+ @exit_status = Process.detach(@pid)
44
+ @pid
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,7 @@
1
+ module Dia
2
+ # @attr_reader [String] klass Returns Exception#class as a String.
3
+ # @attr_reader [String] message Returns Exception#message as a String.
4
+ # @attr_reader [String] backtrace Returns Exception#backtrace as a String.
5
+ class ExceptionStruct < Struct.new(:klass, :message, :backtrace)
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Dia
2
+ module Exceptions
3
+ SandboxException = Class.new(StandardError)
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ module Dia
2
+ module Functions
3
+ extend(FFI::Library)
4
+ ffi_lib(%w(system))
5
+ attach_function(:sandbox_init, [ :pointer, :uint64, :pointer ], :int)
6
+ attach_function(:sandbox_free_error, [ :pointer ], :void)
7
+ end
8
+ end
@@ -1,8 +1,7 @@
1
1
  module Dia
2
-
3
2
  module Profiles
4
- extend FFI::Library
5
- ffi_lib(%w(sandbox system libSystem.B.dylib))
3
+ extend(FFI::Library)
4
+ ffi_lib(%w(system))
6
5
 
7
6
  NO_INTERNET = attach_variable(:kSBXProfileNoInternet,
8
7
  :string).read_string
@@ -14,8 +13,5 @@ module Dia
14
13
  :string).read_string
15
14
  NO_OS_SERVICES = attach_variable(:kSBXProfilePureComputation,
16
15
  :string).read_string
17
-
18
16
  end
19
-
20
17
  end
21
-
@@ -0,0 +1,319 @@
1
+ module Dia
2
+
3
+ class RubyBlock
4
+
5
+ require('io/wait')
6
+ require('stringio')
7
+ include Dia::SharedFeatures
8
+
9
+
10
+ # @param [String] Profile Accepts one of five profiles which can be found
11
+ # under the {Dia::Profiles} module.
12
+ #
13
+ # @param [Proc] Block Accepts a block or Proc object as its second argument.
14
+ #
15
+ # @raise [ArgumentError] It will raise an ArgumentError if a profile and block
16
+ # isn't supplied to the constructor
17
+ #
18
+ # @return [Dia::RubyBlock] Returns an instance of Dia::RubyBlock.
19
+ def initialize(profile, &block)
20
+ raise(ArgumentError, "It is required that a block be passed to the constructor.\n" \
21
+ "Please consult the documentation.") unless block_given?
22
+ @profile = profile
23
+ @proc = block
24
+ @rescue = false
25
+ @redirect_stdout = false
26
+ @redirect_stderr = false
27
+ @pipes = {}
28
+ end
29
+
30
+
31
+ # When the "capture stdout" feature is enabled, this method will return the contents
32
+ # of the standard output stream for the child process used to execute your sandbox.
33
+ #
34
+ # @return [String, nil] Returns the contents of stdout.
35
+ # Returns nil when no data is available on stdout, or when the
36
+ # "capture stdout" feature is disabled.
37
+ #
38
+ # @see #redirect_stdout= This feature is disabled by default. See how to enable it.
39
+ #
40
+ def stdout
41
+ if pipes_readable?(@pipes[:stdout_reader], @pipes[:stdout_writer])
42
+ @pipes[:stdout_writer].close
43
+ @stdout = @pipes[:stdout_reader].read
44
+ @pipes[:stdout_reader].close
45
+ end
46
+ @stdout
47
+ end
48
+
49
+ # This method can enable or disable a feature that will capture standard output
50
+ # in the child process that is spawned to execute a sandbox.
51
+ #
52
+ # @param [Boolean] Boolean Accepts a true(-ish) or false(-ish) value.
53
+ # @return [Boolean] Returns the calling argument.
54
+ # @see #stdout See #stdout for accessing the contents of stdout.
55
+ def redirect_stdout=(boolean)
56
+ @redirect_stdout = boolean
57
+ end
58
+
59
+ # This method will tell you if standatd output is being redirected in the child
60
+ # process used to execute your sandbox.
61
+ #
62
+ # @see #redirect_stdout= See how to enable the "redirect stdout" feature.
63
+ #
64
+ # @return [Boolean] Returns true or false.
65
+ def redirect_stdout?
66
+ !!@rescue_stdout
67
+ end
68
+
69
+ # This method will tell you if standard error is being redirected in the child process
70
+ # used to execute your sandbox.
71
+ #
72
+ # @see #redirect_stderr= See how to enable the "redirect stderr" feature.
73
+ # @return [Boolean] returns true or false.
74
+ def redirect_stderr?
75
+ !!@rescue_stderr
76
+ end
77
+
78
+ # This method can enable or disable a feature that will capture standard error output
79
+ # in the child process that is spawned to execute a sandbox.
80
+ #
81
+ # @param [Boolean] Boolean Accepts a true(-ish) or false(-ish) value.
82
+ # @return [Boolean] Returns the calling argument.
83
+ # @see #stdout See #stderr for accessing the contents of stderr.
84
+ def redirect_stderr=(boolean)
85
+ @redirect_stderr = boolean
86
+ end
87
+
88
+
89
+ # When the "capture stderr" feature is enabled, this method will return the contents
90
+ # of the standard error stream for the child process used to execute your sandbox.
91
+ #
92
+ # @return [String, nil] Returns the contents of stderr.
93
+ # Returns nil when no data is available on stderr, or when the
94
+ # "capture stderr" feature is disabled.
95
+ #
96
+ # @see #redirect_stderr= This feature is disabled by default. See how to enable it.
97
+ #
98
+ def stderr
99
+ if pipes_readable?(@pipes[:stderr_reader], @pipes[:stderr_writer])
100
+ @pipes[:stderr_writer].close
101
+ @stderr = @pipes[:stderr_reader].read
102
+ @pipes[:stderr_reader].close
103
+ end
104
+ @stderr
105
+ end
106
+
107
+ # This method will tell you if an exception has been raised in the child process
108
+ # used to execute your sandbox.
109
+ # The "capture exception" feature must be enabled for this method to ever
110
+ # return true.
111
+ #
112
+ # @see #rescue_exception= See #rescue_exception= for enabling the capture
113
+ # of raised exceptions in your sandbox.
114
+ #
115
+ # @see #exception See the #exception method for accessing an
116
+ # exception raised in your sandbox.
117
+ #
118
+ # @return [Boolean] Returns true or false.
119
+ def exception_raised?
120
+ !!exception
121
+ end
122
+
123
+ # This method will tell you if the {#rescue_exception=} feature is enabled by
124
+ # returning a boolean.
125
+ #
126
+ # @see #rescue_exception= See #rescue_exception= for enabling the capture
127
+ # of raised exceptions in your sandbox.
128
+ #
129
+ # @see #exception See the #exception method for accessing an
130
+ # exception raised in your sandbox.
131
+ #
132
+ # @return [Boolean] Returns true or false.
133
+ # @since 2.0.0
134
+ def rescue_exception?
135
+ !!@rescue
136
+ end
137
+
138
+ # This method can enable or disable a feature that will try to capture
139
+ # raised exceptions in the child process that is spawned to execute a sandbox.
140
+ #
141
+ # @param [Boolean] Boolean Accepts a true(-ish) or false(-ish) value.
142
+ #
143
+ # @return [Boolean] Returns the calling argument.
144
+ #
145
+ # @see #exception See #exception for information on how to access
146
+ # the data of an exception raised in your sandbox.
147
+ #
148
+ # @since 2.0.0
149
+ def rescue_exception=(boolean)
150
+ @rescue = boolean
151
+ end
152
+
153
+ # When the "capture exceptions" feature is enabled and an exception has been raised in
154
+ # the child process used to execute your sandbox, this method will return a subclass
155
+ # of Struct whose attributes represent the exception data.
156
+ #
157
+ # Every call {#run} or {#run_nonblock} will reset the instance variable referencing the
158
+ # object storing exception data to nil.
159
+ #
160
+ # @return [Dia::ExceptionStruct, nil] Returns an instance of {Dia::ExceptionStruct} or nil
161
+ # when there is no exception available.
162
+ #
163
+ # @see #rescue_exception= The "capture exception" feature is disabled by default.
164
+ # See how to enable it.
165
+ #
166
+ # @see Dia::ExceptionStruct The documentation for Dia::ExceptionStruct.
167
+ #
168
+ # @since 1.5
169
+ def exception
170
+ if pipes_readable?(@pipes[:exception_reader], @pipes[:exception_writer])
171
+ @pipes[:exception_writer].close
172
+ @e = ExceptionStruct.new *Marshal.load(@pipes[:exception_reader].read).values_at(:klass,
173
+ :message,
174
+ :backtrace)
175
+ @pipes[:exception_reader].close
176
+ end
177
+ @e
178
+ end
179
+
180
+ # The run method will spawn a child process to execute the block supplied to the constructer
181
+ # in a sandbox.
182
+ # This method will block. See {#run_nonblock} for the non-blocking form of
183
+ # this method.
184
+ #
185
+ # @param [Arguments] Arguments A variable amount of arguments that will
186
+ # be passed onto the block supplied to the
187
+ # constructer. Optional.
188
+ #
189
+ # @raise [SystemCallError] It may raise a number of subclasses of SystemCallError
190
+ # in a child process if a sandbox violates imposed
191
+ # restrictions.
192
+ #
193
+ # @raise [Dia::SandboxException] It may raise
194
+ # {Dia::Exceptions::SandboxException}
195
+ # in a child process if it was not possible
196
+ # to initialize a sandbox environment.
197
+ #
198
+ # @return [Fixnum] The Process ID(PID) that the sandbox has
199
+ # been launched under.
200
+ def run(*args)
201
+ launch(*args)
202
+
203
+ # parent ..
204
+ _, @exit_status = Process.wait2(@pid)
205
+ @pid
206
+ end
207
+
208
+ # An identical, but non-blocking form of {#run}.
209
+ def run_nonblock(*args)
210
+ launch(*args)
211
+
212
+ @exit_status = Process.detach(@pid)
213
+ @pid
214
+ end
215
+
216
+ private
217
+ # @api private
218
+ def launch(*args)
219
+ @e = @stdout = nil
220
+ close_pipes_if_needed
221
+ open_pipes_if_needed
222
+
223
+ @pid = fork do
224
+ redirect(:stdout) if @redirect_stdout
225
+ redirect(:stderr) if @redirect_stderr
226
+ if @rescue
227
+ begin
228
+ initialize_sandbox
229
+ @proc.call(*args)
230
+ rescue SystemExit, SignalException, NoMemoryError => e
231
+ raise(e)
232
+ rescue Exception => e
233
+ begin
234
+ write_exception(e)
235
+ rescue SystemExit, SignalException, NoMemoryError => e
236
+ raise(e)
237
+ rescue Exception => e
238
+ write_exception(e)
239
+ end
240
+ ensure
241
+ write_stdout_and_stderr_if_needed
242
+ close_pipes_if_needed
243
+ end
244
+ else
245
+ begin
246
+ initialize_sandbox
247
+ @proc.call(*args)
248
+ ensure
249
+ write_stdout_and_stderr_if_needed
250
+ close_pipes_if_needed
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ # @api private
257
+ def write_stdout_and_stderr_if_needed
258
+ if @redirect_stdout
259
+ $stdout.rewind
260
+ @pipes[:stdout_reader].close
261
+ @pipes[:stdout_writer].write($stdout.read)
262
+ @pipes[:stdout_writer].close
263
+ end
264
+
265
+ if @redirect_stderr
266
+ $stderr.rewind
267
+ @pipes[:stderr_reader].close
268
+ @pipes[:stderr_writer].write($stderr.read)
269
+ @pipes[:stderr_writer].close
270
+ end
271
+ end
272
+
273
+ # @api private
274
+ def close_pipes_if_needed
275
+ @pipes.each do |key, pipe|
276
+ if !pipe.nil? && !pipe.closed?
277
+ pipe.close
278
+ end
279
+ end
280
+ end
281
+
282
+ # @api private
283
+ def open_pipes_if_needed
284
+ @pipes[:exception_reader], @pipes[:exception_writer] = IO.pipe if @rescue
285
+ @pipes[:stdout_reader] , @pipes[:stdout_writer] = IO.pipe if @redirect_stdout
286
+ @pipes[:stderr_reader] , @pipes[:stderr_writer] = IO.pipe if @redirect_stderr
287
+ end
288
+
289
+ # @api private
290
+ def write_exception(e)
291
+ @pipes[:exception_writer].write(Marshal.dump({ :klass => e.class.to_s ,
292
+ :backtrace => e.backtrace.join("\n"),
293
+ :message => e.message.to_s }) )
294
+ end
295
+
296
+ # @api private
297
+ def pipes_readable?(reader, writer)
298
+ (reader && writer) &&
299
+ (!reader.closed? && !writer.closed?) &&
300
+ (reader.ready?)
301
+ end
302
+
303
+ # @api private
304
+ def redirect(symbol)
305
+ level = $VERBOSE
306
+ $VERBOSE = nil
307
+ if symbol == :stdout
308
+ $stdout = StringIO.new
309
+ Object.const_set(:STDOUT, $stdout)
310
+ else
311
+ $stderr = StringIO.new
312
+ Object.const_set(:STDERR, $stderr)
313
+ end
314
+ $VERBOSE = level
315
+ end
316
+
317
+ end
318
+
319
+ end