nodo 1.6.1 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +57 -2
- data/lib/nodo/constant.rb +1 -1
- data/lib/nodo/core.rb +86 -38
- data/lib/nodo/dependency.rb +1 -1
- data/lib/nodo/errors.rb +7 -2
- data/lib/nodo/nodo.js +23 -7
- data/lib/nodo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95d8c9823753373c6be5ad0c493e7f14346950b611d4ca0d90c5a96bcabfa5e6
|
4
|
+
data.tar.gz: 3ad4b37694f4ef33cfadad03794ce7b4bf2a9fd42dc4e0abb319ec981e529b33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e0004a84541cb4edd4cc398250fe17ee5623aed13f1bbb76b31eee531867906cffd87c5e0f71b50c0c72fdae4f26e57d24f654fdd2623bee90b20c60f3ff0b4
|
7
|
+
data.tar.gz: 539b75f4a8283f4db872db614158403129651f4b0811d9e7f9ef5dfa1f506bedcf238fa0b93896fe776ae936bbc8eba9cbd44ea86fa8f80b87142e5d69cfe957
|
data/README.md
CHANGED
@@ -217,8 +217,63 @@ Nodo.debug = true
|
|
217
217
|
before instantiating any worker instances. The debug mode will be active during
|
218
218
|
the current process run.
|
219
219
|
|
220
|
+
To print a debug message from JS code:
|
220
221
|
|
221
|
-
|
222
|
+
```js
|
223
|
+
nodo.debug("Debug message");
|
224
|
+
```
|
225
|
+
|
226
|
+
### Evaluation
|
227
|
+
|
228
|
+
While `Nodo` is mainly function-based, it is possible to evaluate JS code in the
|
229
|
+
context of the defined object.
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
foo = Foo.new.evaluate("3 + 5")
|
233
|
+
=> 8
|
234
|
+
```
|
235
|
+
|
236
|
+
Evaluated code can access functions, required dependencies and constants:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
class Foo < Nodo::Core
|
240
|
+
const :BAR, 'bar'
|
241
|
+
require :uuid
|
242
|
+
function :hello, code: '() => "world"'
|
243
|
+
end
|
244
|
+
|
245
|
+
foo = Foo.new
|
246
|
+
|
247
|
+
foo.evaluate('BAR')
|
248
|
+
=> "bar"
|
249
|
+
|
250
|
+
foo.evaluate('uuid.v4()')
|
251
|
+
=> "f258bef3-0d6f-4566-ad39-d8dec973ef6b"
|
252
|
+
|
253
|
+
foo.evaluate('hello()')
|
254
|
+
=> "world"
|
255
|
+
```
|
256
|
+
|
257
|
+
Variables defined by evaluation are local to the current instance:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
one = Foo.new
|
261
|
+
one.evaluate('a = 1')
|
262
|
+
two = Foo.new
|
263
|
+
two.evaluate('a = 2')
|
264
|
+
one.evaluate('a') => 1
|
265
|
+
two.evaluate('a') => 2
|
266
|
+
```
|
267
|
+
|
268
|
+
⚠️ Evaluation comes with the usual caveats:
|
269
|
+
|
270
|
+
- Avoid modifying any of your predefined identifiers. Remember that in JS,
|
271
|
+
as in Ruby, constants are not necessarily constant.
|
272
|
+
- Never evaluate any code which includes un-checked user data. The Node.js process
|
273
|
+
has full read/write access to your filesystem! 💥
|
274
|
+
|
275
|
+
|
276
|
+
## Clean your Rails root
|
222
277
|
|
223
278
|
For Rails applications, Nodo enables you to move `node_modules`, `package.json` and
|
224
279
|
`yarn.lock` into your application's `vendor` folder by setting the `NODE_PATH` in
|
@@ -229,7 +284,7 @@ an initializer:
|
|
229
284
|
Nodo.modules_root = Rails.root.join('vendor', 'node_modules')
|
230
285
|
```
|
231
286
|
|
232
|
-
The rationale
|
287
|
+
The rationale for this is NPM modules being external vendor dependencies, which
|
233
288
|
should not clutter the application root directory.
|
234
289
|
|
235
290
|
With this new default, all `yarn` operations should be done after `cd`ing to `vendor`.
|
data/lib/nodo/constant.rb
CHANGED
data/lib/nodo/core.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
module Nodo
|
2
2
|
class Core
|
3
3
|
SOCKET_NAME = 'nodo.sock'
|
4
|
-
DEFINE_METHOD = '__nodo_define_class__'
|
5
|
-
|
4
|
+
DEFINE_METHOD = '__nodo_define_class__'.freeze
|
5
|
+
EVALUATE_METHOD = '__nodo_evaluate__'.freeze
|
6
|
+
GC_METHOD = '__nodo_gc__'.freeze
|
7
|
+
INTERNAL_METHODS = [DEFINE_METHOD, EVALUATE_METHOD, GC_METHOD].freeze
|
8
|
+
LAUNCH_TIMEOUT = 5
|
6
9
|
ARRAY_CLASS_ATTRIBUTES = %i[dependencies constants scripts].freeze
|
7
10
|
HASH_CLASS_ATTRIBUTES = %i[functions].freeze
|
8
11
|
CLASS_ATTRIBUTES = (ARRAY_CLASS_ATTRIBUTES + HASH_CLASS_ATTRIBUTES).freeze
|
@@ -10,6 +13,7 @@ module Nodo
|
|
10
13
|
@@node_pid = nil
|
11
14
|
@@tmpdir = nil
|
12
15
|
@@mutex = Mutex.new
|
16
|
+
@@exiting = nil
|
13
17
|
|
14
18
|
class << self
|
15
19
|
extend Forwardable
|
@@ -21,15 +25,11 @@ module Nodo
|
|
21
25
|
subclass.send "#{attr}=", send(attr).dup
|
22
26
|
end
|
23
27
|
end
|
24
|
-
|
28
|
+
|
25
29
|
def instance
|
26
30
|
@instance ||= new
|
27
31
|
end
|
28
32
|
|
29
|
-
def class_function(*methods)
|
30
|
-
singleton_class.def_delegators(:instance, *methods)
|
31
|
-
end
|
32
|
-
|
33
33
|
def class_defined?
|
34
34
|
!!class_defined
|
35
35
|
end
|
@@ -42,6 +42,7 @@ module Nodo
|
|
42
42
|
define_method "#{attr}=" do |value|
|
43
43
|
instance_variable_set :"@#{attr}", value
|
44
44
|
end
|
45
|
+
protected "#{attr}="
|
45
46
|
end
|
46
47
|
|
47
48
|
ARRAY_CLASS_ATTRIBUTES.each do |attr|
|
@@ -55,30 +56,6 @@ module Nodo
|
|
55
56
|
instance_variable_get(:"@#{attr}") || instance_variable_set(:"@#{attr}", {})
|
56
57
|
end
|
57
58
|
end
|
58
|
-
|
59
|
-
def require(*mods)
|
60
|
-
deps = mods.last.is_a?(Hash) ? mods.pop : {}
|
61
|
-
mods = mods.map { |m| [m, m] }.to_h
|
62
|
-
self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
|
63
|
-
end
|
64
|
-
|
65
|
-
def function(name, _code = nil, timeout: Nodo.timeout, code: nil)
|
66
|
-
raise ArgumentError, "reserved method name #{name.inspect}" if Nodo::Core.method_defined?(name) || name.to_s == DEFINE_METHOD
|
67
|
-
code = (code ||= _code).strip
|
68
|
-
raise ArgumentError, 'function code is required' if '' == code
|
69
|
-
loc = caller_locations(1, 1)[0]
|
70
|
-
source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'"
|
71
|
-
self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout))
|
72
|
-
define_method(name) { |*args| call_js_method(name, args) }
|
73
|
-
end
|
74
|
-
|
75
|
-
def const(name, value)
|
76
|
-
self.constants = constants + [Constant.new(name, value)]
|
77
|
-
end
|
78
|
-
|
79
|
-
def script(code)
|
80
|
-
self.scripts = scripts + [Script.new(code)]
|
81
|
-
end
|
82
59
|
|
83
60
|
def generate_core_code
|
84
61
|
<<~JS
|
@@ -106,8 +83,7 @@ module Nodo
|
|
106
83
|
def generate_class_code
|
107
84
|
<<~JS
|
108
85
|
(() => {
|
109
|
-
const
|
110
|
-
const __nodo_klass__ = {};
|
86
|
+
const __nodo_klass__ = { nodo: global.nodo };
|
111
87
|
#{dependencies.map(&:to_js).join}
|
112
88
|
#{constants.map(&:to_js).join}
|
113
89
|
#{functions.values.map(&:to_js).join}
|
@@ -117,14 +93,57 @@ module Nodo
|
|
117
93
|
JS
|
118
94
|
end
|
119
95
|
|
96
|
+
protected
|
97
|
+
|
98
|
+
def finalize_context(context_id)
|
99
|
+
proc do
|
100
|
+
if not @@exiting and core = Nodo::Core.instance
|
101
|
+
core.send(:call_js_method, GC_METHOD, context_id)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
120
106
|
private
|
121
107
|
|
108
|
+
def require(*mods)
|
109
|
+
deps = mods.last.is_a?(Hash) ? mods.pop : {}
|
110
|
+
mods = mods.map { |m| [m, m] }.to_h
|
111
|
+
self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def function(name, _code = nil, timeout: Nodo.timeout, code: nil)
|
115
|
+
raise ArgumentError, "reserved method name #{name.inspect}" if reserved_method_name?(name)
|
116
|
+
code = (code ||= _code).strip
|
117
|
+
raise ArgumentError, 'function code is required' if '' == code
|
118
|
+
loc = caller_locations(1, 1)[0]
|
119
|
+
source_location = "#{loc.path}:#{loc.lineno}: in `#{name}'"
|
120
|
+
self.functions = functions.merge(name => Function.new(name, _code || code, source_location, timeout))
|
121
|
+
define_method(name) { |*args| call_js_method(name, args) }
|
122
|
+
end
|
123
|
+
|
124
|
+
def class_function(*methods)
|
125
|
+
singleton_class.def_delegators(:instance, *methods)
|
126
|
+
end
|
127
|
+
|
128
|
+
def const(name, value)
|
129
|
+
self.constants = constants + [Constant.new(name, value)]
|
130
|
+
end
|
131
|
+
|
132
|
+
def script(code)
|
133
|
+
self.scripts = scripts + [Script.new(code)]
|
134
|
+
end
|
135
|
+
|
122
136
|
def nodo_js
|
123
137
|
Pathname.new(__FILE__).dirname.join('nodo.js').to_s.to_json
|
124
138
|
end
|
139
|
+
|
140
|
+
def reserved_method_name?(name)
|
141
|
+
Nodo::Core.method_defined?(name, false) || Nodo::Core.private_method_defined?(name, false) || name.to_s == DEFINE_METHOD
|
142
|
+
end
|
125
143
|
end
|
126
144
|
|
127
145
|
def initialize
|
146
|
+
raise ClassError, :new if self.class == Nodo::Core
|
128
147
|
@@mutex.synchronize do
|
129
148
|
ensure_process_is_spawned
|
130
149
|
wait_for_socket
|
@@ -132,6 +151,13 @@ module Nodo
|
|
132
151
|
end
|
133
152
|
end
|
134
153
|
|
154
|
+
def evaluate(code)
|
155
|
+
ensure_context_is_defined
|
156
|
+
call_js_method(EVALUATE_METHOD, code)
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
135
161
|
def node_pid
|
136
162
|
@@node_pid
|
137
163
|
end
|
@@ -148,9 +174,15 @@ module Nodo
|
|
148
174
|
self.class.clsid
|
149
175
|
end
|
150
176
|
|
177
|
+
def context_defined?
|
178
|
+
@context_defined
|
179
|
+
end
|
180
|
+
|
151
181
|
def log_exception(e)
|
152
182
|
return unless logger = Nodo.logger
|
153
|
-
|
183
|
+
message = "\n#{e.class} (#{e.message})"
|
184
|
+
message << ":\n\n#{e.backtrace.join("\n")}" if e.backtrace
|
185
|
+
logger.error message
|
154
186
|
end
|
155
187
|
|
156
188
|
def ensure_process_is_spawned
|
@@ -164,12 +196,22 @@ module Nodo
|
|
164
196
|
self.class.class_defined = true
|
165
197
|
end
|
166
198
|
|
199
|
+
def ensure_context_is_defined
|
200
|
+
return if context_defined?
|
201
|
+
@@mutex.synchronize do
|
202
|
+
call_js_method(EVALUATE_METHOD, '')
|
203
|
+
ObjectSpace.define_finalizer(self, self.class.send(:finalize_context, self.object_id))
|
204
|
+
@context_defined = true
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
167
208
|
def spawn_process
|
168
209
|
@@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
|
169
210
|
env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
|
170
211
|
env['NODO_DEBUG'] = '1' if Nodo.debug
|
171
212
|
@@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
|
172
213
|
at_exit do
|
214
|
+
@@exiting = true
|
173
215
|
Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
|
174
216
|
Process.wait(node_pid) rescue Errno::ECHILD
|
175
217
|
FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
|
@@ -179,7 +221,7 @@ module Nodo
|
|
179
221
|
def wait_for_socket
|
180
222
|
start = Time.now
|
181
223
|
socket = nil
|
182
|
-
while Time.now - start <
|
224
|
+
while Time.now - start < LAUNCH_TIMEOUT
|
183
225
|
begin
|
184
226
|
break if socket = UNIXSocket.new(socket_path)
|
185
227
|
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
|
@@ -192,10 +234,16 @@ module Nodo
|
|
192
234
|
|
193
235
|
def call_js_method(method, args)
|
194
236
|
raise CallError, 'Node process not ready' unless node_pid
|
195
|
-
raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || method
|
237
|
+
raise CallError, "Class #{clsid} not defined" unless self.class.class_defined? || INTERNAL_METHODS.include?(method)
|
196
238
|
function = self.class.functions[method]
|
197
|
-
raise NameError, "undefined function `#{method}' for #{self.class}" unless function || method
|
198
|
-
|
239
|
+
raise NameError, "undefined function `#{method}' for #{self.class}" unless function || INTERNAL_METHODS.include?(method)
|
240
|
+
context_id = case method
|
241
|
+
when DEFINE_METHOD then 0
|
242
|
+
when GC_METHOD then args.first
|
243
|
+
else
|
244
|
+
object_id
|
245
|
+
end
|
246
|
+
request = Net::HTTP::Post.new("/#{clsid}/#{context_id}/#{method}", 'Content-Type': 'application/json')
|
199
247
|
request.body = JSON.dump(args)
|
200
248
|
client = Client.new("unix://#{socket_path}")
|
201
249
|
client.read_timeout = function.timeout if function
|
data/lib/nodo/dependency.rb
CHANGED
data/lib/nodo/errors.rb
CHANGED
@@ -2,6 +2,11 @@ module Nodo
|
|
2
2
|
class Error < StandardError; end
|
3
3
|
class TimeoutError < Error; end
|
4
4
|
class CallError < Error; end
|
5
|
+
class ClassError < Error
|
6
|
+
def initialize(method = nil)
|
7
|
+
super("Cannot call method `#{method}' on Nodo::Core, use subclass instead")
|
8
|
+
end
|
9
|
+
end
|
5
10
|
|
6
11
|
class JavaScriptError < Error
|
7
12
|
attr_reader :attributes
|
@@ -37,7 +42,7 @@ module Nodo
|
|
37
42
|
end
|
38
43
|
|
39
44
|
def generate_message
|
40
|
-
message = "#{attributes['message'] || 'Unknown error'}"
|
45
|
+
message = "#{attributes['message'] || attributes['name'] || 'Unknown error'}"
|
41
46
|
message << format_location(attributes['loc'])
|
42
47
|
end
|
43
48
|
|
@@ -51,7 +56,7 @@ module Nodo
|
|
51
56
|
private
|
52
57
|
|
53
58
|
def generate_message
|
54
|
-
message = "#{attributes['message'] || 'Dependency error'}\n"
|
59
|
+
message = "#{attributes['message'] || attributes['name'] || 'Dependency error'}\n"
|
55
60
|
message << "The specified dependency '#{attributes['nodo_dependency']}' could not be loaded. "
|
56
61
|
message << "Run 'yarn add #{attributes['nodo_dependency']}' to install it.\n"
|
57
62
|
end
|
data/lib/nodo/nodo.js
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module.exports = (function() {
|
2
2
|
const DEFINE_METHOD = '__nodo_define_class__';
|
3
|
+
const EVALUATE_METHOD = '__nodo_evaluate__';
|
4
|
+
const GC_METHOD = '__nodo_gc__';
|
3
5
|
const DEBUG = process.env.NODO_DEBUG;
|
4
6
|
|
5
7
|
const vm = require('vm');
|
@@ -10,6 +12,7 @@ module.exports = (function() {
|
|
10
12
|
|
11
13
|
let server, closing;
|
12
14
|
const classes = {};
|
15
|
+
const contexts = {};
|
13
16
|
|
14
17
|
function render_error(e) {
|
15
18
|
let errInfo = {};
|
@@ -60,12 +63,17 @@ module.exports = (function() {
|
|
60
63
|
}
|
61
64
|
|
62
65
|
const url = req.url.substring(1);
|
63
|
-
const [class_name, method] = url.split('/');
|
64
|
-
let klass;
|
66
|
+
const [class_name, object_id, method] = url.split('/');
|
67
|
+
let klass, context;
|
65
68
|
|
66
69
|
if (classes.hasOwnProperty(class_name)) {
|
67
70
|
klass = classes[class_name];
|
68
|
-
if (
|
71
|
+
if (EVALUATE_METHOD == method) {
|
72
|
+
if (!contexts.hasOwnProperty(object_id)) {
|
73
|
+
contexts[object_id] = vm.createContext({ require: require, ...klass });
|
74
|
+
}
|
75
|
+
context = contexts[object_id];
|
76
|
+
} else if (!klass.hasOwnProperty(method) && !GC_METHOD == method) {
|
69
77
|
return respond_with_error(res, 404, `Method ${class_name}#${method} not found`);
|
70
78
|
}
|
71
79
|
} else if (DEFINE_METHOD != method) {
|
@@ -87,13 +95,21 @@ module.exports = (function() {
|
|
87
95
|
|
88
96
|
try {
|
89
97
|
if (DEFINE_METHOD == method) {
|
90
|
-
|
91
|
-
classes[class_name] = new_class;
|
98
|
+
classes[class_name] = vm.runInThisContext(input, class_name);
|
92
99
|
respond_with_data(res, class_name, start);
|
100
|
+
} else if (EVALUATE_METHOD == method) {
|
101
|
+
Promise.resolve(vm.runInNewContext(input, context)).then((result) => {
|
102
|
+
respond_with_data(res, result, start);
|
103
|
+
}).catch((error) => {
|
104
|
+
respond_with_error(res, 500, error);
|
105
|
+
});
|
106
|
+
} else if (GC_METHOD == method) {
|
107
|
+
delete contexts[object_id];
|
108
|
+
respond_with_data(res, true, start);
|
93
109
|
} else {
|
94
|
-
Promise.resolve(klass[method].apply(null, input)).then(
|
110
|
+
Promise.resolve(klass[method].apply(null, input)).then((result) => {
|
95
111
|
respond_with_data(res, result, start);
|
96
|
-
}).catch(
|
112
|
+
}).catch((error) => {
|
97
113
|
respond_with_error(res, 500, error);
|
98
114
|
});
|
99
115
|
}
|
data/lib/nodo/version.rb
CHANGED