isomorfeus-speednode 0.3.1 → 0.4.3

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 = 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