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.
@@ -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