speednode 0.8.1

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,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: []