nodo 1.5.1 → 1.5.6
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 +59 -6
- data/lib/nodo/client.rb +4 -4
- data/lib/nodo/core.rb +55 -24
- data/lib/nodo/function.rb +3 -3
- data/lib/nodo/nodo.js +13 -10
- data/lib/nodo/railtie.rb +2 -0
- data/lib/nodo/version.rb +1 -1
- data/lib/nodo.rb +8 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 650d220075d68a34268a03f2e68587b1caab13f47707c8b44f7cb7d05f5a5588
|
4
|
+
data.tar.gz: 0b73f0cd827a1ab364975d3559f64f2d9c49afe265759715f92d23f133bab10c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c1049cd1e574c0a7ec503ef01ae735d7b3ca33d21695a1a76c4c8f5c92f0914db58b59136a7ea0bcf17b83f9c6776817b8b4890061e6c442e414c5476d80037
|
7
|
+
data.tar.gz: d5fd39f74f9ebfabaec0463dbf7234e6da34a73b3167f2580fb6ac276f52f6aef433156ef0917b4a5c9c344f1dfc6d0f28e7c1af1bfe329829c5c87068d932dd
|
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`.
|
@@ -126,7 +138,8 @@ end
|
|
126
138
|
|
127
139
|
### Inheritance
|
128
140
|
|
129
|
-
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.
|
130
143
|
|
131
144
|
```ruby
|
132
145
|
class Foo < Nodo::Core
|
@@ -149,7 +162,8 @@ SubSubFoo.new.bar => "callingsubsubclass"
|
|
149
162
|
### Async functions
|
150
163
|
|
151
164
|
`Nodo` supports calling `async` functions from Ruby.
|
152
|
-
The Ruby call will happen synchronously, i.e. it will block until the JS
|
165
|
+
The Ruby call will happen synchronously, i.e. it will block until the JS
|
166
|
+
function resolves:
|
153
167
|
|
154
168
|
```ruby
|
155
169
|
class SyncFoo < Nodo::Core
|
@@ -158,3 +172,42 @@ class SyncFoo < Nodo::Core
|
|
158
172
|
JS
|
159
173
|
end
|
160
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
|
+
```
|
191
|
+
|
192
|
+
### Logging
|
193
|
+
|
194
|
+
By default, JS errors will be logged to `STDOUT`.
|
195
|
+
|
196
|
+
To set a custom logger:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
Nodo.logger = Logger.new('nodo.log')
|
200
|
+
```
|
201
|
+
|
202
|
+
In Rails applications, `Rails.logger` will automatically be set.
|
203
|
+
|
204
|
+
### Debugging
|
205
|
+
|
206
|
+
To get verbose debug output, set
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
Nodo.debug = true
|
210
|
+
```
|
211
|
+
|
212
|
+
before instantiating any worker instances. The debug mode will be active during
|
213
|
+
the current process run.
|
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,13 @@ 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) || 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))
|
62
72
|
define_method(name) { |*args| call_js_method(name, args) }
|
63
73
|
end
|
64
74
|
|
@@ -80,11 +90,13 @@ module Nodo
|
|
80
90
|
process.exit(1);
|
81
91
|
}
|
82
92
|
|
93
|
+
process.title = `nodo-core ${socket}`;
|
94
|
+
|
83
95
|
const shutdown = () => {
|
84
96
|
nodo.core.close(() => { process.exit(0) });
|
85
97
|
};
|
86
98
|
|
87
|
-
process.on('SIGINT', shutdown);
|
99
|
+
// process.on('SIGINT', shutdown);
|
88
100
|
process.on('SIGTERM', shutdown);
|
89
101
|
|
90
102
|
nodo.core.run(socket);
|
@@ -104,16 +116,6 @@ module Nodo
|
|
104
116
|
})()
|
105
117
|
JS
|
106
118
|
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
119
|
|
118
120
|
private
|
119
121
|
|
@@ -146,6 +148,11 @@ module Nodo
|
|
146
148
|
self.class.clsid
|
147
149
|
end
|
148
150
|
|
151
|
+
def log_exception(e)
|
152
|
+
return unless logger = Nodo.logger
|
153
|
+
logger.error "\n#{e.class} (#{e.message}):\n\n#{e.backtrace.join("\n")}"
|
154
|
+
end
|
155
|
+
|
149
156
|
def ensure_process_is_spawned
|
150
157
|
return if node_pid
|
151
158
|
spawn_process
|
@@ -155,23 +162,32 @@ module Nodo
|
|
155
162
|
return if self.class.class_defined?
|
156
163
|
call_js_method(DEFINE_METHOD, self.class.generate_class_code)
|
157
164
|
self.class.class_defined = true
|
158
|
-
# rescue => e
|
159
|
-
# raise Error, e.message
|
160
165
|
end
|
161
|
-
|
166
|
+
|
162
167
|
def spawn_process
|
163
168
|
@@tmpdir = Pathname.new(Dir.mktmpdir('nodo'))
|
164
169
|
env = Nodo.env.merge('NODE_PATH' => Nodo.modules_root.to_s)
|
165
|
-
|
166
|
-
|
170
|
+
env['NODO_DEBUG'] = '1' if Nodo.debug
|
171
|
+
@@node_pid = Process.spawn(env, Nodo.binary, '-e', self.class.generate_core_code, '--', socket_path.to_s, err: :out)
|
172
|
+
at_exit do
|
173
|
+
Process.kill(:SIGTERM, node_pid) rescue Errno::ECHILD
|
174
|
+
Process.wait(node_pid) rescue Errno::ECHILD
|
175
|
+
FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
|
176
|
+
end
|
167
177
|
end
|
168
178
|
|
169
179
|
def wait_for_socket
|
170
180
|
start = Time.now
|
171
|
-
|
172
|
-
|
173
|
-
|
181
|
+
socket = nil
|
182
|
+
while Time.now - start < TIMEOUT
|
183
|
+
begin
|
184
|
+
break if socket = UNIXSocket.new(socket_path)
|
185
|
+
rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTDIR
|
186
|
+
Kernel.sleep(0.2)
|
187
|
+
end
|
174
188
|
end
|
189
|
+
socket.close if socket
|
190
|
+
raise TimeoutError, "could not connect to socket #{socket_path}" unless socket
|
175
191
|
end
|
176
192
|
|
177
193
|
def call_js_method(method, args)
|
@@ -182,12 +198,15 @@ module Nodo
|
|
182
198
|
request = Net::HTTP::Post.new("/#{clsid}/#{method}", 'Content-Type': 'application/json')
|
183
199
|
request.body = JSON.dump(args)
|
184
200
|
client = Client.new("unix://#{socket_path}")
|
201
|
+
client.read_timeout = function.timeout if function
|
185
202
|
response = client.request(request)
|
186
203
|
if response.is_a?(Net::HTTPOK)
|
187
204
|
parse_response(response)
|
188
205
|
else
|
189
206
|
handle_error(response, function)
|
190
207
|
end
|
208
|
+
rescue Net::ReadTimeout
|
209
|
+
raise TimeoutError, "function call #{self.class}##{method} timed out"
|
191
210
|
rescue Errno::EPIPE, IOError
|
192
211
|
# TODO: restart or something? If this happens the process is completely broken
|
193
212
|
raise Error, 'Node process failed'
|
@@ -196,13 +215,25 @@ module Nodo
|
|
196
215
|
def handle_error(response, function)
|
197
216
|
if response.body
|
198
217
|
result = parse_response(response)
|
199
|
-
|
218
|
+
error = JavaScriptError.new(result['error'], function) if result.is_a?(Hash) && result.key?('error')
|
200
219
|
end
|
201
|
-
|
220
|
+
error ||= CallError.new("Node returned #{response.code}")
|
221
|
+
log_exception(error)
|
222
|
+
raise error
|
202
223
|
end
|
203
224
|
|
204
225
|
def parse_response(response)
|
205
|
-
|
226
|
+
data = response.body.force_encoding('UTF-8')
|
227
|
+
JSON.parse(data) unless data == ''
|
228
|
+
end
|
229
|
+
|
230
|
+
def with_tempfile(name)
|
231
|
+
ext = File.extname(name)
|
232
|
+
result = nil
|
233
|
+
Tempfile.create([File.basename(name, ext), ext], tmpdir) do |file|
|
234
|
+
result = yield(file)
|
235
|
+
end
|
236
|
+
result
|
206
237
|
end
|
207
238
|
|
208
239
|
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
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module.exports = (function() {
|
2
2
|
const DEFINE_METHOD = '__nodo_define_class__';
|
3
|
+
const DEBUG = process.env.NODO_DEBUG;
|
3
4
|
|
4
5
|
const vm = require('vm');
|
5
6
|
const http = require('http');
|
@@ -24,7 +25,7 @@ module.exports = (function() {
|
|
24
25
|
function respond_with_error(res, code, name) {
|
25
26
|
res.statusCode = code;
|
26
27
|
const rendered = render_error(name);
|
27
|
-
|
28
|
+
debug(`Error ${code} ${rendered}`);
|
28
29
|
res.end(rendered, 'utf8');
|
29
30
|
}
|
30
31
|
|
@@ -35,22 +36,24 @@ module.exports = (function() {
|
|
35
36
|
if (start) {
|
36
37
|
timing = ` in ${(performance.now() - start).toFixed(2)}ms`;
|
37
38
|
}
|
38
|
-
|
39
|
+
debug(`Completed 200 OK${timing}\n`);
|
39
40
|
}
|
40
41
|
|
41
|
-
function
|
42
|
-
|
43
|
-
|
42
|
+
function debug(message) {
|
43
|
+
if (DEBUG) {
|
44
|
+
// fs.appendFileSync('log/nodo.log', `${message}\n`);
|
45
|
+
console.log(`[Nodo] ${message}`);
|
46
|
+
}
|
44
47
|
}
|
45
48
|
|
46
49
|
const core = {
|
47
50
|
run: (socket) => {
|
48
|
-
|
51
|
+
debug('Starting up...');
|
49
52
|
server = http.createServer((req, res) => {
|
50
53
|
const start = performance.now();
|
51
54
|
|
52
55
|
res.setHeader('Content-Type', 'application/json');
|
53
|
-
|
56
|
+
debug(`${req.method} ${req.url}`);
|
54
57
|
|
55
58
|
if (req.method !== 'POST' || !req.url.startsWith('/')) {
|
56
59
|
return respond_with_error(res, 405, 'Method Not Allowed');
|
@@ -103,12 +106,12 @@ module.exports = (function() {
|
|
103
106
|
|
104
107
|
//server.maxConnections = 64;
|
105
108
|
server.listen(socket, () => {
|
106
|
-
|
109
|
+
debug(`server ready, listening on ${socket} (max connections: ${server.maxConnections})`);
|
107
110
|
});
|
108
111
|
},
|
109
112
|
|
110
113
|
close: (finalizer) => {
|
111
|
-
|
114
|
+
debug("Shutting down");
|
112
115
|
if (!closing) {
|
113
116
|
closing = true;
|
114
117
|
server.close(finalizer);
|
@@ -116,5 +119,5 @@ module.exports = (function() {
|
|
116
119
|
}
|
117
120
|
};
|
118
121
|
|
119
|
-
return { core: core,
|
122
|
+
return { core: core, debug: debug };
|
120
123
|
})();
|
data/lib/nodo/railtie.rb
CHANGED
data/lib/nodo/version.rb
CHANGED
data/lib/nodo.rb
CHANGED
@@ -2,14 +2,20 @@ require 'pathname'
|
|
2
2
|
require 'json'
|
3
3
|
require 'fileutils'
|
4
4
|
require 'tmpdir'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'logger'
|
7
|
+
require 'socket'
|
8
|
+
require 'forwardable'
|
5
9
|
|
6
10
|
module Nodo
|
7
11
|
class << self
|
8
|
-
attr_accessor :modules_root, :env, :binary
|
12
|
+
attr_accessor :modules_root, :env, :binary, :logger, :debug
|
9
13
|
end
|
10
14
|
self.modules_root = './node_modules'
|
11
15
|
self.env = {}
|
12
16
|
self.binary = 'node'
|
17
|
+
self.logger = Logger.new(STDOUT)
|
18
|
+
self.debug = false
|
13
19
|
end
|
14
20
|
|
15
21
|
require_relative 'nodo/version'
|
@@ -21,4 +27,4 @@ require_relative 'nodo/constant'
|
|
21
27
|
require_relative 'nodo/client'
|
22
28
|
require_relative 'nodo/core'
|
23
29
|
|
24
|
-
require_relative 'nodo/railtie' if defined?(Rails)
|
30
|
+
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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthias Grosser
|
8
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
|
@@ -105,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
105
|
- !ruby/object:Gem::Version
|
106
106
|
version: '0'
|
107
107
|
requirements: []
|
108
|
-
rubygems_version: 3.
|
108
|
+
rubygems_version: 3.2.22
|
109
109
|
signing_key:
|
110
110
|
specification_version: 4
|
111
111
|
summary: Call Node.js from Ruby
|