isomorfeus-speednode 0.2.10 → 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,217 +1,45 @@
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
- @socket.sendmsg(message + "\x04")
15
- begin
16
- result << @socket.recvmsg()[0]
17
- end until result.end_with?("\x04")
18
- ::Oj.load(result.chop!, create_additions: false)
19
- end
20
- end
21
-
22
- class VM
23
- def initialize(options)
24
- @mutex = Mutex.new
25
- @socket_path = nil
26
- @options = options
27
- @started = false
28
- @socket = nil
29
- end
30
-
31
- def started?
32
- @started
33
- end
34
-
35
- def self.finalize(socket, socket_dir, socket_path, pid)
36
- proc { exit_node(socket, socket_dir, socket_path, pid) }
37
- end
38
-
39
- def self.exit_node(socket, socket_dir, socket_path, pid)
40
- VMCommand.new(socket, "exit", [0]).execute
41
- socket.close
42
- File.unlink(socket_path)
43
- Dir.rmdir(socket_dir)
44
- Process.kill('KILL', pid)
45
- end
46
-
47
- def exec(context, source)
48
- command("exec", {'context' => context, 'source' => source})
49
- end
50
-
51
- def execp(context, source)
52
- command("execp", {'context' => context, 'source' => source})
53
- end
54
-
55
- def delete_context(context)
56
- command("deleteContext", context)
57
- end
58
-
59
- def start
60
- @mutex.synchronize do
61
- start_without_synchronization
62
- end
63
- end
64
-
65
- private
66
-
67
- def start_without_synchronization
68
- return if @started
69
- @socket_dir = Dir.mktmpdir("isomorfeus-speednode-")
70
- @socket_path = File.join(@socket_dir, "socket")
71
- @pid = Process.spawn({"SOCKET_PATH" => @socket_path}, @options[:binary], @options[:runner_path])
72
-
73
- retries = 20
74
- while !File.exist?(@socket_path)
75
- sleep 0.05
76
- retries -= 1
77
-
78
- if retries == 0
79
- raise "Unable to start nodejs process in time"
80
- end
81
- end
82
-
83
- @socket = UNIXSocket.new(@socket_path)
84
- @started = true
85
-
86
- at_exit do
87
- begin
88
- self.class.exit_node(@socket, @socket_dir, @socket_path, @pid) unless @socket.closed?
89
- rescue
90
- # do nothing
91
- end
92
- end
93
-
94
- ObjectSpace.define_finalizer(self, self.class.finalize(@socket, @socket_dir, @socket_path, @pid))
95
- end
96
-
97
- def command(cmd, *arguments)
98
- @mutex.synchronize do
99
- start_without_synchronization
100
- VMCommand.new(@socket, cmd, arguments).execute
101
- end
102
- end
103
- end
104
-
105
- class Context < ::ExecJS::Runtime::Context
106
- def initialize(runtime, source = "", options = {})
107
- @runtime = runtime
108
- @uuid = SecureRandom.uuid
109
- @permissive = !!options[:permissive]
110
-
111
- ObjectSpace.define_finalizer(self, self.class.finalize(@runtime, @uuid))
112
-
113
- begin
114
- source = encode(source)
115
- rescue
116
- source = source.force_encoding('UTF-8')
117
- end
118
-
119
- @permissive ? raw_execp(source) : raw_exec(source)
120
- end
121
-
122
- def self.finalize(runtime, uuid)
123
- proc { runtime.vm.delete_context(uuid) }
124
- end
125
-
126
- def call(identifier, *args)
127
- eval "#{identifier}.apply(this, #{::Oj.dump(args)})"
128
- end
129
-
130
- def eval(source, options = {})
131
- if /\S/ =~ source
132
- raw_exec("(#{source})")
133
- end
134
- end
135
-
136
- def exec(source, options = {})
137
- raw_exec("(function(){#{source}})()")
138
- end
139
-
140
- def permissive?
141
- @permissive
142
- end
143
-
144
- def permissive_eval(source, options = {})
145
- if /\S/ =~ source
146
- raw_execp("(#{source})")
147
- end
148
- end
149
-
150
- def permissive_exec(source, options = {})
151
- raw_execp("(function(){#{source}})()")
152
- end
153
-
154
- def raw_exec(source)
155
- source = encode(source)
156
-
157
- result = @runtime.vm.exec(@uuid, source)
158
- extract_result(result)
159
- end
160
-
161
- def raw_execp(source)
162
- source = encode(source)
163
-
164
- result = @runtime.vm.execp(@uuid, source)
165
- extract_result(result)
166
- end
167
-
168
- protected
169
-
170
- def extract_result(output)
171
- status, value, stack = output
172
- if status == "ok"
173
- value
174
- else
175
- stack ||= ""
176
- stack = stack.split("\n").map do |line|
177
- line.sub(" at ", "").strip
178
- end
179
- stack.reject! { |line| ["eval code", "eval@[native code]"].include?(line) }
180
- stack.shift unless stack[0].to_s.include?("(execjs)")
181
- error_class = value =~ /SyntaxError:/ ? ExecJS::RuntimeError : ExecJS::ProgramError
182
- error = error_class.new(value)
183
- error.set_backtrace(stack + caller)
184
- raise error
185
- end
186
- end
187
- end
188
-
189
- attr_reader :name, :vm
190
-
191
- def initialize(options)
192
- @name = options[:name]
193
- @binary = Isomorfeus::Speednode::NodeCommand.cached(options[:command])
194
- @runner_path = options[:runner_path]
195
- @encoding = options[:encoding]
196
- @deprecated = !!options[:deprecated]
197
-
198
- @vm = VM.new(
199
- binary: @binary,
200
- runner_path: @runner_path
201
- )
202
-
203
- @popen_options = {}
204
- @popen_options[:external_encoding] = @encoding if @encoding
205
- @popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
206
- end
207
-
208
- def available?
209
- @binary ? true : false
210
- end
211
-
212
- def deprecated?
213
- @deprecated
214
- end
215
- end
216
- end
217
- 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