isomorfeus-speednode 0.3.1 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +22 -22
- data/README.md +126 -98
- data/lib/isomorfeus/speednode/attach_pipe.rb +226 -0
- data/lib/isomorfeus/speednode/attach_socket.rb +46 -0
- data/lib/isomorfeus/speednode/config.rb +23 -0
- data/lib/isomorfeus/speednode/runner.js +327 -239
- data/lib/isomorfeus/speednode/runtime/context.rb +176 -0
- data/lib/isomorfeus/speednode/runtime/vm.rb +204 -0
- data/lib/isomorfeus/speednode/runtime/vm_command.rb +40 -0
- data/lib/isomorfeus/speednode/runtime.rb +45 -288
- data/lib/isomorfeus/speednode/version.rb +1 -1
- data/lib/isomorfeus-speednode.rb +21 -12
- metadata +31 -11
@@ -0,0 +1,176 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Speednode
|
3
|
+
class Runtime < ExecJS::Runtime
|
4
|
+
class Context < ::ExecJS::Runtime::Context
|
5
|
+
def self.finalize(runtime, uuid)
|
6
|
+
proc { runtime.vm.delete_context(uuid) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(runtime, source = "", options = {})
|
10
|
+
|
11
|
+
@runtime = runtime
|
12
|
+
@uuid = SecureRandom.uuid
|
13
|
+
@permissive = !!options.delete(:permissive)
|
14
|
+
@vm = @runtime.vm
|
15
|
+
@context = nil
|
16
|
+
@timeout = options[:timeout] ? options[:timeout]/1000 : 600
|
17
|
+
|
18
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@runtime, @uuid))
|
19
|
+
|
20
|
+
filename = options.delete(:filename)
|
21
|
+
source = File.read(filename) if filename
|
22
|
+
|
23
|
+
begin
|
24
|
+
source = encode(source)
|
25
|
+
rescue
|
26
|
+
source = source.force_encoding('UTF-8')
|
27
|
+
end
|
28
|
+
|
29
|
+
@permissive ? raw_createp(source, options) : raw_create(source, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# def options
|
33
|
+
# @vm.context_options(@uuid)
|
34
|
+
# end
|
35
|
+
|
36
|
+
def attach(func, procedure = nil, &block)
|
37
|
+
raise "#attach requires a permissive context." unless @permissive
|
38
|
+
run_block = block_given? ? block : procedure
|
39
|
+
Isomorfeus::Speednode::Runtime.attach_proc(@uuid, func, run_block)
|
40
|
+
@vm.attach(@uuid, func)
|
41
|
+
end
|
42
|
+
|
43
|
+
def await(source)
|
44
|
+
raw_eval <<~JAVASCRIPT
|
45
|
+
(async () => {
|
46
|
+
global.__LastExecutionFinished = false;
|
47
|
+
global.__LastResult = null;
|
48
|
+
global.__LastErr = null;
|
49
|
+
global.__LastResult = await #{source};
|
50
|
+
global.__LastExecutionFinished = true;
|
51
|
+
})().catch(function(err) {
|
52
|
+
global.__LastResult = null;
|
53
|
+
global.__LastErr = err;
|
54
|
+
global.__LastExecutionFinished = true;
|
55
|
+
})
|
56
|
+
JAVASCRIPT
|
57
|
+
await_result
|
58
|
+
end
|
59
|
+
|
60
|
+
def call(identifier, *args)
|
61
|
+
raw_eval("#{identifier}.apply(this, #{::Oj.dump(args, mode: :strict)})")
|
62
|
+
end
|
63
|
+
|
64
|
+
def eval(source, _options = nil)
|
65
|
+
raw_eval(source) if /\S/ =~ source
|
66
|
+
end
|
67
|
+
|
68
|
+
def exec(source, _options = nil)
|
69
|
+
raw_exec("(function(){#{source}})()")
|
70
|
+
end
|
71
|
+
|
72
|
+
def permissive?
|
73
|
+
@permissive
|
74
|
+
end
|
75
|
+
|
76
|
+
def permissive_eval(source, _options = nil)
|
77
|
+
raise "Context not permissive!" unless @permissive
|
78
|
+
raw_eval(source) if /\S/ =~ source
|
79
|
+
end
|
80
|
+
|
81
|
+
def permissive_exec(source, _options = nil)
|
82
|
+
raise "Context not permissive!" unless @permissive
|
83
|
+
raw_exec("(function(){#{source}})()")
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def raw_eval(source)
|
89
|
+
extract_result(@vm.eval(@uuid, encode(source)))
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def raw_exec(source)
|
94
|
+
extract_result(@vm.exec(@uuid, encode(source)))
|
95
|
+
end
|
96
|
+
|
97
|
+
def raw_create(source, options)
|
98
|
+
return if @context
|
99
|
+
source = encode(source)
|
100
|
+
result = @vm.create(@uuid, source, options)
|
101
|
+
@context = true
|
102
|
+
extract_result(result)
|
103
|
+
end
|
104
|
+
|
105
|
+
def raw_createp(source, options)
|
106
|
+
return if @context
|
107
|
+
source = encode(source)
|
108
|
+
result = @vm.createp(@uuid, source, options)
|
109
|
+
@context = true
|
110
|
+
extract_result(result)
|
111
|
+
end
|
112
|
+
|
113
|
+
def extract_result(output)
|
114
|
+
if output[0] == 'ok'
|
115
|
+
output[1]
|
116
|
+
else
|
117
|
+
_status, value, stack = output
|
118
|
+
stack ||= ""
|
119
|
+
stack = stack.split("\n").map do |line|
|
120
|
+
line.sub(" at ", "").strip
|
121
|
+
end
|
122
|
+
stack.reject! do |line|
|
123
|
+
line.include?('(node:') ||
|
124
|
+
line.include?('lib\isomorfeus\speednode\runner.js') ||
|
125
|
+
line.include?('lib/isomorfeus/speednode/runner.js')
|
126
|
+
end
|
127
|
+
stack.shift unless stack[0].to_s.include?("(execjs)")
|
128
|
+
error_class = value =~ /SyntaxError:/ ? ExecJS::RuntimeError : ExecJS::ProgramError
|
129
|
+
error = error_class.new(value)
|
130
|
+
error.set_backtrace(stack + caller)
|
131
|
+
raise error
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def await_result
|
136
|
+
start_time = Time.now
|
137
|
+
while !execution_finished? && !timed_out?(start_time)
|
138
|
+
sleep 0.005
|
139
|
+
end
|
140
|
+
result = exec <<~JAVASCRIPT
|
141
|
+
if (global.__LastExecutionFinished === true) {
|
142
|
+
var err = global.__LastErr;
|
143
|
+
var result = global.__LastResult;
|
144
|
+
|
145
|
+
global.__LastErr = null;
|
146
|
+
global.__LastResult = null;
|
147
|
+
global.__LastExecutionFinished = false;
|
148
|
+
|
149
|
+
if (err) { return ['err', ['', err].join(''), err.stack]; }
|
150
|
+
else if (typeof result === 'undefined' && result !== null) { return ['ok']; }
|
151
|
+
else {
|
152
|
+
try { return ['ok', result]; }
|
153
|
+
catch (err) { return ['err', ['', err].join(''), err.stack]; }
|
154
|
+
}
|
155
|
+
} else {
|
156
|
+
var new_err = new Error('Last command did not yet finish execution!');
|
157
|
+
return ['err', ['', new_err].join(''), new_err.stack];
|
158
|
+
}
|
159
|
+
JAVASCRIPT
|
160
|
+
extract_result(result)
|
161
|
+
end
|
162
|
+
|
163
|
+
def execution_finished?
|
164
|
+
eval 'global.__LastExecutionFinished'
|
165
|
+
end
|
166
|
+
|
167
|
+
def timed_out?(start_time)
|
168
|
+
if (Time.now - start_time) > @timeout
|
169
|
+
raise "Command Execution timed out!"
|
170
|
+
end
|
171
|
+
false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
if Gem.win_platform?
|
2
|
+
require 'securerandom'
|
3
|
+
require 'win32/pipe'
|
4
|
+
|
5
|
+
module Win32
|
6
|
+
class Pipe
|
7
|
+
def write(data)
|
8
|
+
bytes = FFI::MemoryPointer.new(:ulong)
|
9
|
+
|
10
|
+
raise Error, "no pipe created" unless @pipe
|
11
|
+
|
12
|
+
if @asynchronous
|
13
|
+
bool = WriteFile(@pipe, data, data.bytesize, bytes, @overlapped)
|
14
|
+
bytes_written = bytes.read_ulong
|
15
|
+
|
16
|
+
if bool && bytes_written > 0
|
17
|
+
@pending_io = false
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
|
21
|
+
error = GetLastError()
|
22
|
+
if !bool && error == ERROR_IO_PENDING
|
23
|
+
@pending_io = true
|
24
|
+
return true
|
25
|
+
end
|
26
|
+
|
27
|
+
return false
|
28
|
+
else
|
29
|
+
unless WriteFile(@pipe, data, data.bytesize, bytes, nil)
|
30
|
+
raise SystemCallError.new("WriteFile", FFI.errno)
|
31
|
+
end
|
32
|
+
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Isomorfeus
|
41
|
+
module Speednode
|
42
|
+
class Runtime < ExecJS::Runtime
|
43
|
+
class VM
|
44
|
+
attr_reader :responder
|
45
|
+
|
46
|
+
def initialize(options)
|
47
|
+
@mutex = Mutex.new
|
48
|
+
@socket_path = nil
|
49
|
+
@options = options
|
50
|
+
@started = false
|
51
|
+
@socket = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def started?
|
55
|
+
@started
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.finalize(socket, socket_dir, socket_path, pid)
|
59
|
+
proc do
|
60
|
+
Isomorfeus::Speednode::Runtime.responders[socket_path].kill if Isomorfeus::Speednode::Runtime.responders[socket_path]
|
61
|
+
exit_node(socket, socket_dir, socket_path, pid)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.exit_node(socket, socket_dir, socket_path, pid)
|
66
|
+
VMCommand.new(socket, "exit", [0]).execute
|
67
|
+
socket.close
|
68
|
+
File.unlink(socket_path) if File.exist?(socket_path)
|
69
|
+
Dir.rmdir(socket_dir) if socket_dir && Dir.exist?(socket_dir)
|
70
|
+
if Gem.win_platform?
|
71
|
+
# SIGINT or SIGKILL are unreliable on Windows, try native taskkill first
|
72
|
+
unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
|
73
|
+
Process.kill('KILL', pid) rescue nil
|
74
|
+
end
|
75
|
+
else
|
76
|
+
Process.kill('KILL', pid) rescue nil
|
77
|
+
end
|
78
|
+
rescue
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def eval(context, source)
|
83
|
+
command("eval", {'context' => context, 'source' => source})
|
84
|
+
end
|
85
|
+
|
86
|
+
def exec(context, source)
|
87
|
+
command("exec", {'context' => context, 'source' => source})
|
88
|
+
end
|
89
|
+
|
90
|
+
def create(context, source, options)
|
91
|
+
command("create", {'context' => context, 'source' => source, 'options' => options})
|
92
|
+
end
|
93
|
+
|
94
|
+
def createp(context, source, options)
|
95
|
+
command("createp", {'context' => context, 'source' => source, 'options' => options})
|
96
|
+
end
|
97
|
+
|
98
|
+
def attach(context, func)
|
99
|
+
create_responder(context) unless responder
|
100
|
+
command("attach", {'context' => context, 'func' => func })
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete_context(context)
|
104
|
+
command("deleteContext", context)
|
105
|
+
end
|
106
|
+
|
107
|
+
# def context_options(context)
|
108
|
+
# command('ctxo', {'context' => context })
|
109
|
+
# end
|
110
|
+
|
111
|
+
def start
|
112
|
+
@mutex.synchronize do
|
113
|
+
start_without_synchronization
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def start_without_synchronization
|
120
|
+
return if @started
|
121
|
+
if ExecJS.windows?
|
122
|
+
@socket_dir = nil
|
123
|
+
@socket_path = SecureRandom.uuid
|
124
|
+
else
|
125
|
+
@socket_dir = Dir.mktmpdir("isomorfeus-speednode-")
|
126
|
+
@socket_path = File.join(@socket_dir, "socket")
|
127
|
+
end
|
128
|
+
@pid = Process.spawn({"SOCKET_PATH" => @socket_path}, @options[:binary], @options[:source_maps], @options[:runner_path])
|
129
|
+
|
130
|
+
retries = 50
|
131
|
+
|
132
|
+
if ExecJS.windows?
|
133
|
+
timeout_or_connected = false
|
134
|
+
begin
|
135
|
+
retries -= 1
|
136
|
+
begin
|
137
|
+
@socket = Win32::Pipe::Client.new(@socket_path, Win32::Pipe::ACCESS_DUPLEX)
|
138
|
+
rescue
|
139
|
+
sleep 0.1
|
140
|
+
raise "Unable to start nodejs process in time" if retries == 0
|
141
|
+
next
|
142
|
+
end
|
143
|
+
timeout_or_connected = true
|
144
|
+
end until timeout_or_connected
|
145
|
+
else
|
146
|
+
while !File.exist?(@socket_path)
|
147
|
+
sleep 0.1
|
148
|
+
retries -= 1
|
149
|
+
raise "Unable to start nodejs process in time" if retries == 0
|
150
|
+
end
|
151
|
+
|
152
|
+
@socket = UNIXSocket.new(@socket_path)
|
153
|
+
end
|
154
|
+
|
155
|
+
@started = true
|
156
|
+
|
157
|
+
at_exit do
|
158
|
+
begin
|
159
|
+
self.class.exit_node(@socket, @socket_dir, @socket_path, @pid) unless @socket.closed?
|
160
|
+
rescue
|
161
|
+
# do nothing
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@socket, @socket_dir, @socket_path, @pid))
|
166
|
+
Kernel.at_exit { self.class.finalize(@socket, @socket_dir, @socket_path, @pid).call }
|
167
|
+
end
|
168
|
+
|
169
|
+
def create_responder(context)
|
170
|
+
start unless @started
|
171
|
+
run_block = Proc.new do |request|
|
172
|
+
args = ::Oj.load(request.chop!, mode: :strict)
|
173
|
+
req_context = args[0]
|
174
|
+
method = args[1]
|
175
|
+
method_args = args[2]
|
176
|
+
begin
|
177
|
+
result = Isomorfeus::Speednode::Runtime.attached_procs[req_context][method].call(*method_args)
|
178
|
+
::Oj.dump(['ok', result], mode: :strict)
|
179
|
+
rescue Exception => err
|
180
|
+
::Oj.dump(['err', err.class.to_s, [err.message].concat(err.backtrace)], mode: :strict)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
responder_path = @socket_path + '_responder'
|
184
|
+
@responder = Thread.new do
|
185
|
+
if ExecJS.windows?
|
186
|
+
Isomorfeus::Speednode::AttachPipe.new(responder_path, run_block).run
|
187
|
+
else
|
188
|
+
Isomorfeus::Speednode::AttachSocket.new(responder_path, run_block).run
|
189
|
+
end
|
190
|
+
end
|
191
|
+
Isomorfeus::Speednode::Runtime.responders[@socket_path] = @responder_thread
|
192
|
+
@responder.run
|
193
|
+
end
|
194
|
+
|
195
|
+
def command(cmd, *arguments)
|
196
|
+
@mutex.synchronize do
|
197
|
+
start_without_synchronization unless @started
|
198
|
+
VMCommand.new(@socket, cmd, arguments).execute
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Isomorfeus
|
2
|
+
module Speednode
|
3
|
+
class Runtime < ExecJS::Runtime
|
4
|
+
class VMCommand
|
5
|
+
def initialize(socket, cmd, arguments)
|
6
|
+
@socket = socket
|
7
|
+
@cmd = cmd
|
8
|
+
@arguments = arguments
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute
|
12
|
+
result = ''
|
13
|
+
message = ::Oj.dump({ 'cmd' => @cmd, 'args' => @arguments }, mode: :strict)
|
14
|
+
message = message + "\x04"
|
15
|
+
bytes_to_send = message.bytesize
|
16
|
+
sent_bytes = 0
|
17
|
+
|
18
|
+
if ExecJS.windows?
|
19
|
+
@socket.write(message)
|
20
|
+
begin
|
21
|
+
result << @socket.read
|
22
|
+
end until result.end_with?("\x04")
|
23
|
+
else
|
24
|
+
sent_bytes = @socket.sendmsg(message)
|
25
|
+
if sent_bytes < bytes_to_send
|
26
|
+
while sent_bytes < bytes_to_send
|
27
|
+
sent_bytes += @socket.sendmsg(message.byteslice((sent_bytes)..-1))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
result << @socket.recvmsg()[0]
|
33
|
+
end until result.end_with?("\x04")
|
34
|
+
end
|
35
|
+
::Oj.load(result.chop!, create_additions: false)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|