Class: Nodo::Core
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_defined ⇒ Object
Returns the value of attribute class_defined.
Class Method Summary collapse
- .class_defined? ⇒ Boolean
- .class_function(*methods) ⇒ Object
- .clsid ⇒ Object
- .const(name, value) ⇒ Object
- .function(name, _code = nil, timeout: 60, code: nil) ⇒ Object
- .generate_class_code ⇒ Object
- .generate_core_code ⇒ Object
- .inherited(subclass) ⇒ Object
- .instance ⇒ Object
- .require(*mods) ⇒ Object
- .script(code) ⇒ Object
Instance Method Summary collapse
- #call_js_method(method, args) ⇒ Object
- #clsid ⇒ Object
- #ensure_class_is_defined ⇒ Object
- #ensure_process_is_spawned ⇒ Object
- #handle_error(response, function) ⇒ Object
-
#initialize ⇒ Core
constructor
A new instance of Core.
- #node_pid ⇒ Object
- #parse_response(response) ⇒ Object
- #socket_path ⇒ Object
- #spawn_process ⇒ Object
- #tmpdir ⇒ Object
- #wait_for_socket ⇒ Object
- #with_tempfile(name) ⇒ Object
Constructor Details
#initialize ⇒ Core
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_defined ⇒ Object
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
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 |
.clsid ⇒ Object
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
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_code ⇒ Object
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_code ⇒ Object
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 |
.instance ⇒ Object
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 |
#clsid ⇒ Object
144 145 146 |
# File 'lib/nodo/core.rb', line 144 def clsid self.class.clsid end |
#ensure_class_is_defined ⇒ Object
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_spawned ⇒ Object
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
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_pid ⇒ Object
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_path ⇒ Object
140 141 142 |
# File 'lib/nodo/core.rb', line 140 def socket_path tmpdir && tmpdir.join(SOCKET_NAME) end |
#spawn_process ⇒ Object
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 |
#tmpdir ⇒ Object
136 137 138 |
# File 'lib/nodo/core.rb', line 136 def tmpdir @@tmpdir end |
#wait_for_socket ⇒ Object
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 |