dia 1.5 → 2.0.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.
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