speednode 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,223 @@
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 Speednode
41
+ class Runtime < ExecJS::Runtime
42
+ class VM
43
+ def self.finalize(socket, socket_dir, socket_path, pid)
44
+ proc do
45
+ ::Speednode::Runtime.responders[socket_path].kill if ::Speednode::Runtime.responders[socket_path]
46
+ exit_node(socket, socket_dir, socket_path, pid)
47
+ end
48
+ end
49
+
50
+ def self.exit_node(socket, socket_dir, socket_path, pid)
51
+ VMCommand.new(socket, "exit", 0).execute rescue nil
52
+ socket.close
53
+ File.unlink(socket_path) if File.exist?(socket_path)
54
+ Dir.rmdir(socket_dir) if socket_dir && Dir.exist?(socket_dir)
55
+ if Gem.win_platform?
56
+ # SIGINT or SIGKILL are unreliable on Windows, try native taskkill first
57
+ unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
58
+ Process.kill('KILL', pid) rescue nil
59
+ end
60
+ else
61
+ Process.kill('KILL', pid) rescue nil
62
+ end
63
+ rescue
64
+ nil
65
+ end
66
+
67
+ attr_reader :responder
68
+
69
+ def initialize(options)
70
+ @mutex = ::Thread::Mutex.new
71
+ @socket_path = nil
72
+ @options = options
73
+ @started = false
74
+ @socket = nil
75
+ end
76
+
77
+ def started?
78
+ @started
79
+ end
80
+
81
+ def evsc(context, key)
82
+ command('evsc', { 'context' => context, 'key' => key })
83
+ end
84
+
85
+ def scsc(context, key, source)
86
+ command('scsc', { 'context' => context, 'key' => key, 'source' => source })
87
+ end
88
+
89
+ def eval(context, source)
90
+ command('eval', {'context' => context, 'source' => source})
91
+ end
92
+
93
+ def exec(context, source)
94
+ command('exec', {'context' => context, 'source' => source})
95
+ end
96
+
97
+ def bench(context, source)
98
+ command('bench', {'context' => context, 'source' => source})
99
+ end
100
+
101
+ def create(context, source, options)
102
+ command('create', {'context' => context, 'source' => source, 'options' => options})
103
+ end
104
+
105
+ def created(context, source, options)
106
+ command('created', {'context' => context, 'source' => source, 'options' => options})
107
+ end
108
+
109
+ def createp(context, source, options)
110
+ command('createp', {'context' => context, 'source' => source, 'options' => options})
111
+ end
112
+
113
+ def attach(context, func)
114
+ create_responder(context) unless responder
115
+ command('attach', {'context' => context, 'func' => func })
116
+ end
117
+
118
+ def delete_context(context)
119
+ @mutex.synchronize do
120
+ VMCommand.new(@socket, "deleteContext", context).execute rescue nil
121
+ end
122
+ rescue ThreadError
123
+ nil
124
+ end
125
+
126
+ # def context_options(context)
127
+ # command('ctxo', {'context' => context })
128
+ # end
129
+
130
+ def start
131
+ @mutex.synchronize do
132
+ start_without_synchronization
133
+ end
134
+ end
135
+
136
+ def stop
137
+ return unless @started
138
+ @mutex.synchronize do
139
+ self.class.exit_node(@socket, @socket_dir, @socket_path, @pid)
140
+ @socket_path = nil
141
+ @started = false
142
+ @socket = nil
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def start_without_synchronization
149
+ return if @started
150
+ if ExecJS.windows?
151
+ @socket_dir = nil
152
+ @socket_path = SecureRandom.uuid
153
+ else
154
+ @socket_dir = Dir.mktmpdir("iso-speednode-")
155
+ @socket_path = File.join(@socket_dir, "socket")
156
+ end
157
+ @pid = Process.spawn({"SOCKET_PATH" => @socket_path}, @options[:binary], '--expose-gc', @options[:source_maps], @options[:runner_path])
158
+
159
+ retries = 500
160
+
161
+ if ExecJS.windows?
162
+ timeout_or_connected = false
163
+ begin
164
+ retries -= 1
165
+ begin
166
+ @socket = Win32::Pipe::Client.new(@socket_path, Win32::Pipe::ACCESS_DUPLEX)
167
+ rescue
168
+ sleep 0.1
169
+ raise "Unable to start nodejs process in time" if retries == 0
170
+ next
171
+ end
172
+ timeout_or_connected = true
173
+ end until timeout_or_connected
174
+ else
175
+ while !File.exist?(@socket_path)
176
+ sleep 0.1
177
+ retries -= 1
178
+ raise "Unable to start nodejs process in time" if retries == 0
179
+ end
180
+
181
+ @socket = UNIXSocket.new(@socket_path)
182
+ end
183
+
184
+ @started = true
185
+
186
+ Kernel.at_exit { self.class.finalize(@socket, @socket_dir, @socket_path, @pid).call }
187
+ end
188
+
189
+ def create_responder(context)
190
+ start unless @started
191
+ run_block = Proc.new do |request|
192
+ args = ::Oj.load(request.chop!, mode: :strict)
193
+ req_context = args[0]
194
+ method = args[1]
195
+ method_args = args[2]
196
+ begin
197
+ result = ::Speednode::Runtime.attached_procs[req_context][method].call(*method_args)
198
+ ::Oj.dump(['ok', result], mode: :strict)
199
+ rescue Exception => err
200
+ ::Oj.dump(['err', err.class.to_s, [err.message].concat(err.backtrace)], mode: :strict)
201
+ end
202
+ end
203
+ responder_path = @socket_path + '_responder'
204
+ @responder = Thread.new do
205
+ if ExecJS.windows?
206
+ ::Speednode::AttachPipe.new(responder_path, run_block).run
207
+ else
208
+ ::Speednode::AttachSocket.new(responder_path, run_block).run
209
+ end
210
+ end
211
+ ::Speednode::Runtime.responders[@socket_path] = @responder_thread
212
+ @responder.run
213
+ end
214
+
215
+ def command(cmd, argument)
216
+ @mutex.synchronize do
217
+ start_without_synchronization unless @started
218
+ VMCommand.new(@socket, cmd, argument).execute
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,38 @@
1
+ module Speednode
2
+ class Runtime < ExecJS::Runtime
3
+ class VMCommand
4
+ def initialize(socket, cmd, arguments)
5
+ @socket = socket
6
+ @cmd = cmd
7
+ @arguments = arguments
8
+ end
9
+
10
+ def execute
11
+ result = ''
12
+ message = ::Oj.dump({ 'cmd' => @cmd, 'args' => @arguments }, mode: :strict)
13
+ message = message + "\x04"
14
+ bytes_to_send = message.bytesize
15
+ sent_bytes = 0
16
+
17
+ if ::ExecJS.windows?
18
+ @socket.write(message)
19
+ begin
20
+ result << @socket.read
21
+ end until result.end_with?("\x04")
22
+ else
23
+ sent_bytes = @socket.sendmsg(message)
24
+ if sent_bytes < bytes_to_send
25
+ while sent_bytes < bytes_to_send
26
+ sent_bytes += @socket.sendmsg(message.byteslice((sent_bytes)..-1))
27
+ end
28
+ end
29
+
30
+ begin
31
+ result << @socket.recvmsg()[0]
32
+ end until result.end_with?("\x04")
33
+ end
34
+ ::Oj.load(result.chop!, mode: :strict)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ module Speednode
2
+ class Runtime < ExecJS::Runtime
3
+ def self.attach_proc(context_id, func, run_block)
4
+ attached_procs[context_id] = { func => run_block }
5
+ end
6
+
7
+ def self.attached_procs
8
+ @attached_procs ||= {}
9
+ end
10
+
11
+ def self.responders
12
+ @responders ||= {}
13
+ end
14
+
15
+ attr_reader :name, :vm
16
+
17
+ def initialize(options)
18
+ @name = options[:name]
19
+ @binary = ::Speednode::NodeCommand.cached(options[:command])
20
+ @runner_path = options[:runner_path]
21
+ @vm = VM.new(binary: @binary, source_maps: '--enable-source-maps', runner_path: @runner_path)
22
+ @encoding = options[:encoding]
23
+ @deprecated = !!options[:deprecated]
24
+ @popen_options = {}
25
+ @popen_options[:external_encoding] = @encoding if @encoding
26
+ @popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
27
+ @contexts = {}
28
+ end
29
+
30
+ def register_context(uuid, context)
31
+ @contexts[uuid] = context
32
+ end
33
+
34
+ def unregister_context(uuid)
35
+ context = @contexts.delete(uuid)
36
+ if context && @vm
37
+ ObjectSpace.undefine_finalizer(context)
38
+ @vm.delete_context(uuid) rescue nil # if delete_context fails, the vm exited before probably
39
+ end
40
+ @vm.stop if @contexts.size == 0 && @vm.started?
41
+ end
42
+
43
+ def stop_context(context)
44
+ unregister_context(context.instance_variable_get(:@uuid))
45
+ end
46
+
47
+ def available?
48
+ @binary ? true : false
49
+ end
50
+
51
+ def deprecated?
52
+ @deprecated
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module Speednode
2
+ VERSION = '0.8.1'
3
+ end
data/lib/speednode.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'securerandom'
2
+ require 'socket'
3
+ require 'oj'
4
+ require 'execjs/module'
5
+ require 'speednode/config'
6
+ require 'speednode/node_command'
7
+ require 'execjs/runtime'
8
+ if Gem.win_platform?
9
+ require 'speednode/attach_pipe'
10
+ else
11
+ require 'speednode/attach_socket'
12
+ end
13
+ require 'speednode/runtime/vm'
14
+ require 'speednode/runtime/vm_command'
15
+ require 'speednode/runtime/context'
16
+ require 'speednode/runtime'
17
+ require 'execjs/runtimes'
18
+ require 'speednode/execjs_runtime'
19
+ require 'speednode/execjs_module'
20
+ require 'speednode/execjs_runtimes'
21
+ require 'execjs'
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: speednode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.1
5
+ platform: ruby
6
+ authors:
7
+ - Jan Biedermann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: execjs
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.9.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.9.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 3.13.23
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: 3.17.0
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.13.23
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: 3.17.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: win32-pipe
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.4.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.4.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: minitest
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 5.20.0
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 5.20.0
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 13.1.0
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 13.1.0
103
+ - !ruby/object:Gem::Dependency
104
+ name: uglifier
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description: A fast ExecJS runtime based on nodejs. As fast as mini_racer, but without
118
+ the need to compile the js engine, because it uses the system provided nodejs.
119
+ email: jan@kursator.de
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - LICENSE
125
+ - README.md
126
+ - lib/speednode.rb
127
+ - lib/speednode/attach_pipe.rb
128
+ - lib/speednode/attach_socket.rb
129
+ - lib/speednode/config.rb
130
+ - lib/speednode/execjs_module.rb
131
+ - lib/speednode/execjs_runtime.rb
132
+ - lib/speednode/execjs_runtimes.rb
133
+ - lib/speednode/node_command.rb
134
+ - lib/speednode/runner.js
135
+ - lib/speednode/runtime.rb
136
+ - lib/speednode/runtime/context.rb
137
+ - lib/speednode/runtime/vm.rb
138
+ - lib/speednode/runtime/vm_command.rb
139
+ - lib/speednode/version.rb
140
+ homepage: https://github.com/janbiedermann/speednode
141
+ licenses:
142
+ - MIT
143
+ metadata: {}
144
+ post_install_message:
145
+ rdoc_options: []
146
+ require_paths:
147
+ - lib
148
+ required_ruby_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: 3.1.0
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ requirements: []
159
+ rubygems_version: 3.5.3
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: A fast ExecJS runtime based on nodejs.
163
+ test_files: []