mini_racer 0.17.0.pre5 → 0.17.0.pre7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,380 @@
1
+ # This code used to be shared in lib/mini_racer.rb
2
+ # but was moved to the extension with https://github.com/rubyjs/mini_racer/pull/325.
3
+ # So now this is effectively duplicate logic with C/C++ code.
4
+ # Maybe one day it can be actually shared again between both backends.
5
+
6
+ module MiniRacer
7
+
8
+ MARSHAL_STACKDEPTH_DEFAULT = 2**9-2
9
+ MARSHAL_STACKDEPTH_MAX_VALUE = 2**10-2
10
+
11
+ class FailedV8Conversion
12
+ attr_reader :info
13
+ def initialize(info)
14
+ @info = info
15
+ end
16
+ end
17
+
18
+ # helper class returned when we have a JavaScript function
19
+ class JavaScriptFunction
20
+ def to_s
21
+ "JavaScript Function"
22
+ end
23
+ end
24
+
25
+ class Isolate
26
+ def initialize(snapshot = nil)
27
+ unless snapshot.nil? || snapshot.is_a?(Snapshot)
28
+ raise ArgumentError, "snapshot must be a Snapshot object, passed a #{snapshot.inspect}"
29
+ end
30
+
31
+ # defined in the C class
32
+ init_with_snapshot(snapshot)
33
+ end
34
+ end
35
+
36
+ class Platform
37
+ class << self
38
+ def set_flags!(*args, **kwargs)
39
+ flags_to_strings([args, kwargs]).each do |flag|
40
+ # defined in the C class
41
+ set_flag_as_str!(flag)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def flags_to_strings(flags)
48
+ flags.flatten.map { |flag| flag_to_string(flag) }.flatten
49
+ end
50
+
51
+ # normalize flags to strings, and adds leading dashes if needed
52
+ def flag_to_string(flag)
53
+ if flag.is_a?(Hash)
54
+ flag.map do |key, value|
55
+ "#{flag_to_string(key)} #{value}"
56
+ end
57
+ else
58
+ str = flag.to_s
59
+ str = "--#{str}" unless str.start_with?('--')
60
+ str
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # eval is defined in the C class
67
+ class Context
68
+
69
+ class ExternalFunction
70
+ def initialize(name, callback, parent)
71
+ unless String === name
72
+ raise ArgumentError, "parent_object must be a String"
73
+ end
74
+ parent_object, _ , @name = name.rpartition(".")
75
+ @callback = callback
76
+ @parent = parent
77
+ @parent_object_eval = nil
78
+ @parent_object = nil
79
+
80
+ unless parent_object.empty?
81
+ @parent_object = parent_object
82
+
83
+ @parent_object_eval = ""
84
+ prev = ""
85
+ first = true
86
+ parent_object.split(".").each do |obj|
87
+ prev << obj
88
+ if first
89
+ @parent_object_eval << "if (typeof #{prev} !== 'object' || typeof #{prev} !== 'function') { #{prev} = {} };\n"
90
+ else
91
+ @parent_object_eval << "#{prev} = #{prev} || {};\n"
92
+ end
93
+ prev << "."
94
+ first = false
95
+ end
96
+ @parent_object_eval << "#{parent_object};"
97
+ end
98
+ notify_v8
99
+ end
100
+ end
101
+
102
+ def initialize(max_memory: nil, timeout: nil, isolate: nil, ensure_gc_after_idle: nil, snapshot: nil, marshal_stack_depth: nil)
103
+ options ||= {}
104
+
105
+ check_init_options!(isolate: isolate, snapshot: snapshot, max_memory: max_memory, marshal_stack_depth: marshal_stack_depth, ensure_gc_after_idle: ensure_gc_after_idle, timeout: timeout)
106
+
107
+ @functions = {}
108
+ @timeout = nil
109
+ @max_memory = nil
110
+ @current_exception = nil
111
+ @timeout = timeout
112
+ @max_memory = max_memory
113
+ @marshal_stack_depth = marshal_stack_depth
114
+
115
+ # false signals it should be fetched if requested
116
+ @isolate = isolate || false
117
+
118
+ @ensure_gc_after_idle = ensure_gc_after_idle
119
+
120
+ if @ensure_gc_after_idle
121
+ @last_eval = nil
122
+ @ensure_gc_thread = nil
123
+ @ensure_gc_mutex = Mutex.new
124
+ end
125
+
126
+ @disposed = false
127
+
128
+ @callback_mutex = Mutex.new
129
+ @callback_running = false
130
+ @thread_raise_called = false
131
+ @eval_thread = nil
132
+
133
+ # defined in the C class
134
+ init_unsafe(isolate, snapshot)
135
+ end
136
+
137
+ def isolate
138
+ return @isolate if @isolate != false
139
+ # defined in the C class
140
+ @isolate = create_isolate_value
141
+ end
142
+
143
+ def eval(str, options=nil)
144
+ raise(ContextDisposedError, 'attempted to call eval on a disposed context!') if @disposed
145
+
146
+ filename = options && options[:filename].to_s
147
+
148
+ @eval_thread = Thread.current
149
+ isolate_mutex.synchronize do
150
+ @current_exception = nil
151
+ timeout do
152
+ eval_unsafe(str, filename)
153
+ end
154
+ end
155
+ ensure
156
+ @eval_thread = nil
157
+ ensure_gc_thread if @ensure_gc_after_idle
158
+ end
159
+
160
+ def call(function_name, *arguments)
161
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
162
+
163
+ @eval_thread = Thread.current
164
+ isolate_mutex.synchronize do
165
+ timeout do
166
+ call_unsafe(function_name, *arguments)
167
+ end
168
+ end
169
+ ensure
170
+ @eval_thread = nil
171
+ ensure_gc_thread if @ensure_gc_after_idle
172
+ end
173
+
174
+ def dispose
175
+ return if @disposed
176
+ isolate_mutex.synchronize do
177
+ return if @disposed
178
+ dispose_unsafe
179
+ @disposed = true
180
+ @isolate = nil # allow it to be garbage collected, if set
181
+ end
182
+ end
183
+
184
+
185
+ def attach(name, callback)
186
+ raise(ContextDisposedError, 'attempted to call function on a disposed context!') if @disposed
187
+
188
+ wrapped = lambda do |*args|
189
+ begin
190
+
191
+ r = nil
192
+
193
+ begin
194
+ @callback_mutex.synchronize{
195
+ @callback_running = true
196
+ }
197
+ r = callback.call(*args)
198
+ ensure
199
+ @callback_mutex.synchronize{
200
+ @callback_running = false
201
+ }
202
+ end
203
+
204
+ # wait up to 2 seconds for this to be interrupted
205
+ # will very rarely be called cause #raise is called
206
+ # in another mutex
207
+ @callback_mutex.synchronize {
208
+ if @thread_raise_called
209
+ sleep 2
210
+ end
211
+ }
212
+
213
+ r
214
+
215
+ ensure
216
+ @callback_mutex.synchronize {
217
+ @thread_raise_called = false
218
+ }
219
+ end
220
+ end
221
+
222
+ isolate_mutex.synchronize do
223
+ external = ExternalFunction.new(name, wrapped, self)
224
+ @functions["#{name}"] = external
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ def ensure_gc_thread
231
+ @last_eval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
232
+ @ensure_gc_mutex.synchronize do
233
+ @ensure_gc_thread = nil if !@ensure_gc_thread&.alive?
234
+ return if !Thread.main.alive? # avoid "can't alloc thread" exception
235
+ @ensure_gc_thread ||= Thread.new do
236
+ ensure_gc_after_idle_seconds = @ensure_gc_after_idle / 1000.0
237
+ done = false
238
+ while !done
239
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
240
+
241
+ if @disposed
242
+ @ensure_gc_thread = nil
243
+ break
244
+ end
245
+
246
+ if !@eval_thread && ensure_gc_after_idle_seconds < now - @last_eval
247
+ @ensure_gc_mutex.synchronize do
248
+ isolate_mutex.synchronize do
249
+ if !@eval_thread
250
+ low_memory_notification if !@disposed
251
+ @ensure_gc_thread = nil
252
+ done = true
253
+ end
254
+ end
255
+ end
256
+ end
257
+ sleep ensure_gc_after_idle_seconds if !done
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ def stop_attached
264
+ @callback_mutex.synchronize{
265
+ if @callback_running
266
+ @eval_thread.raise ScriptTerminatedError, "Terminated during callback"
267
+ @thread_raise_called = true
268
+ end
269
+ }
270
+ end
271
+
272
+ def timeout(&blk)
273
+ return blk.call unless @timeout
274
+
275
+ mutex = Mutex.new
276
+ done = false
277
+
278
+ rp,wp = IO.pipe
279
+
280
+ t = Thread.new do
281
+ begin
282
+ result = rp.wait_readable(@timeout/1000.0)
283
+ if !result
284
+ mutex.synchronize do
285
+ stop unless done
286
+ end
287
+ end
288
+ rescue => e
289
+ STDERR.puts e
290
+ STDERR.puts "FAILED TO TERMINATE DUE TO TIMEOUT"
291
+ end
292
+ end
293
+
294
+ rval = blk.call
295
+ mutex.synchronize do
296
+ done = true
297
+ end
298
+
299
+ wp.close
300
+
301
+ # ensure we do not leak a thread in state
302
+ t.join
303
+ t = nil
304
+
305
+ rval
306
+ ensure
307
+ # exceptions need to be handled
308
+ wp&.close
309
+ t&.join
310
+ rp&.close
311
+ end
312
+
313
+ def check_init_options!(isolate:, snapshot:, max_memory:, marshal_stack_depth:, ensure_gc_after_idle:, timeout:)
314
+ assert_option_is_nil_or_a('isolate', isolate, Isolate)
315
+ assert_option_is_nil_or_a('snapshot', snapshot, Snapshot)
316
+
317
+ assert_numeric_or_nil('max_memory', max_memory, min_value: 10_000, max_value: 2**32-1)
318
+ assert_numeric_or_nil('marshal_stack_depth', marshal_stack_depth, min_value: 1, max_value: MARSHAL_STACKDEPTH_MAX_VALUE)
319
+ assert_numeric_or_nil('ensure_gc_after_idle', ensure_gc_after_idle, min_value: 1)
320
+ assert_numeric_or_nil('timeout', timeout, min_value: 1)
321
+
322
+ if isolate && snapshot
323
+ raise ArgumentError, 'can only pass one of isolate and snapshot options'
324
+ end
325
+ end
326
+
327
+ def assert_numeric_or_nil(option_name, object, min_value:, max_value: nil)
328
+ if max_value && object.is_a?(Numeric) && object > max_value
329
+ raise ArgumentError, "#{option_name} must be less than or equal to #{max_value}"
330
+ end
331
+
332
+ if object.is_a?(Numeric) && object < min_value
333
+ raise ArgumentError, "#{option_name} must be larger than or equal to #{min_value}"
334
+ end
335
+
336
+ if !object.nil? && !object.is_a?(Numeric)
337
+ raise ArgumentError, "#{option_name} must be a number, passed a #{object.inspect}"
338
+ end
339
+ end
340
+
341
+ def assert_option_is_nil_or_a(option_name, object, klass)
342
+ unless object.nil? || object.is_a?(klass)
343
+ raise ArgumentError, "#{option_name} must be a #{klass} object, passed a #{object.inspect}"
344
+ end
345
+ end
346
+ end
347
+
348
+ # `size` and `warmup!` public methods are defined in the C class
349
+ class Snapshot
350
+ def initialize(str = '')
351
+ # ensure it first can load
352
+ begin
353
+ ctx = MiniRacer::Context.new
354
+ ctx.eval(str)
355
+ rescue MiniRacer::RuntimeError => e
356
+ raise MiniRacer::SnapshotError, e.message, e.backtrace
357
+ end
358
+
359
+ @source = str
360
+
361
+ # defined in the C class
362
+ load(str)
363
+ end
364
+
365
+ def warmup!(src)
366
+ # we have to do something here
367
+ # we are bloating memory a bit but it is more correct
368
+ # than hitting an exception when attempty to compile invalid source
369
+ begin
370
+ ctx = MiniRacer::Context.new
371
+ ctx.eval(@source)
372
+ ctx.eval(src)
373
+ rescue MiniRacer::RuntimeError => e
374
+ raise MiniRacer::SnapshotError, e.message, e.backtrace
375
+ end
376
+
377
+ warmup_unsafe!(src)
378
+ end
379
+ end
380
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'shared'
4
+
3
5
  module MiniRacer
