# typed: ignore # Copyright (c) 2015 Sqreen. All Rights Reserved. # Please refer to our terms for more information: https://www.sqreen.com/terms.html # TODO: => Sqreen::JS:MiniRacer # TODO: remove class vars require 'sqreen/log' module Sqreen module Js class MiniRacerExecutableJs < ExecutableJs @@ctx_defined = false # rubocop:disable Style/ClassVars def ctx_defined? @@ctx_defined end def define_ctx! @@ctx_defined = true # rubocop:disable Style/ClassVars end def initialize(pool, code, vendored) @pool = pool @code = code @code_id = self.class.code_id(code) @module = vendored ? Sqreen::MiniRacer : MiniRacer mod = vendored ? Sqreen::MiniRacer : MiniRacer return if ctx_defined? self.class.define_sqreen_context(mod) define_ctx! end def run_js_cb(cb_name, budget, arguments) Sqreen.log.debug { "js:#{self.class} callback:#{cb_name} pool:#{@pool.inspect}" } @pool.with_context do |ctx| Sqreen.log.debug { "js:#{self.class} callback:#{cb_name} context:#{ctx.inspect}" } if ctx.code_failed?(@code_id) Sqreen.log.debug do "Skipping execution of callback #{cb_name} (code md5 #{@code_id})" \ " due to prev failure of definition evaluation" end return nil end ctx.add_code(@code_id, @code) unless ctx.code?(@code_id) # mini_racer expects timeout to be in ms ctx.timeout = budget ? budget * 1000.0 : nil begin ctx.call("sqreen_#{@code_id}_#{cb_name}", *arguments) rescue @module::ScriptTerminatedError Sqreen.log.debug "ScriptTerminatedError/#{cb_name}" nil end end end def self.code_id(code) Digest::MD5.hexdigest(code) 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 attr_accessor :gc_threshold_in_bytes attr_accessor :gc_load attr_writer :timeout def code?(code_id) return false unless @code_ids @code_ids.include?(code_id) end def code_failed?(code_id) return false unless @failed_code_ids @failed_code_ids.include?(code_id) end def add_code(code_id, code) # It's important that the definition is run in its own scope (by executing it inside an anonymous function) # Otherwise some auxiliary functions that the backend server sends will collide the name # Because they're defined with `var`, running the definitions inside a function is enough eval_unsafe "(function() { #{code} })()" transf_global_funcs code_id @code_ids ||= Set.new @code_ids << code_id rescue StandardError @failed_code_ids ||= Set.new @failed_code_ids << code_id raise end 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 def possibly_gc @gc_threshold_in_bytes ||= DEFAULT_GC_THRESHOLD @gc_load ||= 0 # garbage collections max 1 in every 4 calls (avg) if heap_stats[:total_heap_size] > @gc_threshold_in_bytes low_memory_notification @gc_load += 4 else @gc_load = [0, @gc_load - 1].max end end private def transf_global_funcs(code_id) # Multiple callbacks may share the same name. In order to avoid collisions, we rename them here. eval_unsafe <<-JS Object.keys(this).forEach(name => { if (typeof this[name] === "function" && !name.startsWith("sqreen_")) { this['sqreen_#{code_id}_' + name] = this[name]; this[name] = undefined; } }); JS end end end end end end end