isomorfeus-speednode 0.3.1 → 0.4.0

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.
@@ -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,199 @@
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)
69
+ Dir.rmdir(socket_dir)
70
+ if Gem.win_platform?
71
+ # SIGINT or SIGKILL are unreliable on Windows, try native taskkill first
72
+ Process.kill('KILL', pid) unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
73
+ else
74
+ Process.kill 'KILL', pid
75
+ end
76
+ end
77
+
78
+ def eval(context, source)
79
+ command("eval", {'context' => context, 'source' => source})
80
+ end
81
+
82
+ def exec(context, source)
83
+ command("exec", {'context' => context, 'source' => source})
84
+ end
85
+
86
+ def create(context, source, options)
87
+ command("create", {'context' => context, 'source' => source, 'options' => options})
88
+ end
89
+
90
+ def createp(context, source, options)
91
+ command("createp", {'context' => context, 'source' => source, 'options' => options})
92
+ end
93
+
94
+ def attach(context, func)
95
+ create_responder(context) unless responder
96
+ command("attach", {'context' => context, 'func' => func })
97
+ end
98
+
99
+ def delete_context(context)
100
+ command("deleteContext", context)
101
+ end
102
+
103
+ # def context_options(context)
104
+ # command('ctxo', {'context' => context })
105
+ # end
106
+
107
+ def start
108
+ @mutex.synchronize do
109
+ start_without_synchronization
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def start_without_synchronization
116
+ return if @started
117
+ if ExecJS.windows?
118
+ @socket_dir = nil
119
+ @socket_path = SecureRandom.uuid
120
+ else
121
+ @socket_dir = Dir.mktmpdir("isomorfeus-speednode-")
122
+ @socket_path = File.join(@socket_dir, "socket")
123
+ end
124
+ @pid = Process.spawn({"SOCKET_PATH" => @socket_path}, @options[:binary], @options[:source_maps], @options[:runner_path])
125
+
126
+ retries = 20
127
+
128
+ if ExecJS.windows?
129
+ timeout_or_connected = false
130
+ begin
131
+ retries -= 1
132
+ begin
133
+ @socket = Win32::Pipe::Client.new(@socket_path, Win32::Pipe::ACCESS_DUPLEX)
134
+ rescue
135
+ sleep 0.1
136
+ raise "Unable to start nodejs process in time" if retries == 0
137
+ next
138
+ end
139
+ timeout_or_connected = true
140
+ end until timeout_or_connected
141
+ else
142
+ while !File.exist?(@socket_path)
143
+ sleep 0.1
144
+ retries -= 1
145
+ raise "Unable to start nodejs process in time" if retries == 0
146
+ end
147
+
148
+ @socket = UNIXSocket.new(@socket_path)
149
+ end
150
+
151
+ @started = true
152
+
153
+ at_exit do
154
+ begin
155
+ self.class.exit_node(@socket, @socket_dir, @socket_path, @pid) unless @socket.closed?
156
+ rescue
157
+ # do nothing
158
+ end
159
+ end
160
+
161
+ ObjectSpace.define_finalizer(self, self.class.finalize(@socket, @socket_dir, @socket_path, @pid))
162
+ end
163
+
164
+ def create_responder(context)
165
+ start unless @started
166
+ run_block = Proc.new do |request|
167
+ args = ::Oj.load(request.chop!, mode: :strict)
168
+ req_context = args[0]
169
+ method = args[1]
170
+ method_args = args[2]
171
+ begin
172
+ result = Isomorfeus::Speednode::Runtime.attached_procs[req_context][method].call(*method_args)
173
+ ::Oj.dump(['ok', result], mode: :strict)
174
+ rescue Exception => err
175
+ ::Oj.dump(['err', err.class.to_s, [err.message].concat(err.backtrace)], mode: :strict)
176
+ end
177
+ end
178
+ responder_path = @socket_path + '_responder'
179
+ @responder = Thread.new do
180
+ if ExecJS.windows?
181
+ Isomorfeus::Speednode::AttachPipe.new(responder_path, run_block).run
182
+ else
183
+ Isomorfeus::Speednode::AttachSocket.new(responder_path, run_block).run
184
+ end
185
+ end
186
+ Isomorfeus::Speednode::Runtime.responders[@socket_path] = @responder_thread
187
+ @responder.run
188
+ end
189
+
190
+ def command(cmd, *arguments)
191
+ @mutex.synchronize do
192
+ start_without_synchronization unless @started
193
+ VMCommand.new(@socket, cmd, arguments).execute
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ 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
@@ -1,288 +1,45 @@
1
- require 'securerandom'
2
- if Gem.win_platform?
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 VMCommand
44
- def initialize(socket, cmd, arguments)
45
- @socket = socket
46
- @cmd = cmd
47
- @arguments = arguments
48
- end
49
-
50
- def execute
51
- result = ''
52
- message = ::Oj.dump({ 'cmd' => @cmd, 'args' => @arguments }, mode: :strict)
53
- message = message + "\x04"
54
- bytes_to_send = message.bytesize
55
- sent_bytes = 0
56
-
57
- if ExecJS.windows?
58
- @socket.write(message)
59
- begin
60
- result << @socket.read
61
- end until result.end_with?("\x04")
62
- else
63
- sent_bytes = @socket.sendmsg(message)
64
- if sent_bytes < bytes_to_send
65
- while sent_bytes < bytes_to_send
66
- sent_bytes += @socket.sendmsg(message.byteslice((sent_bytes)..-1))
67
- end
68
- end
69
-
70
- begin
71
- result << @socket.recvmsg()[0]
72
- end until result.end_with?("\x04")
73
- end
74
- ::Oj.load(result.chop!, create_additions: false)
75
- end
76
- end
77
-
78
- class VM
79
- def initialize(options)
80
- @mutex = Mutex.new
81
- @socket_path = nil
82
- @options = options
83
- @started = false
84
- @socket = nil
85
- end
86
-
87
- def started?
88
- @started
89
- end
90
-
91
- def self.finalize(socket, socket_dir, socket_path, pid)
92
- proc { exit_node(socket, socket_dir, socket_path, pid) }
93
- end
94
-
95
- def self.exit_node(socket, socket_dir, socket_path, pid)
96
- VMCommand.new(socket, "exit", [0]).execute
97
- socket.close
98
- File.unlink(socket_path)
99
- Dir.rmdir(socket_dir)
100
- Process.kill('KILL', pid)
101
- end
102
-
103
- def exec(context, source)
104
- command("exec", {'context' => context, 'source' => source})
105
- end
106
-
107
- def execp(context, source)
108
- command("execp", {'context' => context, 'source' => source})
109
- end
110
-
111
- def delete_context(context)
112
- command("deleteContext", context)
113
- end
114
-
115
- def start
116
- @mutex.synchronize do
117
- start_without_synchronization
118
- end
119
- end
120
-
121
- private
122
-
123
- def start_without_synchronization
124
- return if @started
125
- if ExecJS.windows?
126
- @socket_dir = nil
127
- @socket_path = SecureRandom.uuid
128
- else
129
- @socket_dir = Dir.mktmpdir("isomorfeus-speednode-")
130
- @socket_path = File.join(@socket_dir, "socket")
131
- end
132
- @pid = Process.spawn({"SOCKET_PATH" => @socket_path}, @options[:binary], @options[:runner_path])
133
-
134
- retries = 20
135
-
136
- if ExecJS.windows?
137
- timeout_or_connected = false
138
- begin
139
- retries -= 1
140
- begin
141
- @socket = Win32::Pipe::Client.new(@socket_path, Win32::Pipe::ACCESS_DUPLEX)
142
- rescue
143
- sleep 0.1
144
- raise "Unable to start nodejs process in time" if retries == 0
145
- next
146
- end
147
- timeout_or_connected = true
148
- end until timeout_or_connected
149
- else
150
- while !File.exist?(@socket_path)
151
- sleep 0.1
152
- retries -= 1
153
- raise "Unable to start nodejs process in time" if retries == 0
154
- end
155
-
156
- @socket = UNIXSocket.new(@socket_path)
157
- end
158
-
159
- @started = true
160
-
161
- at_exit do
162
- begin
163
- self.class.exit_node(@socket, @socket_dir, @socket_path, @pid) unless @socket.closed?
164
- rescue
165
- # do nothing
166
- end
167
- end
168
-
169
- ObjectSpace.define_finalizer(self, self.class.finalize(@socket, @socket_dir, @socket_path, @pid))
170
- end
171
-
172
- def command(cmd, *arguments)
173
- @mutex.synchronize do
174
- start_without_synchronization
175
- VMCommand.new(@socket, cmd, arguments).execute
176
- end
177
- end
178
- end
179
-
180
- class Context < ::ExecJS::Runtime::Context
181
- def initialize(runtime, source = "", options = {})
182
- @runtime = runtime
183
- @uuid = SecureRandom.uuid
184
- @permissive = !!options[:permissive]
185
-
186
- ObjectSpace.define_finalizer(self, self.class.finalize(@runtime, @uuid))
187
-
188
- begin
189
- source = encode(source)
190
- rescue
191
- source = source.force_encoding('UTF-8')
192
- end
193
-
194
- @permissive ? raw_execp(source) : raw_exec(source)
195
- end
196
-
197
- def self.finalize(runtime, uuid)
198
- proc { runtime.vm.delete_context(uuid) }
199
- end
200
-
201
- def call(identifier, *args)
202
- eval "#{identifier}.apply(this, #{::Oj.dump(args, mode: :strict)})"
203
- end
204
-
205
- def eval(source, options = {})
206
- raw_exec("(#{source})") if /\S/ =~ source
207
- end
208
-
209
- def exec(source, options = {})
210
- raw_exec("(function(){#{source}})()")
211
- end
212
-
213
- def permissive?
214
- @permissive
215
- end
216
-
217
- def permissive_eval(source, options = {})
218
- raw_execp("(#{source})") if /\S/ =~ source
219
- end
220
-
221
- def permissive_exec(source, options = {})
222
- raw_execp("(function(){#{source}})()")
223
- end
224
-
225
- def raw_exec(source)
226
- source = encode(source)
227
-
228
- result = @runtime.vm.exec(@uuid, source)
229
- extract_result(result)
230
- end
231
-
232
- def raw_execp(source)
233
- source = encode(source)
234
-
235
- result = @runtime.vm.execp(@uuid, source)
236
- extract_result(result)
237
- end
238
-
239
- protected
240
-
241
- def extract_result(output)
242
- status, value, stack = output
243
- if status == "ok"
244
- value
245
- else
246
- stack ||= ""
247
- stack = stack.split("\n").map do |line|
248
- line.sub(" at ", "").strip
249
- end
250
- stack.reject! { |line| ["eval code", "eval@[native code]"].include?(line) }
251
- stack.shift unless stack[0].to_s.include?("(execjs)")
252
- error_class = value =~ /SyntaxError:/ ? ExecJS::RuntimeError : ExecJS::ProgramError
253
- error = error_class.new(value)
254
- error.set_backtrace(stack + caller)
255
- raise error
256
- end
257
- end
258
- end
259
-
260
- attr_reader :name, :vm
261
-
262
- def initialize(options)
263
- @name = options[:name]
264
- @binary = Isomorfeus::Speednode::NodeCommand.cached(options[:command])
265
- @runner_path = options[:runner_path]
266
- @encoding = options[:encoding]
267
- @deprecated = !!options[:deprecated]
268
-
269
- @vm = VM.new(
270
- binary: @binary,
271
- runner_path: @runner_path
272
- )
273
-
274
- @popen_options = {}
275
- @popen_options[:external_encoding] = @encoding if @encoding
276
- @popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
277
- end
278
-
279
- def available?
280
- @binary ? true : false
281
- end
282
-
283
- def deprecated?
284
- @deprecated
285
- end
286
- end
287
- end
288
- end
1
+ module Isomorfeus
2
+ module Speednode
3
+ class Runtime < ExecJS::Runtime
4
+ def self.attach_proc(context_id, func, run_block)
5
+ attached_procs[context_id] = { func => run_block }
6
+ end
7
+
8
+ def self.attached_procs
9
+ @attached_procs ||= {}
10
+ end
11
+
12
+ def self.responders
13
+ @responders ||= {}
14
+ end
15
+
16
+ attr_reader :name, :vm
17
+
18
+ def initialize(options)
19
+ @name = options[:name]
20
+ @binary = Isomorfeus::Speednode::NodeCommand.cached(options[:command])
21
+ @runner_path = options[:runner_path]
22
+ @encoding = options[:encoding]
23
+ @deprecated = !!options[:deprecated]
24
+
25
+ @vm = VM.new(
26
+ binary: @binary,
27
+ source_maps: '--enable-source-maps',
28
+ runner_path: @runner_path
29
+ )
30
+
31
+ @popen_options = {}
32
+ @popen_options[:external_encoding] = @encoding if @encoding
33
+ @popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
34
+ end
35
+
36
+ def available?
37
+ @binary ? true : false
38
+ end
39
+
40
+ def deprecated?
41
+ @deprecated
42
+ end
43
+ end
44
+ end
45
+ end