Class: Nodo::Core

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/nodo/core.rb

Constant Summary collapse

SOCKET_NAME =
'nodo.sock'
DEFINE_METHOD =
'__nodo_define_class__'
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

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCore

Returns a new instance of Core.



124
125
126
127
128
129
130
# File 'lib/nodo/core.rb', line 124

def initialize
  @@mutex.synchronize do
    ensure_process_is_spawned
    wait_for_socket
    ensure_class_is_defined
  end
end

Class Attribute Details

.class_definedObject

Returns the value of attribute class_defined.



17
18
19
# File 'lib/nodo/core.rb', line 17

def class_defined
  @class_defined
end

Class Method Details

.class_defined?Boolean

Returns:

  • (Boolean)


33
34
35
# File 'lib/nodo/core.rb', line 33

def class_defined?
  !!class_defined
end

.class_function(*methods) ⇒ Object



29
30
31
# File 'lib/nodo/core.rb', line 29

def class_function(*methods)
  singleton_class.def_delegators(:instance, *methods)
end

.clsidObject



37
38
39
# File 'lib/nodo/core.rb', line 37

def clsid
  name || "Class:0x#{object_id.to_s(0x10)}"
end

.const(name, value) ⇒ Object



72
73
74
# File 'lib/nodo/core.rb', line 72

def const(name, value)
  self.constants = constants + [Constant.new(name, value)]
end

.function(name, _code = nil, timeout: 60, code: nil) ⇒ Object

Raises:

  • (ArgumentError)


65
66
67
68
69
70
# File 'lib/nodo/core.rb', line 65

def function(name, _code = nil, timeout: 60, code: nil)
  code = (code ||= _code).strip
  raise ArgumentError, 'function code is required' if '' == code
  self.functions = functions.merge(name => Function.new(name, _code || code, caller.first, timeout))
  define_method(name) { |*args| call_js_method(name, args) }
end

.generate_class_codeObject



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/nodo/core.rb', line 103

def generate_class_code
  "    (() => {\n      const __nodo_log = nodo.log;\n      const __nodo_klass__ = {};\n      \#{dependencies.map(&:to_js).join}\n      \#{constants.map(&:to_js).join}\n      \#{functions.values.map(&:to_js).join}\n      \#{scripts.map(&:to_js).join}\n      return __nodo_klass__;\n    })()\n  JS\nend\n"

.generate_core_codeObject



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/nodo/core.rb', line 80

def generate_core_code
  "    global.nodo = require(\#{nodo_js});\n    \n    const socket = process.argv[1];\n    if (!socket) {\n      process.stderr.write('Socket path is required\\\\n');\n      process.exit(1);\n    }\n    \n    process.title = `nodo-core ${socket}`;\n    \n    const shutdown = () => {\n      nodo.core.close(() => { process.exit(0) });\n    };\n\n    process.on('SIGINT', shutdown);\n    process.on('SIGTERM', shutdown);\n\n    nodo.core.run(socket);\n  JS\nend\n"

.inherited(subclass) ⇒ Object



19
20
21
22
23
# File 'lib/nodo/core.rb', line 19

def inherited(subclass)
  CLASS_ATTRIBUTES.each do |attr|
    subclass.send "#{attr}=", send(attr).dup
  end
end

.instanceObject



25
26
27
# File 'lib/nodo/core.rb', line 25

def instance
  @instance ||= new
end

.require(*mods) ⇒ Object



59
60
61
62
63
# File 'lib/nodo/core.rb', line 59

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

.script(code) ⇒ Object



76
77
78
# File 'lib/nodo/core.rb', line 76

def script(code)
  self.scripts = scripts + [Script.new(code)]
end

Instance Method Details

#call_js_method(method, args) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/nodo/core.rb', line 184

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
  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')
  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)
    parse_response(response)
  else
    handle_error(response, function)
  end
rescue Net::ReadTimeout
  raise TimeoutError, "function call #{self.class}##{method} timed out"
rescue Errno::EPIPE, IOError
  # TODO: restart or something? If this happens the process is completely broken
  raise Error, 'Node process failed'
end

#clsidObject



144
145
146
# File 'lib/nodo/core.rb', line 144

def clsid
  self.class.clsid
end

#ensure_class_is_definedObject



153
154
155
156
157
# File 'lib/nodo/core.rb', line 153

def ensure_class_is_defined
  return if self.class.class_defined?
  call_js_method(DEFINE_METHOD, self.class.generate_class_code)
  self.class.class_defined = true
end

#ensure_process_is_spawnedObject



148
149
150
151
# File 'lib/nodo/core.rb', line 148

def ensure_process_is_spawned
  return if node_pid
  spawn_process
end

#handle_error(response, function) ⇒ Object

Raises:



206
207
208
209
210
211
212
# File 'lib/nodo/core.rb', line 206

def handle_error(response, function)
  if response.body
    result = parse_response(response)
    raise JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
  end
  raise CallError, "Node returned #{response.code}"
end

#node_pidObject



132
133
134
# File 'lib/nodo/core.rb', line 132

def node_pid
  @@node_pid
end

#parse_response(response) ⇒ Object



214
215
216
217
# File 'lib/nodo/core.rb', line 214

def parse_response(response)
  data = response.body.force_encoding('UTF-8')
  JSON.parse(data) unless data == ''
end

#socket_pathObject



140
141
142
# File 'lib/nodo/core.rb', line 140

def socket_path
  tmpdir && tmpdir.join(SOCKET_NAME)
end

#spawn_processObject



159
160
161
162
163
164
165
166
167
168
# File 'lib/nodo/core.rb', line 159

def spawn_process
  @@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
  env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
  @@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
  at_exit do
    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

#tmpdirObject



136
137
138
# File 'lib/nodo/core.rb', line 136

def tmpdir
  @@tmpdir
end

#wait_for_socketObject

Raises:



170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/nodo/core.rb', line 170

def wait_for_socket
  start = Time.now
  socket = nil
  while Time.now - start < TIMEOUT
    begin
      break if socket = UNIXSocket.new(socket_path)
    rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
      sleep 0.2
    end
  end
  socket.close if socket
  raise TimeoutError, "could not connect to socket #{socket_path}" unless socket
end

#with_tempfile(name) ⇒ Object



219
220
221
222
223
224
225
226
# File 'lib/nodo/core.rb', line 219

def with_tempfile(name)
  ext = File.extname(name)
  result = nil
  Tempfile.create([File.basename(name, ext), ext], tmpdir) do |file|
    result = yield(file)
  end
  result
end