Class: Nodo::Core

Inherits:
Object
  • Object
show all
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.



116
117
118
119
120
121
122
# File 'lib/nodo/core.rb', line 116

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.



15
16
17
# File 'lib/nodo/core.rb', line 15

def class_defined
  @class_defined
end

Class Method Details

.class_defined?Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/nodo/core.rb', line 27

def class_defined?
  !!class_defined
end

.clsidObject



31
32
33
# File 'lib/nodo/core.rb', line 31

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

.const(name, value) ⇒ Object



64
65
66
# File 'lib/nodo/core.rb', line 64

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

.function(name, code) ⇒ Object



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

def function(name, code)
  self.functions = functions.merge(name => Function.new(name, code, caller.first))
  define_method(name) { |*args| call_js_method(name, args) }
end

.generate_class_codeObject



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/nodo/core.rb', line 95

def generate_class_code
  <<~JS
    (() => {
      const __nodo_log = nodo.log;
      const __nodo_klass__ = {};
      #{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

.generate_core_codeObject



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/nodo/core.rb', line 72

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

    process.on('SIGINT', shutdown);
    process.on('SIGTERM', shutdown);

    nodo.core.run(socket);
  JS
end

.inherited(subclass) ⇒ Object



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

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

.instanceObject



23
24
25
# File 'lib/nodo/core.rb', line 23

def instance
  @instance ||= new
end

.require(*mods) ⇒ Object



53
54
55
56
57
# File 'lib/nodo/core.rb', line 53

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



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

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

Instance Method Details

#call_js_method(method, args) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/nodo/core.rb', line 176

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}")
  response = client.request(request)
  if response.is_a?(Net::HTTPOK)
    parse_response(response)
  else
    handle_error(response, function)
  end
rescue Errno::EPIPE, IOError
  # TODO: restart or something? If this happens the process is completely broken
  raise Error, 'Node process failed'
end

#clsidObject



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

def clsid
  self.class.clsid
end

#ensure_class_is_definedObject



145
146
147
148
149
# File 'lib/nodo/core.rb', line 145

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



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

def ensure_process_is_spawned
  return if node_pid
  spawn_process
end

#handle_error(response, function) ⇒ Object

Raises:



195
196
197
198
199
200
201
# File 'lib/nodo/core.rb', line 195

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



124
125
126
# File 'lib/nodo/core.rb', line 124

def node_pid
  @@node_pid
end

#parse_response(response) ⇒ Object



203
204
205
# File 'lib/nodo/core.rb', line 203

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

#socket_pathObject



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

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

#spawn_processObject



151
152
153
154
155
156
157
158
159
160
# File 'lib/nodo/core.rb', line 151

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



128
129
130
# File 'lib/nodo/core.rb', line 128

def tmpdir
  @@tmpdir
end

#wait_for_socketObject

Raises:



162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/nodo/core.rb', line 162

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



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

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