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)