lib/nodo/core.rb in nodo-1.6.1 vs lib/nodo/core.rb in nodo-1.6.2

- old
+ new

@@ -1,17 +1,21 @@ module Nodo class Core SOCKET_NAME = 'nodo.sock' - DEFINE_METHOD = '__nodo_define_class__' - TIMEOUT = 5 + DEFINE_METHOD = '__nodo_define_class__'.freeze + EVALUATE_METHOD = '__nodo_evaluate__'.freeze + GC_METHOD = '__nodo_gc__'.freeze + INTERNAL_METHODS = [DEFINE_METHOD, EVALUATE_METHOD, GC_METHOD].freeze + LAUNCH_TIMEOUT = 5 ARRAY_CLASS_ATTRIBUTES = %i[dependencies constants scripts].freeze HASH_CLASS_ATTRIBUTES = %i[functions].freeze CLASS_ATTRIBUTES = (ARRAY_CLASS_ATTRIBUTES + HASH_CLASS_ATTRIBUTES).freeze @@node_pid = nil @@tmpdir = nil @@mutex = Mutex.new + @@exiting = nil class << self extend Forwardable attr_accessor :class_defined @@ -19,19 +23,15 @@ def inherited(subclass) CLASS_ATTRIBUTES.each do |attr| subclass.send "#{attr}=", send(attr).dup end end - + def instance @instance ||= new end - def class_function(*methods) - singleton_class.def_delegators(:instance, *methods) - end - def class_defined? !!class_defined end def clsid @@ -40,10 +40,11 @@ CLASS_ATTRIBUTES.each do |attr| define_method "#{attr}=" do |value| instance_variable_set :"@#{attr}", value end + protected "#{attr}=" end ARRAY_CLASS_ATTRIBUTES.each do |attr| define_method "#{attr}" do instance_variable_get(:"@#{attr}") || instance_variable_set(:"@#{attr}", []) @@ -53,35 +54,11 @@ HASH_CLASS_ATTRIBUTES.each do |attr| define_method "#{attr}" do instance_variable_get(:"@#{attr}") || instance_variable_set(:"@#{attr}", {}) end end - - def require(*mods) - deps = mods.last.is_a?(Hash) ? mods.pop : {} - mods = mods.map { |m| [m, m] }.to_h - self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) } - end - def function(name, _code = nil, timeout: Nodo.timeout, code: nil) - raise ArgumentError, "reserved method name #{name.inspect}" if Nodo::Core.method_defined?(name) || name.to_s == DEFINE_METHOD - code = (code ||= _code).strip - raise ArgumentError, 'function code is required' if '' == code - loc = caller_locations(1, 1)[0] - source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'" - self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout)) - define_method(name) { |*args| call_js_method(name, args) } - end - - def const(name, value) - self.constants = constants + [Constant.new(name, value)] - end - - def script(code) - self.scripts = scripts + [Script.new(code)] - end - def generate_core_code <<~JS global.nodo = require(#{nodo_js}); const socket = process.argv[1]; @@ -104,36 +81,85 @@ end def generate_class_code <<~JS (() => { - const __nodo_log = nodo.log; - const __nodo_klass__ = {}; + const __nodo_klass__ = { nodo: global.nodo }; #{dependencies.map(&:to_js).join} #{constants.map(&:to_js).join} #{functions.values.map(&:to_js).join} #{scripts.map(&:to_js).join} return __nodo_klass__; })() JS end + protected + + def finalize_context(context_id) + proc do + if not @@exiting and core = Nodo::Core.instance + core.send(:call_js_method, GC_METHOD, context_id) + end + end + end + private + def require(*mods) + deps = mods.last.is_a?(Hash) ? mods.pop : {} + mods = mods.map { |m| [m, m] }.to_h + self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) } + end + + def function(name, _code = nil, timeout: Nodo.timeout, code: nil) + raise ArgumentError, "reserved method name #{name.inspect}" if reserved_method_name?(name) + code = (code ||= _code).strip + raise ArgumentError, 'function code is required' if '' == code + loc = caller_locations(1, 1)[0] + source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'" + self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout)) + define_method(name) { |*args| call_js_method(name, args) } + end + + def class_function(*methods) + singleton_class.def_delegators(:instance, *methods) + end + + def const(name, value) + self.constants = constants + [Constant.new(name, value)] + end + + def script(code) + self.scripts = scripts + [Script.new(code)] + end + def nodo_js Pathname.new(__FILE__).dirname.join('nodo.js').to_s.to_json end + + def reserved_method_name?(name) + Nodo::Core.method_defined?(name, false) || Nodo::Core.private_method_defined?(name, false) || name.to_s == DEFINE_METHOD + end end def initialize + raise ClassError, :new if self.class == Nodo::Core @@mutex.synchronize do ensure_process_is_spawned wait_for_socket ensure_class_is_defined end end + def evaluate(code) + ensure_context_is_defined + call_js_method(EVALUATE_METHOD, code) + end + + private + def node_pid @@node_pid end def tmpdir @@ -146,13 +172,19 @@ def clsid self.class.clsid end + def context_defined? + @context_defined + end + def log_exception(e) return unless logger = Nodo.logger - logger.error "\n#{e.class} (#{e.message}):\n\n#{e.backtrace.join("\n")}" + message = "\n#{e.class} (#{e.message})" + message << ":\n\n#{e.backtrace.join("\n")}" if e.backtrace + logger.error message end def ensure_process_is_spawned return if node_pid spawn_process @@ -162,26 +194,36 @@ return if self.class.class_defined? call_js_method(DEFINE_METHOD, self.class.generate_class_code) self.class.class_defined = true end + def ensure_context_is_defined + return if context_defined? + @@mutex.synchronize do + call_js_method(EVALUATE_METHOD, '') + ObjectSpace.define_finalizer(self, self.class.send(:finalize_context, self.object_id)) + @context_defined = true + end + end + def spawn_process @@tmpdir = Pathname.new(Dir.mktmpdir('nodo')) env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s) env['NODO_DEBUG'] = '1' if Nodo.debug @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out) at_exit do + @@exiting = true Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD Process.wait(node_pid) rescue Errno::ECHILD FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir) end end def wait_for_socket start = Time.now socket = nil - while Time.now - start < TIMEOUT + while Time.now - start < LAUNCH_TIMEOUT begin break if socket = UNIXSocket.new(socket_path) rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR Kernel.sleep(0.2) end @@ -190,13 +232,19 @@ raise TimeoutError, "could not connect to socket #{socket_path}" unless socket end def call_js_method(method, args) raise CallError, 'Node process not ready' unless node_pid - raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || method == DEFINE_METHOD + raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || INTERNAL_METHODS.include?(method) function = self.class.functions[method] - raise NameError, "undefined function `#{method}' for #{self.class}" unless function || method == DEFINE_METHOD - request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json') + raise NameError, "undefined function `#{method}' for #{self.class}" unless function || INTERNAL_METHODS.include?(method) + context_id = case method + when DEFINE_METHOD then 0 + when GC_METHOD then args.first + else + object_id + end + request = Net::HTTP::Post.new("/#{clsid}/#{context_id}/#{method}", 'Content-Type': 'application/json') request.body = JSON.dump(args) client = Client.new("unix://#{socket_path}") client.read_timeout = function.timeout if function response = client.request(request) if response.is_a?(Net::HTTPOK)