isomorfeus-speednode 0.2.12 → 0.4.2

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,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 = 20
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
@@ -1,213 +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, mode: :strict)})"
128
- end
129
-
130
- def eval(source, options = {})
131
- raw_exec("(#{source})") if /\S/ =~ source
132
- end
133
-
134
- def exec(source, options = {})
135
- raw_exec("(function(){#{source}})()")
136
- end
137
-
138
- def permissive?
139
- @permissive
140
- end
141
-
142
- def permissive_eval(source, options = {})
143
- raw_execp("(#{source})") if /\S/ =~ source
144
- end
145
-
146
- def permissive_exec(source, options = {})
147
- raw_execp("(function(){#{source}})()")
148
- end
149
-
150
- def raw_exec(source)
151
- source = encode(source)
152
-
153
- result = @runtime.vm.exec(@uuid, source)
154
- extract_result(result)
155
- end
156
-
157
- def raw_execp(source)
158
- source = encode(source)
159
-
160
- result = @runtime.vm.execp(@uuid, source)
161
- extract_result(result)
162
- end
163
-
164
- protected
165
-
166
- def extract_result(output)
167
- status, value, stack = output
168
- if status == "ok"
169
- value
170
- else
171
- stack ||= ""
172
- stack = stack.split("\n").map do |line|
173
- line.sub(" at ", "").strip
174
- end
175
- stack.reject! { |line| ["eval code", "eval@[native code]"].include?(line) }
176
- stack.shift unless stack[0].to_s.include?("(execjs)")
177
- error_class = value =~ /SyntaxError:/ ? ExecJS::RuntimeError : ExecJS::ProgramError
178
- error = error_class.new(value)
179
- error.set_backtrace(stack + caller)
180
- raise error
181
- end
182
- end
183
- end
184
-
185
- attr_reader :name, :vm
186
-
187
- def initialize(options)
188
- @name = options[:name]
189
- @binary = Isomorfeus::Speednode::NodeCommand.cached(options[:command])
190
- @runner_path = options[:runner_path]
191
- @encoding = options[:encoding]
192
- @deprecated = !!options[:deprecated]
193
-
194
- @vm = VM.new(
195
- binary: @binary,
196
- runner_path: @runner_path
197
- )
198
-
199
- @popen_options = {}
200
- @popen_options[:external_encoding] = @encoding if @encoding
201
- @popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
202
- end
203
-
204
- def available?
205
- @binary ? true : false
206
- end
207
-
208
- def deprecated?
209
- @deprecated
210
- end
211
- end
212
- end
213
- 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