mini_racer 0.3.0

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