mini_racer 0.3.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,434 @@
1
+ require "mini_racer/version"
2
+ require "mini_racer_extension"
3
+ require "thread"
4
+ require "json"
5
+
6
+ module MiniRacer
7
+
8
+ class Error < ::StandardError; end
9
+
10
+ class ContextDisposedError < Error; end
11
+ class SnapshotError < Error; end
12
+ class PlatformAlreadyInitialized < Error; end
13
+
14
+ class EvalError < Error; end
15
+ class ParseError < EvalError; end
16
+ class ScriptTerminatedError < EvalError; end
17
+ class V8OutOfMemoryError < EvalError; end
18
+
19
+ class FailedV8Conversion
20
+ attr_reader :info
21
+ def initialize(info)
22
+ @info = info
23
+ end
24
+ end
25
+
26
+ class RuntimeError < EvalError
27
+ def initialize(message)
28
+ message, js_backtrace = message.split("\n", 2)
29
+ if js_backtrace && !js_backtrace.empty?
30
+ @js_backtrace = js_backtrace.split("\n")
31
+ @js_backtrace.map!{|f| "JavaScript #{f.strip}"}
32
+ else
33
+ @js_backtrace = nil
34
+ end
35
+ super(message)
36
+ end
37
+
38
+ def backtrace
39
+ val = super
40
+ return unless val
41
+ if @js_backtrace
42
+ @js_backtrace + val
43
+ else
44
+ val
45
+ end
46
+ end
47
+ end
48
+
49
+ # helper class returned when we have a JavaScript function
50
+ class JavaScriptFunction
51
+ def to_s
52
+ "JavaScript Function"
53
+ end
54
+ end
55
+
56
+ class Isolate
57
+ def initialize(snapshot = nil)
58
+ unless snapshot.nil? || snapshot.is_a?(Snapshot)
59
+ raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}"
60
+ end
61
+
62
+ # defined in the C class
63
+ init_with_snapshot(snapshot)
64
+ end
65
+ end
66
+
67
+ class Platform
68
+ class << self
69
+ def set_flags!(*args, **kwargs)
70
+ flags_to_strings([args, kwargs]).each do |flag|
71
+ # defined in the C class
72
+ set_flag_as_str!(flag)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def flags_to_strings(flags)
79
+ flags.flatten.map { |flag| flag_to_string(flag) }.flatten
80
+ end
81
+
82
+ # normalize flags to strings, and adds leading dashes if needed
83
+ def flag_to_string(flag)
84
+ if flag.is_a?(Hash)
85
+ flag.map do |key, value|
86
+ "#{flag_to_string(key)} #{value}"
87
+ end
88
+ else
89
+ str = flag.to_s
90
+ str = "--#{str}" unless str.start_with?('--')
91
+ str
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ # eval is defined in the C class
98
+ class Context
99
+
100
+ class ExternalFunction
101
+ def initialize(name, callback, parent)
102
+ unless String === name
103
+ raise ArgumentError, "parent_object must be a String"
104
+ end
105
+ parent_object, _ , @name = name.rpartition(".")
106
+ @callback = callback
107
+ @parent = parent
108
+ @parent_object_eval = nil
109
+ @parent_object = nil
110
+
111
+ unless parent_object.empty?
112
+ @parent_object = parent_object
113
+
114
+ @parent_object_eval = ""
115
+ prev = ""
116
+ first = true
117
+ parent_object.split(".").each do |obj|
118
+ prev << obj
119
+ if first
120
+ @parent_object_eval << "if (typeof #{prev} === 'undefined') { #{prev} = {} };\n"
121
+ else
122
+ @parent_object_eval << "#{prev} = #{prev} || {};\n"
123
+ end
124
+ prev << "."
125
+ first = false
126
+ end
127
+ @parent_object_eval << "#{parent_object};"
128
+ end
129
+ notify_v8
130
+ end
131
+ end
132
+
133
+ def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil)
134
+ options ||= {}
135
+
136
+ check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout)
137
+
138
+ @functions = {}
139
+ @timeout = nil
140
+ @max_memory = nil
141
+ @current_exception = nil
142
+ @timeout = timeout
143
+ @max_memory = max_memory
144
+
145
+ # false signals it should be fetched if requested
146
+ @isolate = isolate || false
147
+
148
+ @ensure_gc_after_idle = ensure_gc_after_idle
149
+
150
+ if @ensure_gc_after_idle
151
+ @last_eval = nil
152
+ @ensure_gc_thread = nil
153
+ @ensure_gc_mutex = Mutex.new
154
+ end
155
+
156
+ @disposed = false
157
+
158
+ @callback_mutex = Mutex.new
159
+ @callback_running = false
160
+ @thread_raise_called = false
161
+ @eval_thread = nil
162
+
163
+ # defined in the C class
164
+ init_unsafe(isolate, snapshot)
165
+ end
166
+
167
+ def isolate
168
+ return @isolate if @isolate != false
169
+ # defined in the C class
170
+ @isolate = create_isolate_value
171
+ end
172
+
173
+ def load(filename)
174
+ # TODO do this native cause no need to allocate VALUE here
175
+ eval(File.read(filename))
176
+ end
177
+
178
+ def write_heap_snapshot(file_or_io)
179
+ f = nil
180
+ implicit = false
181
+
182
+
183
+ if String === file_or_io
184
+ f = File.open(file_or_io, "w")
185
+ implicit = true
186
+ else
187
+ f = file_or_io
188
+ end
189
+
190
+ if !(File === f)
191
+ raise ArgumentError("file_or_io")
192
+ end
193
+
194
+ write_heap_snapshot_unsafe(f)
195
+
196
+ ensure
197
+ f.close if implicit
198
+ end
199
+
200
+ def eval(str, options=nil)
201
+ raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
202
+
203
+ filename = options && options[:filename].to_s
204
+
205
+ @eval_thread = Thread.current
206
+ isolate_mutex.synchronize do
207
+ @current_exception = nil
208
+ timeout do
209
+ eval_unsafe(str, filename)
210
+ end
211
+ end
212
+ ensure
213
+ @eval_thread = nil
214
+ ensure_gc_thread if @ensure_gc_after_idle
215
+ end
216
+
217
+ def call(function_name, *arguments)
218
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
219
+
220
+ @eval_thread = Thread.current
221
+ isolate_mutex.synchronize do
222
+ timeout do
223
+ call_unsafe(function_name, *arguments)
224
+ end
225
+ end
226
+ ensure
227
+ @eval_thread = nil
228
+ ensure_gc_thread if @ensure_gc_after_idle
229
+ end
230
+
231
+ def dispose
232
+ return if @disposed
233
+ isolate_mutex.synchronize do
234
+ return if @disposed
235
+ dispose_unsafe
236
+ @disposed = true
237
+ @isolate = nil # allow it to be garbage collected, if set
238
+ end
239
+ end
240
+
241
+
242
+ def attach(name, callback)
243
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
244
+
245
+ wrapped = lambda do |*args|
246
+ begin
247
+
248
+ r = nil
249
+
250
+ begin
251
+ @callback_mutex.synchronize{
252
+ @callback_running = true
253
+ }
254
+ r = callback.call(*args)
255
+ ensure
256
+ @callback_mutex.synchronize{
257
+ @callback_running = false
258
+ }
259
+ end
260
+
261
+ # wait up to 2 seconds for this to be interrupted
262
+ # will very rarely be called cause #raise is called
263
+ # in another mutex
264
+ @callback_mutex.synchronize {
265
+ if @thread_raise_called
266
+ sleep 2
267
+ end
268
+ }
269
+
270
+ r
271
+
272
+ ensure
273
+ @callback_mutex.synchronize {
274
+ @thread_raise_called = false
275
+ }
276
+ end
277
+ end
278
+
279
+ isolate_mutex.synchronize do
280
+ external = ExternalFunction.new(name, wrapped, self)
281
+ @functions["#{name}"] = external
282
+ end
283
+ end
284
+
285
+ private
286
+
287
+ def ensure_gc_thread
288
+ @last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
289
+ @ensure_gc_mutex.synchronize do
290
+ @ensure_gc_thread = nil if !@ensure_gc_thread&.alive?
291
+ @ensure_gc_thread ||= Thread.new do
292
+ ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0
293
+ done = false
294
+ while !done
295
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
296
+
297
+ if @disposed
298
+ @ensure_gc_thread = nil
299
+ break
300
+ end
301
+
302
+ if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval
303
+ @ensure_gc_mutex.synchronize do
304
+ isolate_mutex.synchronize do
305
+ if !@eval_thread
306
+ isolate.low_memory_notification if !@disposed
307
+ @ensure_gc_thread = nil
308
+ done = true
309
+ end
310
+ end
311
+ end
312
+ end
313
+ sleep ensure_gc_after_idle_seconds if !done
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ def stop_attached
320
+ @callback_mutex.synchronize{
321
+ if @callback_running
322
+ @eval_thread.raise ScriptTerminatedError, "Terminated during callback"
323
+ @thread_raise_called = true
324
+ end
325
+ }
326
+ end
327
+
328
+ def timeout(&blk)
329
+ return blk.call unless @timeout
330
+
331
+ mutex = Mutex.new
332
+ done = false
333
+
334
+ rp,wp = IO.pipe
335
+
336
+ t = Thread.new do
337
+ begin
338
+ result = IO.select([rp],[],[],(@timeout/1000.0))
339
+ if !result
340
+ mutex.synchronize do
341
+ stop unless done
342
+ end
343
+ end
344
+ rescue => e
345
+ STDERR.puts e
346
+ STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT"
347
+ end
348
+ end
349
+
350
+ rval = blk.call
351
+ mutex.synchronize do
352
+ done = true
353
+ end
354
+
355
+ wp.write("done")
356
+
357
+ # ensure we do not leak a thread in state
358
+ t.join
359
+ t = nil
360
+
361
+ rval
362
+ ensure
363
+ # exceptions need to be handled
364
+ if t && wp
365
+ wp.write("done")
366
+ t.join
367
+ end
368
+ wp.close if wp
369
+ rp.close if rp
370
+ end
371
+
372
+ def check_init_options!(isolate:, snapshot:, max_memory:, ensure_gc_after_idle:, timeout:)
373
+ assert_option_is_nil_or_a('isolate', isolate, Isolate)
374
+ assert_option_is_nil_or_a('snapshot', snapshot, Snapshot)
375
+
376
+ assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000)
377
+ assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1)
378
+ assert_numeric_or_nil('timeout', timeout, min_value: 1)
379
+
380
+ if isolate && snapshot
381
+ raise ArgumentError, 'can only pass one of isolate and snapshot options'
382
+ end
383
+ end
384
+
385
+ def assert_numeric_or_nil(option_name, object, min_value:)
386
+ if object.is_a?(Numeric) && object < min_value
387
+ raise ArgumentError, "#{option_name} must be larger than #{min_value}"
388
+ end
389
+
390
+ if !object.nil? && !object.is_a?(Numeric)
391
+ raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}"
392
+ end
393
+ end
394
+
395
+ def assert_option_is_nil_or_a(option_name, object, klass)
396
+ unless object.nil? || object.is_a?(klass)
397
+ raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}"
398
+ end
399
+ end
400
+ end
401
+
402
+ # `size` and `warmup!` public methods are defined in the C class
403
+ class Snapshot
404
+ def initialize(str = '')
405
+ # ensure it first can load
406
+ begin
407
+ ctx = MiniRacer::Context.new
408
+ ctx.eval(str)
409
+ rescue MiniRacer::RuntimeError => e
410
+ raise MiniRacer::SnapshotError.new, e.message, e.backtrace
411
+ end
412
+
413
+ @source = str
414
+
415
+ # defined in the C class
416
+ load(str)
417
+ end
418
+
419
+ def warmup!(src)
420
+ # we have to do something here
421
+ # we are bloating memory a bit but it is more correct
422
+ # than hitting an exception when attempty to compile invalid source
423
+ begin
424
+ ctx = MiniRacer::Context.new
425
+ ctx.eval(@source)
426
+ ctx.eval(src)
427
+ rescue MiniRacer::RuntimeError => e
428
+ raise MiniRacer::SnapshotError.new, e.message, e.backtrace
429
+ end
430
+
431
+ warmup_unsafe!(src)
432
+ end
433
+ end
434
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniRacer
4
+ VERSION = "0.3.0"
5
+ end
@@ -0,0 +1,41 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mini_racer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mini_racer"
8
+ spec.version = MiniRacer::VERSION
9
+ spec.authors = ["Sam Saffron"]
10
+ spec.email = ["sam.saffron@gmail.com"]
11
+
12
+ spec.summary = %q{Minimal embedded v8 for Ruby}
13
+ spec.description = %q{Minimal embedded v8 engine for Ruby}
14
+ spec.homepage = "https://github.com/discourse/mini_racer"
15
+ spec.license = "MIT"
16
+
17
+ spec.metadata = {
18
+ "bug_tracker_uri" => "https://github.com/discourse/mini_racer/issues",
19
+ "changelog_uri" => "https://github.com/discourse/mini_racer/blob/v#{spec.version}/CHANGELOG",
20
+ "documentation_uri" => "https://www.rubydoc.info/gems/mini_racer/#{spec.version}",
21
+ "source_code_uri" => "https://github.com/discourse/mini_racer/tree/v#{spec.version}",
22
+ }
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(benchmark|test|spec|features|examples)/}) }
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler"
30
+ spec.add_development_dependency "rake", ">= 12.3.3"
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+ spec.add_development_dependency "rake-compiler"
33
+ spec.add_development_dependency "m"
34
+
35
+ spec.add_dependency 'libv8', '> 8.4'
36
+ spec.require_paths = ["lib", "ext"]
37
+
38
+ spec.extensions = ["ext/mini_racer_extension/extconf.rb"]
39
+
40
+ spec.required_ruby_version = '>= 2.3'
41
+ end