4
6
 
5
7
  class Context
@@ -43,6 +45,7 @@ module MiniRacer
43
45
  end
44
46
 
45
47
  def heap_stats
48
+ raise ContextDisposedError if @disposed
46
49
  {
47
50
  total_physical_size: 0,
48
51
  total_heap_size_executable: 0,
@@ -60,6 +63,14 @@ module MiniRacer
60
63
  end
61
64
  end
62
65
 
66
+ def low_memory_notification
67
+ GC.start
68
+ end
69
+
70
+ def idle_notification(idle_time)
71
+ true
72
+ end
73
+
63
74
  private
64
75
 
65
76
  @context_initialized = false
@@ -95,9 +106,11 @@ module MiniRacer
95
106
  else
96
107
  @snapshot = nil
97
108
  end
98
- @is_object_or_array_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE
109
+ @is_object_or_array_func, @is_map_func, @is_map_iterator_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func = eval_in_context <<-CODE
99
110
  [
100
111
  (x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
112
+ (x) => { return x instanceof Map },
113
+ (x) => { return x[Symbol.toStringTag] === 'Map Iterator' },
101
114
  (x) => { return x instanceof Date },
102
115
  (x) => { return x.getTime(x) },
103
116
  (x) => { return typeof x === 'symbol' },
@@ -233,6 +246,10 @@ module MiniRacer
233
246
  js_date_to_time(value)
234
247
  elsif symbol?(value)
235
248
  js_symbol_to_symbol(value)
249
+ elsif map?(value)
250
+ js_map_to_hash(value)
251
+ elsif map_iterator?(value)
252
+ value.map { |e| convert_js_to_ruby(e) }
236
253
  else
237
254
  object = value
238
255
  h = {}
@@ -251,6 +268,14 @@ module MiniRacer
251
268
  @is_object_or_array_func.call(val)
252
269
  end
253
270
 
271
+ def map?(value)
272
+ @is_map_func.call(value)
273
+ end
274
+
275
+ def map_iterator?(value)
276
+ @is_map_iterator_func.call(value)
277
+ end
278
+
254
279
  def time?(value)
255
280
  @is_time_func.call(value)
256
281
  end
@@ -268,6 +293,12 @@ module MiniRacer
268
293
  @js_symbol_to_symbol_func.call(value).to_s.to_sym
269
294
  end
270
295
 
296
+ def js_map_to_hash(map)
297
+ map.to_a.to_h do |key, value|
298
+ [convert_js_to_ruby(key), convert_js_to_ruby(value)]
299
+ end
300
+ end
301
+
271
302
  def js_new_date(value)
272
303
  @js_new_date_func.call(value)
273
304
  end
@@ -320,14 +351,6 @@ module MiniRacer
320
351
  # However, isolate can hold a snapshot, and code and ASTs are shared between contexts.
321
352
  @snapshot = snapshot
322
353
  end
323
-
324
- def low_memory_notification
325
- GC.start
326
- end
327
-
328
- def idle_notification(idle_time)
329
- true
330
- end
331
354
  end
332
355
 
333
356
  class Platform
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.17.0.pre5"
4
+ VERSION = "0.17.0.pre7"
5
5
  LIBV8_NODE_VERSION = "~> 22.7.0.4"
6
6
  end