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