require 'weakref' require 'sqreen/runner' # for Sqreen.on_forked_worker? module Sqreen module Js GC_MINI_RACER = 10_000 class MiniRacerAdapter < JsServiceAdapter def initialize(vendored = false) @vendored = vendored self.class.static_init end def preprocess(_rule_name, code) MiniRacerExecutableJs.new(code, @vendored) end def variant_name @vendored ? 'sq_mini_racer' : 'mini_racer' end def self.static_init return if @done_static_init Sqreen::MiniRacer::Platform.set_flags! :noconcurrent_recompilation @done_static_init = true end end # Auxiliary classes class MiniRacerExecutableJs < ExecutableJs @@ctx_defined = false def initialize(source, vendored) @module = vendored ? Sqreen::MiniRacer : MiniRacer @source = source @recycle_runtime_every = GC_MINI_RACER @runtimes = [] @tl_key = "SQREEN_MINI_RACER_CONTEXT_#{object_id}".freeze snapshot if Sqreen.on_forked_worker? # called to eagerly initialize snapshot unless @@ctx_defined self.class.define_sqreen_context(@module) @@ctx_defined = true end end def run_js_cb(cb_name, budget, arguments) mini_racer_context = Thread.current[@tl_key] dead_runtime = !mini_racer_context || !mini_racer_context[:r] || !mini_racer_context[:r].weakref_alive? if !dead_runtime && mini_racer_context[:c] >= @recycle_runtime_every dispose_runtime(mini_racer_context[:r]) dead_runtime = true end if dead_runtime new_runtime = SqreenContext.new(:snapshot => snapshot) push_runtime new_runtime mini_racer_context = { :c => 0, :r => WeakCtx.new(new_runtime), } Thread.current[@tl_key] = mini_racer_context end mini_racer_context[:c] += 1 begin json_args = "[#{arguments.map(&method(:fixup_bad_encoding)).map(&:to_json).join(',')}]" mini_racer_context[:r].eval_unsafe( "#{cb_name}.apply(this, #{json_args})", nil, budget) rescue @module::ScriptTerminatedError nil end end private def fixup_bad_encoding(arg) # NOTE: we don't fix encoding problems in deeper structures return arg unless arg.is_a?(String) unless arg.valid_encoding? return arg.dup.force_encoding(Encoding::ISO_8859_1) end # encoding is valid if it reaches this point return arg if arg.encoding == Encoding::UTF_8 begin arg.encode(Encoding::UTF_8) rescue Encoding::UndefinedConversionError arg.dup.force_encoding(Encoding::ISO_8859_1) end end def snapshot @snapshot ||= @module::Snapshot.new(@source) end def push_runtime(runtime) @runtimes.delete_if do |th, runt, _thid| del = th.nil? || !th.weakref_alive? || !th.alive? runt.dispose if del del end @runtimes.push [WeakRef.new(Thread.current), runtime, Thread.current.object_id] end def dispose_runtime(runtime) @runtimes.delete_if { |_th, _runt, thid| thid == Thread.current.object_id } runtime.dispose end class << self def define_sqreen_context(modoole) # Context specialized for Sqreen usage Sqreen::Js.const_set 'SqreenContext', Class.new(modoole.const_get('Context')) SqreenContext.class_eval do def eval_unsafe(str, filename = nil, timeoutv = nil) # Beware, timeout could be kept in the context # if perf cap is removed after having been activated # As it's unused by execjscb we are not cleaning it return super(str, filename) if timeoutv.nil? return if timeoutv <= 0.0 timeoutv *= 1000 # Timeout are currently expressed in seconds @timeout = timeoutv @eval_thread = Thread.current timeout do super(str, filename) end end end end end end # Weak ref to a context # enables us to skip a method missing call class WeakCtx < WeakRef def initialize(*args) super(*args) end def eval_unsafe(str, filename, timeoutv) __getobj__.eval_unsafe(str, filename, timeoutv) end end end end