pd4ml 1.0.0 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +4 -9
- data/docs/guide.md +780 -782
- data/lib/pd4ml/bridge.rb +362 -362
- data/lib/pd4ml/version.rb +5 -5
- metadata +1 -1
data/lib/pd4ml/bridge.rb
CHANGED
|
@@ -1,362 +1,362 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'monitor'
|
|
5
|
-
require 'open3'
|
|
6
|
-
require 'socket'
|
|
7
|
-
require 'uri'
|
|
8
|
-
require 'pathname'
|
|
9
|
-
|
|
10
|
-
module Pd4ml
|
|
11
|
-
# An OutputStream argument whose content comes back to Ruby (see Bridge#request).
|
|
12
|
-
Output = Struct.new(:index)
|
|
13
|
-
|
|
14
|
-
# The connection to the Java side: a bridge process (pd4ml-bridge.jar, started on first use) or
|
|
15
|
-
# a bridge server (`pd4ml serve`, PD4ML_BRIDGE=tcp://host:port). One shared bridge serves the
|
|
16
|
-
# whole process; it is thread-safe, and a callback may call PD4ML itself.
|
|
17
|
-
class Bridge
|
|
18
|
-
BRIDGE_JAR = File.expand_path('pd4ml-bridge.jar', __dir__)
|
|
19
|
-
|
|
20
|
-
@options = {}
|
|
21
|
-
@default = nil
|
|
22
|
-
@lock = Monitor.new
|
|
23
|
-
|
|
24
|
-
class << self
|
|
25
|
-
# Sets up the shared bridge; call before the first use of PD4ML.
|
|
26
|
-
# Options: classpath (Array), java, jvm_options (Array), server ('tcp://127.0.0.1:8723'), log_file.
|
|
27
|
-
def configure(**options)
|
|
28
|
-
@lock.synchronize do
|
|
29
|
-
raise BridgeError, 'the PD4ML bridge runs already: configure before the first use' if @default
|
|
30
|
-
@options = @options.merge(options)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# The shared bridge, started on first use.
|
|
35
|
-
def get
|
|
36
|
-
@lock.synchronize do
|
|
37
|
-
unless @at_exit
|
|
38
|
-
@at_exit = true
|
|
39
|
-
at_exit { shutdown } # Java ends with the Ruby process, before it exits
|
|
40
|
-
end
|
|
41
|
-
@default ||= new(**@options)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def started?
|
|
46
|
-
@lock.synchronize { !@default.nil? }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Stops the shared bridge (it starts again on the next use).
|
|
50
|
-
def shutdown
|
|
51
|
-
@lock.synchronize do
|
|
52
|
-
@default&.close
|
|
53
|
-
@default = nil
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def forget(bridge)
|
|
58
|
-
@lock.synchronize { @default = nil if @default.equal?(bridge) }
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# The classpath the bridge starts with: configured, from the environment, or the installed build.
|
|
62
|
-
def classpath(options = @options)
|
|
63
|
-
entries = options[:classpath]
|
|
64
|
-
entries ||= ENV['PD4ML_CLASSPATH'].split(File::PATH_SEPARATOR) if ENV['PD4ML_CLASSPATH'].to_s != ''
|
|
65
|
-
entries ||= File.read(ENV['PD4ML_CLASSPATH_FILE']).split(/\r?\n/) if ENV['PD4ML_CLASSPATH_FILE'].to_s != ''
|
|
66
|
-
unless entries
|
|
67
|
-
entries = Installer.installed_classpath
|
|
68
|
-
if entries.empty? && Installer.auto_install?
|
|
69
|
-
warn 'pd4ml: no PD4ML build installed yet: downloading the most recent release ' \
|
|
70
|
-
"(once; 'pd4ml install' updates it, PD4ML_AUTO_INSTALL=0 turns this off)"
|
|
71
|
-
Installer.install
|
|
72
|
-
entries = Installer.installed_classpath
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
entries.map(&:strip).reject(&:empty?).flat_map do |e|
|
|
76
|
-
if e.end_with?('/*', '\\*')
|
|
77
|
-
Dir.glob(File.join(e[0..-3].tr('\\', '/'), '*.jar')).sort
|
|
78
|
-
else
|
|
79
|
-
[e]
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# The java executable: PD4ML_JAVA, JAVA_HOME/bin/java, else `java`.
|
|
85
|
-
def java(options = @options)
|
|
86
|
-
return options[:java] if options[:java]
|
|
87
|
-
return ENV['PD4ML_JAVA'] if ENV['PD4ML_JAVA'].to_s != ''
|
|
88
|
-
|
|
89
|
-
home = ENV['JAVA_HOME'].to_s
|
|
90
|
-
unless home.empty?
|
|
91
|
-
exe = File.join(home, 'bin', Gem.win_platform? ? 'java.exe' : 'java')
|
|
92
|
-
return exe if File.exist?(exe)
|
|
93
|
-
end
|
|
94
|
-
'java'
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# The command line that runs the bridge (with a port: a bridge server).
|
|
98
|
-
def command(options = @options, port: nil)
|
|
99
|
-
cp = classpath(options)
|
|
100
|
-
if cp.empty?
|
|
101
|
-
raise BridgeError, "PD4ML not found: install a build with 'pd4ml install' (or Pd4ml.install), or point at " \
|
|
102
|
-
'one with Pd4ml.configure(classpath: [...]), PD4ML_CLASSPATH or PD4ML_CLASSPATH_FILE'
|
|
103
|
-
end
|
|
104
|
-
args = [java(options), '-Djava.awt.headless=true', *Array(options[:jvm_options])]
|
|
105
|
-
args.concat(ENV['PD4ML_JVM_OPTIONS'].split) if ENV['PD4ML_JVM_OPTIONS'].to_s != ''
|
|
106
|
-
args.push('-cp', [BRIDGE_JAR, *cp].join(File::PATH_SEPARATOR), 'com.pd4ml.bridge.Bridge')
|
|
107
|
-
args.push('--port', port.to_s) if port
|
|
108
|
-
args
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def initialize(**options)
|
|
113
|
-
@lock = Monitor.new
|
|
114
|
-
@next_id = 1
|
|
115
|
-
@next_callback = 1
|
|
116
|
-
@callbacks = {}
|
|
117
|
-
@pending = {}
|
|
118
|
-
@released = Queue.new
|
|
119
|
-
@callback_error = nil
|
|
120
|
-
@closed = false
|
|
121
|
-
server = options[:server] || ENV['PD4ML_BRIDGE']
|
|
122
|
-
if server.to_s != ''
|
|
123
|
-
uri = URI(server)
|
|
124
|
-
begin
|
|
125
|
-
@socket = TCPSocket.new(uri.host, uri.port)
|
|
126
|
-
rescue SystemCallError => e
|
|
127
|
-
raise BridgeError, "cannot connect to the PD4ML bridge at #{server}: #{e.message}"
|
|
128
|
-
end
|
|
129
|
-
@in = @out = @socket
|
|
130
|
-
else
|
|
131
|
-
cmd = self.class.command(options)
|
|
132
|
-
begin
|
|
133
|
-
@in, @out, err, @thread = Open3.popen3(*cmd)
|
|
134
|
-
rescue SystemCallError => e
|
|
135
|
-
raise BridgeError, "cannot start java (#{cmd.first}): #{e.message}"
|
|
136
|
-
end
|
|
137
|
-
log = options[:log_file] || ENV['PD4ML_BRIDGE_LOG']
|
|
138
|
-
@stderr_tail = +''
|
|
139
|
-
# drained either way, so Java never blocks on a full pipe
|
|
140
|
-
@stderr_reader = Thread.new do
|
|
141
|
-
File.open(log, 'a') do |f|
|
|
142
|
-
err.each_line { |l| f.write(l); keep_tail(l) }
|
|
143
|
-
end if log
|
|
144
|
-
err.each_line { |l| keep_tail(l) } unless log
|
|
145
|
-
rescue IOError
|
|
146
|
-
nil
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
@in.binmode
|
|
150
|
-
@out.binmode
|
|
151
|
-
@in.sync = true
|
|
152
|
-
hello = request({ 'op' => 'hello' })
|
|
153
|
-
raise BridgeError, 'incompatible PD4ML bridge protocol' unless hello.is_a?(Hash) && hello['protocol'] == 1
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def close
|
|
157
|
-
@lock.synchronize do
|
|
158
|
-
return if @closed
|
|
159
|
-
|
|
160
|
-
@closed = true
|
|
161
|
-
begin
|
|
162
|
-
@in.write("{\"id\":0,\"op\":\"close\"}\n")
|
|
163
|
-
@in.close unless @socket
|
|
164
|
-
rescue IOError, SystemCallError
|
|
165
|
-
nil
|
|
166
|
-
end
|
|
167
|
-
@socket&.close
|
|
168
|
-
# let Java finish (it exits on 'close' or end of input), so it never outlives Ruby
|
|
169
|
-
if @thread && !@thread.join(5)
|
|
170
|
-
begin
|
|
171
|
-
Process.kill(:KILL, @thread.pid)
|
|
172
|
-
rescue SystemCallError
|
|
173
|
-
nil
|
|
174
|
-
end
|
|
175
|
-
@thread.join(5)
|
|
176
|
-
end
|
|
177
|
-
@stderr_reader&.join(1)
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# ------------------------------------------------------------------------------- protocol
|
|
182
|
-
|
|
183
|
-
# Sends one request and returns its decoded result; callbacks run while it waits. With `outs`
|
|
184
|
-
# (an Array), the content of Output arguments is stored there (binary Strings).
|
|
185
|
-
def request(req, outs = nil)
|
|
186
|
-
response = @lock.synchronize do
|
|
187
|
-
raise BridgeError, 'the PD4ML bridge is closed' if @closed
|
|
188
|
-
|
|
189
|
-
refs = []
|
|
190
|
-
refs << @released.pop until @released.empty?
|
|
191
|
-
exchange({ 'op' => 'release', 'refs' => refs }) unless refs.empty?
|
|
192
|
-
exchange(req)
|
|
193
|
-
end
|
|
194
|
-
raise JavaError.from(response['error']) if response['error']
|
|
195
|
-
|
|
196
|
-
if outs && response['out'].is_a?(Array)
|
|
197
|
-
response['out'].each_with_index { |data, i| outs[i] = data&.unpack1('m0') }
|
|
198
|
-
end
|
|
199
|
-
if (e = @callback_error)
|
|
200
|
-
@callback_error = nil
|
|
201
|
-
begin
|
|
202
|
-
raise e
|
|
203
|
-
rescue StandardError
|
|
204
|
-
raise CallbackError, "a callback failed: #{e.message}"
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
decode(response['result'])
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# ----------------------------------------------------------------------------- operations
|
|
211
|
-
|
|
212
|
-
def new_object(cls, args) = request({ 'op' => 'new', 'class' => cls, 'args' => args })
|
|
213
|
-
|
|
214
|
-
def call(target, method, args, outs = nil)
|
|
215
|
-
request({ 'op' => 'call', 'ref' => handle_of(target), 'method' => method, 'args' => args }, outs)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def call_static(cls, method, args, outs = nil)
|
|
219
|
-
request({ 'op' => 'static', 'class' => cls, 'method' => method, 'args' => args }, outs)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def get_field(target, field)
|
|
223
|
-
if target.is_a?(String)
|
|
224
|
-
request({ 'op' => 'get', 'class' => target, 'field' => field })
|
|
225
|
-
else
|
|
226
|
-
request({ 'op' => 'get', 'ref' => handle_of(target), 'field' => field })
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def set_field(target, field, value)
|
|
231
|
-
if target.is_a?(String)
|
|
232
|
-
request({ 'op' => 'set', 'class' => target, 'field' => field, 'value' => value })
|
|
233
|
-
else
|
|
234
|
-
request({ 'op' => 'set', 'ref' => handle_of(target), 'field' => field, 'value' => value })
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def describe(cls) = request({ 'op' => 'describe', 'class' => cls })
|
|
239
|
-
|
|
240
|
-
def instance_of?(target, cls)
|
|
241
|
-
request({ 'op' => 'instanceof', 'ref' => handle_of(target), 'class' => cls }) == true
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
# A Java object implementing `iface`; its methods call the block with (method, args).
|
|
245
|
-
def proxy(iface, &block)
|
|
246
|
-
id = @lock.synchronize { (@next_callback += 1) - 1 }
|
|
247
|
-
@callbacks[id] = block
|
|
248
|
-
request({ 'op' => 'proxy', 'interface' => iface, 'callback' => id })
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Called by JavaObject finalizers; released with the next request.
|
|
252
|
-
def release(handle)
|
|
253
|
-
@released << handle unless @closed
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# ------------------------------------------------------------------------------- values
|
|
257
|
-
|
|
258
|
-
def encode(v)
|
|
259
|
-
case v
|
|
260
|
-
when JavaObject then { '$ref' => v.__handle }
|
|
261
|
-
when Output then { '$out' => v.index }
|
|
262
|
-
when PD4ML then { '$ref' => v.java.__handle }
|
|
263
|
-
when String
|
|
264
|
-
v.encoding == Encoding::BINARY || !v.valid_encoding? ? { '$bytes' => [v].pack('m0') } : v
|
|
265
|
-
when Symbol then v.to_s
|
|
266
|
-
when Pathname, URI::Generic then v.to_s
|
|
267
|
-
when Hash then v.to_h { |k, x| [k.to_s, encode(x)] }
|
|
268
|
-
when Array then v.map { |x| encode(x) }
|
|
269
|
-
when nil, true, false, Integer then v
|
|
270
|
-
when Float
|
|
271
|
-
raise ArgumentError, "not a finite number: #{v}" unless v.finite?
|
|
272
|
-
|
|
273
|
-
v
|
|
274
|
-
else
|
|
275
|
-
if v.respond_to?(:read)
|
|
276
|
-
data = v.read
|
|
277
|
-
{ '$bytes' => [data.to_s].pack('m0') }
|
|
278
|
-
elsif v.respond_to?(:to_str)
|
|
279
|
-
v.to_str
|
|
280
|
-
else
|
|
281
|
-
raise ArgumentError, "cannot pass #{v.class} to Java"
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def decode(v)
|
|
287
|
-
case v
|
|
288
|
-
when Array then v.map { |x| decode(x) }
|
|
289
|
-
when Hash
|
|
290
|
-
if v.key?('$ref')
|
|
291
|
-
JavaObject.new(self, v['$ref'], v['class'], v['enum'])
|
|
292
|
-
elsif v.key?('$bytes')
|
|
293
|
-
v['$bytes'].unpack1('m0')
|
|
294
|
-
else
|
|
295
|
-
v.transform_values { |x| decode(x) }
|
|
296
|
-
end
|
|
297
|
-
else
|
|
298
|
-
v
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
private
|
|
303
|
-
|
|
304
|
-
def keep_tail(line)
|
|
305
|
-
@stderr_tail = (@stderr_tail + line)[-4000..] || (@stderr_tail + line)
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def handle_of(target)
|
|
309
|
-
case target
|
|
310
|
-
when JavaObject then target.__handle
|
|
311
|
-
when PD4ML then target.java.__handle
|
|
312
|
-
else raise ArgumentError, "not a Java object: #{target.inspect}"
|
|
313
|
-
end
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def exchange(req)
|
|
317
|
-
id = @next_id
|
|
318
|
-
@next_id += 1
|
|
319
|
-
line = JSON.generate(encode(req.merge('id' => id)))
|
|
320
|
-
begin
|
|
321
|
-
@in.write(line, "\n")
|
|
322
|
-
rescue IOError, SystemCallError => e
|
|
323
|
-
gone("the PD4ML bridge is gone (#{e.message})")
|
|
324
|
-
end
|
|
325
|
-
loop do
|
|
326
|
-
return @pending.delete(id) if @pending.key?(id)
|
|
327
|
-
|
|
328
|
-
raw = @out.gets
|
|
329
|
-
gone('the PD4ML bridge closed the connection') if raw.nil?
|
|
330
|
-
next if raw.strip.empty?
|
|
331
|
-
|
|
332
|
-
msg = JSON.parse(raw.force_encoding(Encoding::UTF_8))
|
|
333
|
-
if msg.key?('callback')
|
|
334
|
-
dispatch(msg)
|
|
335
|
-
elsif msg['id'] == id
|
|
336
|
-
return msg
|
|
337
|
-
else
|
|
338
|
-
@pending[msg['id']] = msg # an outer request's response, during a callback
|
|
339
|
-
end
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def dispatch(msg)
|
|
344
|
-
callback = @callbacks[msg['callback']]
|
|
345
|
-
return if callback.nil? || @callback_error
|
|
346
|
-
|
|
347
|
-
begin
|
|
348
|
-
callback.call(msg['method'], Array(msg['args']).map { |a| decode(a) })
|
|
349
|
-
rescue StandardError => e
|
|
350
|
-
@callback_error = e # raised by the request that caused the callback
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def gone(why)
|
|
355
|
-
@closed = true
|
|
356
|
-
self.class.forget(self)
|
|
357
|
-
detail = @stderr_tail.to_s.strip
|
|
358
|
-
raise BridgeError, why + (detail.empty? ? '' : "\n#{detail}") +
|
|
359
|
-
(ENV['PD4ML_BRIDGE_LOG'] ? '' : ' (PD4ML_BRIDGE_LOG=file keeps its output)')
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'socket'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'pathname'
|
|
9
|
+
|
|
10
|
+
module Pd4ml
|
|
11
|
+
# An OutputStream argument whose content comes back to Ruby (see Bridge#request).
|
|
12
|
+
Output = Struct.new(:index)
|
|
13
|
+
|
|
14
|
+
# The connection to the Java side: a bridge process (pd4ml-bridge.jar, started on first use) or
|
|
15
|
+
# a bridge server (`pd4ml serve`, PD4ML_BRIDGE=tcp://host:port). One shared bridge serves the
|
|
16
|
+
# whole process; it is thread-safe, and a callback may call PD4ML itself.
|
|
17
|
+
class Bridge
|
|
18
|
+
BRIDGE_JAR = File.expand_path('pd4ml-bridge.jar', __dir__)
|
|
19
|
+
|
|
20
|
+
@options = {}
|
|
21
|
+
@default = nil
|
|
22
|
+
@lock = Monitor.new
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Sets up the shared bridge; call before the first use of PD4ML.
|
|
26
|
+
# Options: classpath (Array), java, jvm_options (Array), server ('tcp://127.0.0.1:8723'), log_file.
|
|
27
|
+
def configure(**options)
|
|
28
|
+
@lock.synchronize do
|
|
29
|
+
raise BridgeError, 'the PD4ML bridge runs already: configure before the first use' if @default
|
|
30
|
+
@options = @options.merge(options)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The shared bridge, started on first use.
|
|
35
|
+
def get
|
|
36
|
+
@lock.synchronize do
|
|
37
|
+
unless @at_exit
|
|
38
|
+
@at_exit = true
|
|
39
|
+
at_exit { shutdown } # Java ends with the Ruby process, before it exits
|
|
40
|
+
end
|
|
41
|
+
@default ||= new(**@options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def started?
|
|
46
|
+
@lock.synchronize { !@default.nil? }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Stops the shared bridge (it starts again on the next use).
|
|
50
|
+
def shutdown
|
|
51
|
+
@lock.synchronize do
|
|
52
|
+
@default&.close
|
|
53
|
+
@default = nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def forget(bridge)
|
|
58
|
+
@lock.synchronize { @default = nil if @default.equal?(bridge) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The classpath the bridge starts with: configured, from the environment, or the installed build.
|
|
62
|
+
def classpath(options = @options)
|
|
63
|
+
entries = options[:classpath]
|
|
64
|
+
entries ||= ENV['PD4ML_CLASSPATH'].split(File::PATH_SEPARATOR) if ENV['PD4ML_CLASSPATH'].to_s != ''
|
|
65
|
+
entries ||= File.read(ENV['PD4ML_CLASSPATH_FILE']).split(/\r?\n/) if ENV['PD4ML_CLASSPATH_FILE'].to_s != ''
|
|
66
|
+
unless entries
|
|
67
|
+
entries = Installer.installed_classpath
|
|
68
|
+
if entries.empty? && Installer.auto_install?
|
|
69
|
+
warn 'pd4ml: no PD4ML build installed yet: downloading the most recent release ' \
|
|
70
|
+
"(once; 'pd4ml install' updates it, PD4ML_AUTO_INSTALL=0 turns this off)"
|
|
71
|
+
Installer.install
|
|
72
|
+
entries = Installer.installed_classpath
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
entries.map(&:strip).reject(&:empty?).flat_map do |e|
|
|
76
|
+
if e.end_with?('/*', '\\*')
|
|
77
|
+
Dir.glob(File.join(e[0..-3].tr('\\', '/'), '*.jar')).sort
|
|
78
|
+
else
|
|
79
|
+
[e]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The java executable: PD4ML_JAVA, JAVA_HOME/bin/java, else `java`.
|
|
85
|
+
def java(options = @options)
|
|
86
|
+
return options[:java] if options[:java]
|
|
87
|
+
return ENV['PD4ML_JAVA'] if ENV['PD4ML_JAVA'].to_s != ''
|
|
88
|
+
|
|
89
|
+
home = ENV['JAVA_HOME'].to_s
|
|
90
|
+
unless home.empty?
|
|
91
|
+
exe = File.join(home, 'bin', Gem.win_platform? ? 'java.exe' : 'java')
|
|
92
|
+
return exe if File.exist?(exe)
|
|
93
|
+
end
|
|
94
|
+
'java'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The command line that runs the bridge (with a port: a bridge server).
|
|
98
|
+
def command(options = @options, port: nil)
|
|
99
|
+
cp = classpath(options)
|
|
100
|
+
if cp.empty?
|
|
101
|
+
raise BridgeError, "PD4ML not found: install a build with 'pd4ml install' (or Pd4ml.install), or point at " \
|
|
102
|
+
'one with Pd4ml.configure(classpath: [...]), PD4ML_CLASSPATH or PD4ML_CLASSPATH_FILE'
|
|
103
|
+
end
|
|
104
|
+
args = [java(options), '-Djava.awt.headless=true', *Array(options[:jvm_options])]
|
|
105
|
+
args.concat(ENV['PD4ML_JVM_OPTIONS'].split) if ENV['PD4ML_JVM_OPTIONS'].to_s != ''
|
|
106
|
+
args.push('-cp', [BRIDGE_JAR, *cp].join(File::PATH_SEPARATOR), 'com.pd4ml.bridge.Bridge')
|
|
107
|
+
args.push('--port', port.to_s) if port
|
|
108
|
+
args
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def initialize(**options)
|
|
113
|
+
@lock = Monitor.new
|
|
114
|
+
@next_id = 1
|
|
115
|
+
@next_callback = 1
|
|
116
|
+
@callbacks = {}
|
|
117
|
+
@pending = {}
|
|
118
|
+
@released = Queue.new
|
|
119
|
+
@callback_error = nil
|
|
120
|
+
@closed = false
|
|
121
|
+
server = options[:server] || ENV['PD4ML_BRIDGE']
|
|
122
|
+
if server.to_s != ''
|
|
123
|
+
uri = URI(server)
|
|
124
|
+
begin
|
|
125
|
+
@socket = TCPSocket.new(uri.host, uri.port)
|
|
126
|
+
rescue SystemCallError => e
|
|
127
|
+
raise BridgeError, "cannot connect to the PD4ML bridge at #{server}: #{e.message}"
|
|
128
|
+
end
|
|
129
|
+
@in = @out = @socket
|
|
130
|
+
else
|
|
131
|
+
cmd = self.class.command(options)
|
|
132
|
+
begin
|
|
133
|
+
@in, @out, err, @thread = Open3.popen3(*cmd)
|
|
134
|
+
rescue SystemCallError => e
|
|
135
|
+
raise BridgeError, "cannot start java (#{cmd.first}): #{e.message}"
|
|
136
|
+
end
|
|
137
|
+
log = options[:log_file] || ENV['PD4ML_BRIDGE_LOG']
|
|
138
|
+
@stderr_tail = +''
|
|
139
|
+
# drained either way, so Java never blocks on a full pipe
|
|
140
|
+
@stderr_reader = Thread.new do
|
|
141
|
+
File.open(log, 'a') do |f|
|
|
142
|
+
err.each_line { |l| f.write(l); keep_tail(l) }
|
|
143
|
+
end if log
|
|
144
|
+
err.each_line { |l| keep_tail(l) } unless log
|
|
145
|
+
rescue IOError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
@in.binmode
|
|
150
|
+
@out.binmode
|
|
151
|
+
@in.sync = true
|
|
152
|
+
hello = request({ 'op' => 'hello' })
|
|
153
|
+
raise BridgeError, 'incompatible PD4ML bridge protocol' unless hello.is_a?(Hash) && hello['protocol'] == 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def close
|
|
157
|
+
@lock.synchronize do
|
|
158
|
+
return if @closed
|
|
159
|
+
|
|
160
|
+
@closed = true
|
|
161
|
+
begin
|
|
162
|
+
@in.write("{\"id\":0,\"op\":\"close\"}\n")
|
|
163
|
+
@in.close unless @socket
|
|
164
|
+
rescue IOError, SystemCallError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
@socket&.close
|
|
168
|
+
# let Java finish (it exits on 'close' or end of input), so it never outlives Ruby
|
|
169
|
+
if @thread && !@thread.join(5)
|
|
170
|
+
begin
|
|
171
|
+
Process.kill(:KILL, @thread.pid)
|
|
172
|
+
rescue SystemCallError
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
@thread.join(5)
|
|
176
|
+
end
|
|
177
|
+
@stderr_reader&.join(1)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------------------- protocol
|
|
182
|
+
|
|
183
|
+
# Sends one request and returns its decoded result; callbacks run while it waits. With `outs`
|
|
184
|
+
# (an Array), the content of Output arguments is stored there (binary Strings).
|
|
185
|
+
def request(req, outs = nil)
|
|
186
|
+
response = @lock.synchronize do
|
|
187
|
+
raise BridgeError, 'the PD4ML bridge is closed' if @closed
|
|
188
|
+
|
|
189
|
+
refs = []
|
|
190
|
+
refs << @released.pop until @released.empty?
|
|
191
|
+
exchange({ 'op' => 'release', 'refs' => refs }) unless refs.empty?
|
|
192
|
+
exchange(req)
|
|
193
|
+
end
|
|
194
|
+
raise JavaError.from(response['error']) if response['error']
|
|
195
|
+
|
|
196
|
+
if outs && response['out'].is_a?(Array)
|
|
197
|
+
response['out'].each_with_index { |data, i| outs[i] = data&.unpack1('m0') }
|
|
198
|
+
end
|
|
199
|
+
if (e = @callback_error)
|
|
200
|
+
@callback_error = nil
|
|
201
|
+
begin
|
|
202
|
+
raise e
|
|
203
|
+
rescue StandardError
|
|
204
|
+
raise CallbackError, "a callback failed: #{e.message}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
decode(response['result'])
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ----------------------------------------------------------------------------- operations
|
|
211
|
+
|
|
212
|
+
def new_object(cls, args) = request({ 'op' => 'new', 'class' => cls, 'args' => args })
|
|
213
|
+
|
|
214
|
+
def call(target, method, args, outs = nil)
|
|
215
|
+
request({ 'op' => 'call', 'ref' => handle_of(target), 'method' => method, 'args' => args }, outs)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def call_static(cls, method, args, outs = nil)
|
|
219
|
+
request({ 'op' => 'static', 'class' => cls, 'method' => method, 'args' => args }, outs)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def get_field(target, field)
|
|
223
|
+
if target.is_a?(String)
|
|
224
|
+
request({ 'op' => 'get', 'class' => target, 'field' => field })
|
|
225
|
+
else
|
|
226
|
+
request({ 'op' => 'get', 'ref' => handle_of(target), 'field' => field })
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def set_field(target, field, value)
|
|
231
|
+
if target.is_a?(String)
|
|
232
|
+
request({ 'op' => 'set', 'class' => target, 'field' => field, 'value' => value })
|
|
233
|
+
else
|
|
234
|
+
request({ 'op' => 'set', 'ref' => handle_of(target), 'field' => field, 'value' => value })
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def describe(cls) = request({ 'op' => 'describe', 'class' => cls })
|
|
239
|
+
|
|
240
|
+
def instance_of?(target, cls)
|
|
241
|
+
request({ 'op' => 'instanceof', 'ref' => handle_of(target), 'class' => cls }) == true
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# A Java object implementing `iface`; its methods call the block with (method, args).
|
|
245
|
+
def proxy(iface, &block)
|
|
246
|
+
id = @lock.synchronize { (@next_callback += 1) - 1 }
|
|
247
|
+
@callbacks[id] = block
|
|
248
|
+
request({ 'op' => 'proxy', 'interface' => iface, 'callback' => id })
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Called by JavaObject finalizers; released with the next request.
|
|
252
|
+
def release(handle)
|
|
253
|
+
@released << handle unless @closed
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# ------------------------------------------------------------------------------- values
|
|
257
|
+
|
|
258
|
+
def encode(v)
|
|
259
|
+
case v
|
|
260
|
+
when JavaObject then { '$ref' => v.__handle }
|
|
261
|
+
when Output then { '$out' => v.index }
|
|
262
|
+
when PD4ML then { '$ref' => v.java.__handle }
|
|
263
|
+
when String
|
|
264
|
+
v.encoding == Encoding::BINARY || !v.valid_encoding? ? { '$bytes' => [v].pack('m0') } : v
|
|
265
|
+
when Symbol then v.to_s
|
|
266
|
+
when Pathname, URI::Generic then v.to_s
|
|
267
|
+
when Hash then v.to_h { |k, x| [k.to_s, encode(x)] }
|
|
268
|
+
when Array then v.map { |x| encode(x) }
|
|
269
|
+
when nil, true, false, Integer then v
|
|
270
|
+
when Float
|
|
271
|
+
raise ArgumentError, "not a finite number: #{v}" unless v.finite?
|
|
272
|
+
|
|
273
|
+
v
|
|
274
|
+
else
|
|
275
|
+
if v.respond_to?(:read)
|
|
276
|
+
data = v.read
|
|
277
|
+
{ '$bytes' => [data.to_s].pack('m0') }
|
|
278
|
+
elsif v.respond_to?(:to_str)
|
|
279
|
+
v.to_str
|
|
280
|
+
else
|
|
281
|
+
raise ArgumentError, "cannot pass #{v.class} to Java"
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def decode(v)
|
|
287
|
+
case v
|
|
288
|
+
when Array then v.map { |x| decode(x) }
|
|
289
|
+
when Hash
|
|
290
|
+
if v.key?('$ref')
|
|
291
|
+
JavaObject.new(self, v['$ref'], v['class'], v['enum'])
|
|
292
|
+
elsif v.key?('$bytes')
|
|
293
|
+
v['$bytes'].unpack1('m0')
|
|
294
|
+
else
|
|
295
|
+
v.transform_values { |x| decode(x) }
|
|
296
|
+
end
|
|
297
|
+
else
|
|
298
|
+
v
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
private
|
|
303
|
+
|
|
304
|
+
def keep_tail(line)
|
|
305
|
+
@stderr_tail = (@stderr_tail + line)[-4000..] || (@stderr_tail + line)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def handle_of(target)
|
|
309
|
+
case target
|
|
310
|
+
when JavaObject then target.__handle
|
|
311
|
+
when PD4ML then target.java.__handle
|
|
312
|
+
else raise ArgumentError, "not a Java object: #{target.inspect}"
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def exchange(req)
|
|
317
|
+
id = @next_id
|
|
318
|
+
@next_id += 1
|
|
319
|
+
line = JSON.generate(encode(req.merge('id' => id)))
|
|
320
|
+
begin
|
|
321
|
+
@in.write(line, "\n")
|
|
322
|
+
rescue IOError, SystemCallError => e
|
|
323
|
+
gone("the PD4ML bridge is gone (#{e.message})")
|
|
324
|
+
end
|
|
325
|
+
loop do
|
|
326
|
+
return @pending.delete(id) if @pending.key?(id)
|
|
327
|
+
|
|
328
|
+
raw = @out.gets
|
|
329
|
+
gone('the PD4ML bridge closed the connection') if raw.nil?
|
|
330
|
+
next if raw.strip.empty?
|
|
331
|
+
|
|
332
|
+
msg = JSON.parse(raw.force_encoding(Encoding::UTF_8))
|
|
333
|
+
if msg.key?('callback')
|
|
334
|
+
dispatch(msg)
|
|
335
|
+
elsif msg['id'] == id
|
|
336
|
+
return msg
|
|
337
|
+
else
|
|
338
|
+
@pending[msg['id']] = msg # an outer request's response, during a callback
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def dispatch(msg)
|
|
344
|
+
callback = @callbacks[msg['callback']]
|
|
345
|
+
return if callback.nil? || @callback_error
|
|
346
|
+
|
|
347
|
+
begin
|
|
348
|
+
callback.call(msg['method'], Array(msg['args']).map { |a| decode(a) })
|
|
349
|
+
rescue StandardError => e
|
|
350
|
+
@callback_error = e # raised by the request that caused the callback
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def gone(why)
|
|
355
|
+
@closed = true
|
|
356
|
+
self.class.forget(self)
|
|
357
|
+
detail = @stderr_tail.to_s.strip
|
|
358
|
+
raise BridgeError, why + (detail.empty? ? '' : "\n#{detail}") +
|
|
359
|
+
(ENV['PD4ML_BRIDGE_LOG'] ? '' : ' (PD4ML_BRIDGE_LOG=file keeps its output)')
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|