nodo 1.5.0 → 1.5.5
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 +69 -7
- data/lib/nodo/client.rb +4 -4
- data/lib/nodo/core.rb +42 -21
- data/lib/nodo/function.rb +3 -3
- data/lib/nodo/nodo.js +120 -0
- data/lib/nodo/version.rb +1 -1
- data/lib/nodo.rb +4 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3cef66bc4b1d53b20500110ac7acbc226ec361b6aaed569874ce8568a6718242
|
4
|
+
data.tar.gz: ab2cae6eef01d53ebc1c471a5e08edfd37a263178adc49aa459c98dfcea4e92f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bf90e8b56366f2e42039026575db43a0c5a9808a677a533c3e211c9ae211a8f06b2ea0890550e22fca259f9fa416c0e96017a676538a3eb673f2a66caa0996a
|
7
|
+
data.tar.gz: 2573e2dfd199bb9f8e25dc11c5dcb49e0ba6f29796aaa523656fa1020e0d8ce325ab411cf2d60413ac0557407fec4a132ba51d75075585a0e7b56d76bfc42811
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[](http://badge.fury.io/rb/nodo)
|
1
|
+
[](http://badge.fury.io/rb/nodo) [](https://github.com/mtgrosser/nodo/actions/workflows/build.yml)
|
2
2
|
|
3
3
|
# Nōdo – call Node.js from Ruby
|
4
4
|
|
@@ -39,13 +39,13 @@ In Nodo, you define JS functions as you would define Ruby methods:
|
|
39
39
|
|
40
40
|
```ruby
|
41
41
|
class Foo < Nodo::Core
|
42
|
-
|
42
|
+
|
43
43
|
function :say_hi, <<~JS
|
44
44
|
(name) => {
|
45
45
|
return `Hello ${name}!`;
|
46
46
|
}
|
47
47
|
JS
|
48
|
-
|
48
|
+
|
49
49
|
end
|
50
50
|
|
51
51
|
foo = Foo.new
|
@@ -61,7 +61,7 @@ Install your modules to `node_modules`:
|
|
61
61
|
$ yarn add uuid
|
62
62
|
```
|
63
63
|
|
64
|
-
|
64
|
+
`require`ing your dependencies will make the library available as a `const` with the same name:
|
65
65
|
|
66
66
|
```ruby
|
67
67
|
class Bar < Nodo::Core
|
@@ -78,14 +78,26 @@ bar = Bar.new
|
|
78
78
|
bar.v4 => "b305f5c4-db9a-4504-b0c3-4e097a5ec8b9"
|
79
79
|
```
|
80
80
|
|
81
|
+
|
81
82
|
### Aliasing requires
|
82
83
|
|
84
|
+
If the library name cannot be used as name of the constant, the `const` name
|
85
|
+
can be given using hash syntax:
|
86
|
+
|
83
87
|
```ruby
|
84
88
|
class FooBar < Nodo::Core
|
85
89
|
require commonjs: '@rollup/plugin-commonjs'
|
86
90
|
end
|
87
91
|
```
|
88
92
|
|
93
|
+
### Alternate function definition syntax
|
94
|
+
|
95
|
+
JS code can also be supplied using the `code:` keyword argument:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
function :hello, code: "() => 'world'"
|
99
|
+
```
|
100
|
+
|
89
101
|
### Setting NODE_PATH
|
90
102
|
|
91
103
|
By default, `./node_modules` is used as the `NODE_PATH`.
|
@@ -117,12 +129,62 @@ end
|
|
117
129
|
class BarFoo < Nodo::Core
|
118
130
|
|
119
131
|
script <<~JS
|
120
|
-
//
|
121
|
-
//
|
132
|
+
// custom JS to be executed during initialization
|
133
|
+
// things defined here can later be used inside functions
|
134
|
+
const bigThing = someLib.init();
|
122
135
|
JS
|
123
136
|
end
|
124
137
|
```
|
125
138
|
|
126
139
|
### Inheritance
|
127
140
|
|
128
|
-
Subclasses will inherit functions, constants, dependencies and scripts from
|
141
|
+
Subclasses will inherit functions, constants, dependencies and scripts from
|
142
|
+
their superclasses, while only functions can be overwritten.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
class Foo < Nodo::Core
|
146
|
+
function :foo, "() => 'superclass'"
|
147
|
+
end
|
148
|
+
|
149
|
+
class SubFoo < Foo
|
150
|
+
function :bar, "() => { return 'calling' + foo() }"
|
151
|
+
end
|
152
|
+
|
153
|
+
class SubSubFoo < SubFoo
|
154
|
+
function :foo, "() => 'subsubclass'"
|
155
|
+
end
|
156
|
+
|
157
|
+
Foo.new.foo => "superclass"
|
158
|
+
SubFoo.new.bar => "callingsuperclass"
|
159
|
+
SubSubFoo.new.bar => "callingsubsubclass"
|
160
|
+
```
|
161
|
+
|
162
|
+
### Async functions
|
163
|
+
|
164
|
+
`Nodo` supports calling `async` functions from Ruby.
|
165
|
+
The Ruby call will happen synchronously, i.e. it will block until the JS
|
166
|
+
function resolves:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class SyncFoo < Nodo::Core
|
170
|
+
function :do_something, <<~JS
|
171
|
+
async () => { return await asyncFunc(); }
|
172
|
+
JS
|
173
|
+
end
|
174
|
+
```
|
175
|
+
|
176
|
+
### Limiting function execution time
|
177
|
+
|
178
|
+
The default timeout for a single JS function call is 60 seconds due to the
|
179
|
+
`Net::HTTP` default. It can be overridden on a per-function basis:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class Foo < Nodo::Core
|
183
|
+
function :sleep, timeout: 1, code: <<~'JS'
|
184
|
+
async (sec) => await new Promise(resolve => setTimeout(resolve, sec * 1000))
|
185
|
+
JS
|
186
|
+
end
|
187
|
+
|
188
|
+
foo.new.sleep(2)
|
189
|
+
=> Nodo::TimeoutError raised
|
190
|
+
```
|
data/lib/nodo/client.rb
CHANGED
@@ -28,10 +28,10 @@ module Nodo
|
|
28
28
|
|
29
29
|
def connect_unix
|
30
30
|
s = Timeout.timeout(@open_timeout) { UNIXSocket.open(@socket_path) }
|
31
|
-
@socket = Net::BufferedIO.new(s
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
@socket = Net::BufferedIO.new(s, read_timeout: @read_timeout,
|
32
|
+
write_timeout: @write_timeout,
|
33
|
+
continue_timeout: @continue_timeout,
|
34
|
+
debug_output: @debug_output)
|
35
35
|
on_connect
|
36
36
|
end
|
37
37
|
end
|
data/lib/nodo/core.rb
CHANGED
@@ -12,6 +12,7 @@ module Nodo
|
|
12
12
|
@@mutex = Mutex.new
|
13
13
|
|
14
14
|
class << self
|
15
|
+
extend Forwardable
|
15
16
|
|
16
17
|
attr_accessor :class_defined
|
17
18
|
|
@@ -25,6 +26,10 @@ module Nodo
|
|
25
26
|
@instance ||= new
|
26
27
|
end
|
27
28
|
|
29
|
+
def class_function(*methods)
|
30
|
+
singleton_class.def_delegators(:instance, *methods)
|
31
|
+
end
|
32
|
+
|
28
33
|
def class_defined?
|
29
34
|
!!class_defined
|
30
35
|
end
|
@@ -57,8 +62,11 @@ module Nodo
|
|
57
62
|
self.dependencies = dependencies + mods.merge(deps).map { |name, package| Dependency.new(name, package) }
|
58
63
|
end
|
59
64
|
|
60
|
-
def function(name, code)
|
61
|
-
|
65
|
+
def function(name, _code = nil, timeout: 60, code: nil)
|
66
|
+
raise ArgumentError, "reserved method name #{name.inspect}" if Nodo::Core.method_defined?(name)
|
67
|
+
code = (code ||= _code).strip
|
68
|
+
raise ArgumentError, 'function code is required' if '' == code
|
69
|
+
self.functions = functions.merge(name => Function.new(name, _code || code, caller.first, timeout))
|
62
70
|
define_method(name) { |*args| call_js_method(name, args) }
|
63
71
|
end
|
64
72
|
|
@@ -80,6 +88,8 @@ module Nodo
|
|
80
88
|
process.exit(1);
|
81
89
|
}
|
82
90
|
|
91
|
+
process.title = `nodo-core ${socket}`;
|
92
|
+
|
83
93
|
const shutdown = () => {
|
84
94
|
nodo.core.close(() => { process.exit(0) });
|
85
95
|
};
|
@@ -104,16 +114,6 @@ module Nodo
|
|
104
114
|
})()
|
105
115
|
JS
|
106
116
|
end
|
107
|
-
|
108
|
-
protected
|
109
|
-
|
110
|
-
def finalize(pid, tmpdir)
|
111
|
-
proc do
|
112
|
-
Process.kill(:SIGTERM, pid)
|
113
|
-
Process.wait(pid)
|
114
|
-
FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
|
115
|
-
end
|
116
|
-
end
|
117
117
|
|
118
118
|
private
|
119
119
|
|
@@ -155,23 +155,31 @@ module Nodo
|
|
155
155
|
return if self.class.class_defined?
|
156
156
|
call_js_method(DEFINE_METHOD, self.class.generate_class_code)
|
157
157
|
self.class.class_defined = true
|
158
|
-
# rescue => e
|
159
|
-
# raise Error, e.message
|
160
158
|
end
|
161
|
-
|
159
|
+
|
162
160
|
def spawn_process
|
163
161
|
@@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
|
164
162
|
env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
|
165
|
-
@@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s)
|
166
|
-
|
163
|
+
@@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
|
164
|
+
at_exit do
|
165
|
+
Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
|
166
|
+
Process.wait(node_pid) rescue Errno::ECHILD
|
167
|
+
FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
|
168
|
+
end
|
167
169
|
end
|
168
170
|
|
169
171
|
def wait_for_socket
|
170
172
|
start = Time.now
|
171
|
-
|
172
|
-
|
173
|
-
|
173
|
+
socket = nil
|
174
|
+
while Time.now - start < TIMEOUT
|
175
|
+
begin
|
176
|
+
break if socket = UNIXSocket.new(socket_path)
|
177
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
|
178
|
+
Kernel.sleep(0.2)
|
179
|
+
end
|
174
180
|
end
|
181
|
+
socket.close if socket
|
182
|
+
raise TimeoutError, "could not connect to socket #{socket_path}" unless socket
|
175
183
|
end
|
176
184
|
|
177
185
|
def call_js_method(method, args)
|
@@ -182,12 +190,15 @@ module Nodo
|
|
182
190
|
request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
|
183
191
|
request.body = JSON.dump(args)
|
184
192
|
client = Client.new("unix://#{socket_path}")
|
193
|
+
client.read_timeout = function.timeout if function
|
185
194
|
response = client.request(request)
|
186
195
|
if response.is_a?(Net::HTTPOK)
|
187
196
|
parse_response(response)
|
188
197
|
else
|
189
198
|
handle_error(response, function)
|
190
199
|
end
|
200
|
+
rescue Net::ReadTimeout
|
201
|
+
raise TimeoutError, "function call #{self.class}##{method} timed out"
|
191
202
|
rescue Errno::EPIPE, IOError
|
192
203
|
# TODO: restart or something? If this happens the process is completely broken
|
193
204
|
raise Error, 'Node process failed'
|
@@ -202,7 +213,17 @@ module Nodo
|
|
202
213
|
end
|
203
214
|
|
204
215
|
def parse_response(response)
|
205
|
-
|
216
|
+
data = response.body.force_encoding('UTF-8')
|
217
|
+
JSON.parse(data) unless data == ''
|
218
|
+
end
|
219
|
+
|
220
|
+
def with_tempfile(name)
|
221
|
+
ext = File.extname(name)
|
222
|
+
result = nil
|
223
|
+
Tempfile.create([File.basename(name, ext), ext], tmpdir) do |file|
|
224
|
+
result = yield(file)
|
225
|
+
end
|
226
|
+
result
|
206
227
|
end
|
207
228
|
|
208
229
|
end
|
data/lib/nodo/function.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Nodo
|
2
2
|
class Function
|
3
|
-
attr_reader :name, :code, :source_location
|
3
|
+
attr_reader :name, :code, :source_location, :timeout
|
4
4
|
|
5
|
-
def initialize(name, code, source_location)
|
6
|
-
@name, @code, @source_location = name, code, source_location
|
5
|
+
def initialize(name, code, source_location, timeout)
|
6
|
+
@name, @code, @source_location, @timeout = name, code, source_location, timeout
|
7
7
|
end
|
8
8
|
|
9
9
|
def to_js
|
data/lib/nodo/nodo.js
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module.exports = (function() {
|
2
|
+
const DEFINE_METHOD = '__nodo_define_class__';
|
3
|
+
|
4
|
+
const vm = require('vm');
|
5
|
+
const http = require('http');
|
6
|
+
const path = require('path');
|
7
|
+
const fs = require('fs');
|
8
|
+
const performance = require('perf_hooks').performance;
|
9
|
+
|
10
|
+
let server, closing;
|
11
|
+
const classes = {};
|
12
|
+
|
13
|
+
function render_error(e) {
|
14
|
+
let errInfo = {};
|
15
|
+
if (e instanceof Error) {
|
16
|
+
errInfo.name = e.name;
|
17
|
+
Object.getOwnPropertyNames(e).reduce((obj, prop) => { obj[prop] = e[prop]; return obj }, errInfo);
|
18
|
+
} else {
|
19
|
+
errInfo.name = e.toString();
|
20
|
+
}
|
21
|
+
return JSON.stringify({ error: errInfo });
|
22
|
+
}
|
23
|
+
|
24
|
+
function respond_with_error(res, code, name) {
|
25
|
+
res.statusCode = code;
|
26
|
+
const rendered = render_error(name);
|
27
|
+
log(`Error ${code} ${rendered}`);
|
28
|
+
res.end(rendered, 'utf8');
|
29
|
+
}
|
30
|
+
|
31
|
+
function respond_with_data(res, data, start) {
|
32
|
+
let timing;
|
33
|
+
res.statusCode = 200;
|
34
|
+
res.end(JSON.stringify(data), 'utf8');
|
35
|
+
if (start) {
|
36
|
+
timing = ` in ${(performance.now() - start).toFixed(2)}ms`;
|
37
|
+
}
|
38
|
+
log(`Completed 200 OK${timing}`);
|
39
|
+
}
|
40
|
+
|
41
|
+
function log(message) {
|
42
|
+
// fs.appendFileSync('log/nodo.log', `${message}\n`);
|
43
|
+
// console.log(`[Nodo] ${message}`);
|
44
|
+
}
|
45
|
+
|
46
|
+
const core = {
|
47
|
+
run: (socket) => {
|
48
|
+
log('Starting up...');
|
49
|
+
server = http.createServer((req, res) => {
|
50
|
+
const start = performance.now();
|
51
|
+
|
52
|
+
res.setHeader('Content-Type', 'application/json');
|
53
|
+
log(`${req.method} ${req.url}`);
|
54
|
+
|
55
|
+
if (req.method !== 'POST' || !req.url.startsWith('/')) {
|
56
|
+
return respond_with_error(res, 405, 'Method Not Allowed');
|
57
|
+
}
|
58
|
+
|
59
|
+
const url = req.url.substring(1);
|
60
|
+
const [class_name, method] = url.split('/');
|
61
|
+
let klass;
|
62
|
+
|
63
|
+
if (classes.hasOwnProperty(class_name)) {
|
64
|
+
klass = classes[class_name];
|
65
|
+
if (!klass.hasOwnProperty(method)) {
|
66
|
+
return respond_with_error(res, 404, `Method ${class_name}#${method} not found`);
|
67
|
+
}
|
68
|
+
} else if (DEFINE_METHOD != method) {
|
69
|
+
return respond_with_error(res, 404, `Class ${class_name} not defined`);
|
70
|
+
}
|
71
|
+
|
72
|
+
let body = '';
|
73
|
+
|
74
|
+
req.on('data', (data) => { body += data; });
|
75
|
+
|
76
|
+
req.on('end', () => {
|
77
|
+
let input, result;
|
78
|
+
|
79
|
+
try {
|
80
|
+
input = JSON.parse(body);
|
81
|
+
} catch (e) {
|
82
|
+
return respond_with_error(res, 400, 'Bad Request');
|
83
|
+
}
|
84
|
+
|
85
|
+
try {
|
86
|
+
if (DEFINE_METHOD == method) {
|
87
|
+
let new_class = vm.runInThisContext(input, class_name);
|
88
|
+
classes[class_name] = new_class;
|
89
|
+
respond_with_data(res, class_name, start);
|
90
|
+
} else {
|
91
|
+
Promise.resolve(klass[method].apply(null, input)).then(function (result) {
|
92
|
+
respond_with_data(res, result, start);
|
93
|
+
}).catch(function(error) {
|
94
|
+
respond_with_error(res, 500, error);
|
95
|
+
});
|
96
|
+
}
|
97
|
+
} catch(error) {
|
98
|
+
return respond_with_error(res, 500, error);
|
99
|
+
}
|
100
|
+
|
101
|
+
});
|
102
|
+
});
|
103
|
+
|
104
|
+
//server.maxConnections = 64;
|
105
|
+
server.listen(socket, () => {
|
106
|
+
log(`server ready, listening on ${socket} (max connections: ${server.maxConnections})`);
|
107
|
+
});
|
108
|
+
},
|
109
|
+
|
110
|
+
close: (finalizer) => {
|
111
|
+
log("Shutting down");
|
112
|
+
if (!closing) {
|
113
|
+
closing = true;
|
114
|
+
server.close(finalizer);
|
115
|
+
}
|
116
|
+
}
|
117
|
+
};
|
118
|
+
|
119
|
+
return { core: core, log: log };
|
120
|
+
})();
|
data/lib/nodo/version.rb
CHANGED
data/lib/nodo.rb
CHANGED
@@ -2,6 +2,9 @@ require 'pathname'
|
|
2
2
|
require 'json'
|
3
3
|
require 'fileutils'
|
4
4
|
require 'tmpdir'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'socket'
|
7
|
+
require 'forwardable'
|
5
8
|
|
6
9
|
module Nodo
|
7
10
|
class << self
|
@@ -21,4 +24,4 @@ require_relative 'nodo/constant'
|
|
21
24
|
require_relative 'nodo/client'
|
22
25
|
require_relative 'nodo/core'
|
23
26
|
|
24
|
-
require_relative 'nodo/railtie' if defined?(Rails)
|
27
|
+
require_relative 'nodo/railtie' if defined?(Rails::Railtie)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nodo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthias Grosser
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-10-
|
11
|
+
date: 2021-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -82,6 +82,7 @@ files:
|
|
82
82
|
- lib/nodo/dependency.rb
|
83
83
|
- lib/nodo/errors.rb
|
84
84
|
- lib/nodo/function.rb
|
85
|
+
- lib/nodo/nodo.js
|
85
86
|
- lib/nodo/railtie.rb
|
86
87
|
- lib/nodo/script.rb
|
87
88
|
- lib/nodo/version.rb
|
@@ -89,7 +90,7 @@ homepage: https://github.com/mtgrosser/nodo
|
|
89
90
|
licenses:
|
90
91
|
- MIT
|
91
92
|
metadata: {}
|
92
|
-
post_install_message:
|
93
|
+
post_install_message:
|
93
94
|
rdoc_options: []
|
94
95
|
require_paths:
|
95
96
|
- lib
|
@@ -104,8 +105,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
105
|
- !ruby/object:Gem::Version
|
105
106
|
version: '0'
|
106
107
|
requirements: []
|
107
|
-
rubygems_version: 3.
|
108
|
-
signing_key:
|
108
|
+
rubygems_version: 3.2.22
|
109
|
+
signing_key:
|
109
110
|
specification_version: 4
|
110
111
|
summary: Call Node.js from Ruby
|
111
112
|
test_files: []
|