iruby 0.2.9 → 0.6.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 +5 -5
- data/.github/workflows/ubuntu.yml +62 -0
- data/CHANGES +64 -0
- data/Gemfile +3 -1
- data/LICENSE +1 -1
- data/README.md +120 -92
- data/Rakefile +36 -10
- data/ci/Dockerfile.base.erb +41 -0
- data/ci/Dockerfile.main.erb +7 -0
- data/ci/requirements.txt +1 -0
- data/docker/setup.sh +15 -0
- data/docker/test.sh +7 -0
- data/iruby.gemspec +13 -18
- data/lib/iruby.rb +13 -6
- data/lib/iruby/backend.rb +38 -9
- data/lib/iruby/command.rb +68 -12
- data/lib/iruby/display.rb +81 -41
- data/lib/iruby/event_manager.rb +40 -0
- data/lib/iruby/formatter.rb +3 -3
- data/lib/iruby/input.rb +6 -6
- data/lib/iruby/input/README.ipynb +55 -3
- data/lib/iruby/input/README.md +299 -0
- data/lib/iruby/input/autoload.rb +3 -2
- data/lib/iruby/input/builder.rb +4 -4
- data/lib/iruby/input/button.rb +2 -2
- data/lib/iruby/input/cancel.rb +1 -1
- data/lib/iruby/input/checkbox.rb +22 -4
- data/lib/iruby/input/date.rb +17 -6
- data/lib/iruby/input/field.rb +5 -4
- data/lib/iruby/input/file.rb +3 -3
- data/lib/iruby/input/form.rb +6 -6
- data/lib/iruby/input/label.rb +9 -3
- data/lib/iruby/input/multiple.rb +76 -0
- data/lib/iruby/input/popup.rb +5 -2
- data/lib/iruby/input/radio.rb +18 -6
- data/lib/iruby/input/select.rb +26 -12
- data/lib/iruby/input/textarea.rb +2 -1
- data/lib/iruby/input/widget.rb +2 -2
- data/lib/iruby/jupyter.rb +77 -0
- data/lib/iruby/kernel.rb +171 -31
- data/lib/iruby/ostream.rb +29 -8
- data/lib/iruby/session.rb +116 -0
- data/lib/iruby/session/{rbczmq.rb → cztop.rb} +21 -19
- data/lib/iruby/session/ffi_rzmq.rb +1 -1
- data/lib/iruby/session_adapter.rb +72 -0
- data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
- data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
- data/lib/iruby/session_adapter/pyzmq_adapter.rb +77 -0
- data/lib/iruby/session_adapter/test_adapter.rb +49 -0
- data/lib/iruby/utils.rb +13 -2
- data/lib/iruby/version.rb +1 -1
- data/run-test.sh +12 -0
- data/tasks/ci.rake +65 -0
- data/test/helper.rb +133 -0
- data/test/integration_test.rb +22 -11
- data/test/iruby/backend_test.rb +37 -0
- data/test/iruby/command_test.rb +207 -0
- data/test/iruby/event_manager_test.rb +92 -0
- data/test/iruby/jupyter_test.rb +27 -0
- data/test/iruby/kernel_test.rb +153 -0
- data/test/iruby/mime_test.rb +43 -0
- data/test/iruby/multi_logger_test.rb +1 -2
- data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
- data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
- data/test/iruby/session_adapter/session_adapter_test_base.rb +27 -0
- data/test/iruby/session_adapter_test.rb +91 -0
- data/test/iruby/session_test.rb +48 -0
- data/test/run-test.rb +19 -0
- metadata +107 -50
- data/.travis.yml +0 -16
- data/CONTRIBUTORS +0 -19
- data/test/test_helper.rb +0 -5
data/lib/iruby/ostream.rb
CHANGED
@@ -25,22 +25,43 @@ module IRuby
|
|
25
25
|
alias_method :next, :read
|
26
26
|
alias_method :readline, :read
|
27
27
|
|
28
|
-
def write(
|
29
|
-
|
30
|
-
|
31
|
-
nil
|
28
|
+
def write(*obj)
|
29
|
+
str = build_string { |sio| sio.write(*obj) }
|
30
|
+
session_send(str)
|
32
31
|
end
|
33
32
|
alias_method :<<, :write
|
34
33
|
alias_method :print, :write
|
35
34
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
def printf(format, *obj)
|
36
|
+
str = build_string { |sio| sio.printf(format, *obj) }
|
37
|
+
session_send(str)
|
38
|
+
end
|
39
|
+
|
40
|
+
def puts(*obj)
|
41
|
+
str = build_string { |sio| sio.puts(*obj) }
|
42
|
+
session_send(str)
|
40
43
|
end
|
41
44
|
|
42
45
|
def writelines(lines)
|
43
46
|
lines.each { |s| write(s) }
|
44
47
|
end
|
48
|
+
|
49
|
+
# Called by irb
|
50
|
+
def set_encoding(extern, intern)
|
51
|
+
a = extern
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def build_string
|
57
|
+
StringIO.open { |sio| yield(sio); sio.string }
|
58
|
+
end
|
59
|
+
|
60
|
+
def session_send(str)
|
61
|
+
raise 'I/O operation on closed file' unless @session
|
62
|
+
|
63
|
+
@session.send(:publish, :stream, name: @name, text: str)
|
64
|
+
nil
|
65
|
+
end
|
45
66
|
end
|
46
67
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'iruby/session_adapter'
|
2
|
+
require 'iruby/session/mixin'
|
3
|
+
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module IRuby
|
7
|
+
class Session
|
8
|
+
include SessionSerialize
|
9
|
+
|
10
|
+
def initialize(config, adapter_name=nil)
|
11
|
+
@config = config
|
12
|
+
@adapter = create_session_adapter(config, adapter_name)
|
13
|
+
|
14
|
+
setup
|
15
|
+
setup_sockets
|
16
|
+
setup_heartbeat
|
17
|
+
setup_security
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :adapter, :config
|
21
|
+
|
22
|
+
def description
|
23
|
+
"#{@adapter.name} session adapter"
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup_sockets
|
30
|
+
protocol, host = config.values_at('transport', 'ip')
|
31
|
+
shell_port = config['shell_port']
|
32
|
+
iopub_port = config['iopub_port']
|
33
|
+
stdin_port = config['stdin_port']
|
34
|
+
|
35
|
+
@shell_socket, @shell_port = @adapter.make_router_socket(protocol, host, shell_port)
|
36
|
+
@iopub_socket, @iopub_port = @adapter.make_pub_socket(protocol, host, iopub_port)
|
37
|
+
@stdin_socket, @stdin_port = @adapter.make_router_socket(protocol, host, stdin_port)
|
38
|
+
|
39
|
+
@sockets = {
|
40
|
+
publish: @iopub_socket,
|
41
|
+
reply: @shell_socket,
|
42
|
+
stdin: @stdin_socket
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def setup_heartbeat
|
47
|
+
protocol, host = config.values_at('transport', 'ip')
|
48
|
+
hb_port = config['hb_port']
|
49
|
+
@hb_socket, @hb_port = @adapter.make_rep_socket(protocol, host, hb_port)
|
50
|
+
@heartbeat_thread = Thread.start do
|
51
|
+
begin
|
52
|
+
# NOTE: this loop is copied from CZTop's old session code
|
53
|
+
@adapter.heartbeat_loop(@hb_socket)
|
54
|
+
rescue Exception => e
|
55
|
+
IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_security
|
61
|
+
@session_id = SecureRandom.uuid
|
62
|
+
unless config['key'].empty? || config['signature_scheme'].empty?
|
63
|
+
unless config['signature_scheme'] =~ /\Ahmac-/
|
64
|
+
raise "Unknown signature_scheme: #{config['signature_scheme']}"
|
65
|
+
end
|
66
|
+
digest_algorithm = config['signature_scheme'][/\Ahmac-(.*)\Z/, 1]
|
67
|
+
@hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new(digest_algorithm))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def send(socket_type, message_type, content)
|
72
|
+
sock = check_socket_type(socket_type)
|
73
|
+
idents = if socket_type == :reply && @last_recvd_msg
|
74
|
+
@last_recvd_msg[:idents]
|
75
|
+
else
|
76
|
+
message_type == :stream ? "stream.#{content[:name]}" : message_type
|
77
|
+
end
|
78
|
+
header = {
|
79
|
+
msg_type: message_type,
|
80
|
+
msg_id: SecureRandom.uuid,
|
81
|
+
username: 'kernel',
|
82
|
+
session: @session_id,
|
83
|
+
version: '5.0'
|
84
|
+
}
|
85
|
+
@adapter.send(sock, serialize(idents, header, content))
|
86
|
+
end
|
87
|
+
|
88
|
+
def recv(socket_type)
|
89
|
+
sock = check_socket_type(socket_type)
|
90
|
+
data = @adapter.recv(sock)
|
91
|
+
@last_recvd_msg = unserialize(data)
|
92
|
+
end
|
93
|
+
|
94
|
+
def recv_input
|
95
|
+
sock = check_socket_type(:stdin)
|
96
|
+
data = @adapter.recv(sock)
|
97
|
+
unserialize(data)[:content]["value"]
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def check_socket_type(socket_type)
|
103
|
+
case socket_type
|
104
|
+
when :publish, :reply, :stdin
|
105
|
+
@sockets[socket_type]
|
106
|
+
else
|
107
|
+
raise ArgumentError, "Invalid socket type #{socket_type}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def create_session_adapter(config, adapter_name)
|
112
|
+
adapter_class = SessionAdapter.select_adapter_class(adapter_name)
|
113
|
+
adapter_class.new(config)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -1,34 +1,32 @@
|
|
1
|
-
require '
|
1
|
+
require 'cztop'
|
2
2
|
|
3
3
|
module IRuby
|
4
4
|
class Session
|
5
5
|
include SessionSerialize
|
6
6
|
|
7
7
|
def initialize(config)
|
8
|
-
c = ZMQ::Context.new
|
9
|
-
|
10
8
|
connection = "#{config['transport']}://#{config['ip']}:%d"
|
11
|
-
reply_socket = c.socket(:ROUTER)
|
12
|
-
reply_socket.bind(connection % config['shell_port'])
|
13
|
-
|
14
|
-
pub_socket = c.socket(:PUB)
|
15
|
-
pub_socket.bind(connection % config['iopub_port'])
|
16
9
|
|
17
|
-
|
18
|
-
|
10
|
+
reply_socket = CZTop::Socket::ROUTER.new(connection % config['shell_port'])
|
11
|
+
pub_socket = CZTop::Socket::PUB.new(connection % config['iopub_port'])
|
12
|
+
stdin_socket = CZTop::Socket::ROUTER.new(connection % config['stdin_port'])
|
19
13
|
|
20
14
|
Thread.new do
|
21
15
|
begin
|
22
|
-
hb_socket =
|
23
|
-
|
24
|
-
|
16
|
+
hb_socket = CZTop::Socket::REP.new(connection % config['hb_port'])
|
17
|
+
loop do
|
18
|
+
message = hb_socket.receive
|
19
|
+
hb_socket << message
|
20
|
+
end
|
25
21
|
rescue Exception => e
|
26
22
|
IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
|
27
23
|
end
|
28
24
|
end
|
29
25
|
|
30
|
-
@sockets = {
|
31
|
-
publish: pub_socket,
|
26
|
+
@sockets = {
|
27
|
+
publish: pub_socket,
|
28
|
+
reply: reply_socket,
|
29
|
+
stdin: stdin_socket,
|
32
30
|
}
|
33
31
|
|
34
32
|
@session = SecureRandom.uuid
|
@@ -38,6 +36,10 @@ module IRuby
|
|
38
36
|
end
|
39
37
|
end
|
40
38
|
|
39
|
+
def description
|
40
|
+
'old-stle session using cztop'
|
41
|
+
end
|
42
|
+
|
41
43
|
# Build and send a message
|
42
44
|
def send(socket, type, content)
|
43
45
|
idents =
|
@@ -53,16 +55,16 @@ module IRuby
|
|
53
55
|
session: @session,
|
54
56
|
version: '5.0'
|
55
57
|
}
|
56
|
-
@sockets[socket]
|
58
|
+
@sockets[socket] << serialize(idents, header, content)
|
57
59
|
end
|
58
60
|
|
59
61
|
# Receive a message and decode it
|
60
62
|
def recv(socket)
|
61
|
-
@last_recvd_msg = unserialize(@sockets[socket].
|
63
|
+
@last_recvd_msg = unserialize(@sockets[socket].receive)
|
62
64
|
end
|
63
65
|
|
64
|
-
def recv_input
|
65
|
-
unserialize(@sockets[:stdin].
|
66
|
+
def recv_input
|
67
|
+
unserialize(@sockets[:stdin].receive)[:content]["value"]
|
66
68
|
end
|
67
69
|
end
|
68
70
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module IRuby
|
2
|
+
class SessionAdapterNotFound < RuntimeError; end
|
3
|
+
|
4
|
+
module SessionAdapter
|
5
|
+
class BaseAdapter
|
6
|
+
def self.available?
|
7
|
+
load_requirements
|
8
|
+
true
|
9
|
+
rescue LoadError
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.load_requirements
|
14
|
+
# Do nothing
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(config)
|
18
|
+
@config = config
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
self.class.name[/::(\w+)Adapter\Z/, 1].downcase
|
23
|
+
end
|
24
|
+
|
25
|
+
def make_router_socket(protocol, host, port)
|
26
|
+
socket, port = make_socket(:ROUTER, protocol, host, port)
|
27
|
+
[socket, port]
|
28
|
+
end
|
29
|
+
|
30
|
+
def make_pub_socket(protocol, host, port)
|
31
|
+
socket, port = make_socket(:PUB, protocol, host, port)
|
32
|
+
[socket, port]
|
33
|
+
end
|
34
|
+
|
35
|
+
def make_rep_socket(protocol, host, port)
|
36
|
+
socket, port = make_socket(:REP, protocol, host, port)
|
37
|
+
[socket, port]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
require_relative 'session_adapter/ffirzmq_adapter'
|
42
|
+
require_relative 'session_adapter/cztop_adapter'
|
43
|
+
require_relative 'session_adapter/pyzmq_adapter'
|
44
|
+
require_relative 'session_adapter/test_adapter'
|
45
|
+
|
46
|
+
def self.select_adapter_class(name=nil)
|
47
|
+
classes = {
|
48
|
+
'ffi-rzmq' => SessionAdapter::FfirzmqAdapter,
|
49
|
+
'cztop' => SessionAdapter::CztopAdapter,
|
50
|
+
# 'pyzmq' => SessionAdapter::PyzmqAdapter
|
51
|
+
'test' => SessionAdapter::TestAdapter,
|
52
|
+
}
|
53
|
+
if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil))
|
54
|
+
cls = classes[name]
|
55
|
+
unless cls.available?
|
56
|
+
if ENV['IRUBY_SESSION_ADAPTER']
|
57
|
+
raise SessionAdapterNotFound,
|
58
|
+
"Session adapter `#{name}` from IRUBY_SESSION_ADAPTER is unavailable"
|
59
|
+
else
|
60
|
+
raise SessionAdapterNotFound,
|
61
|
+
"Session adapter `#{name}` is unavailable"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
return cls
|
65
|
+
end
|
66
|
+
classes.each_value do |cls|
|
67
|
+
return cls if cls.available?
|
68
|
+
end
|
69
|
+
raise SessionAdapterNotFound, "No session adapter is available"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module IRuby
|
2
|
+
module SessionAdapter
|
3
|
+
class CztopAdapter < BaseAdapter
|
4
|
+
def self.load_requirements
|
5
|
+
require 'cztop'
|
6
|
+
end
|
7
|
+
|
8
|
+
def send(sock, data)
|
9
|
+
sock << data
|
10
|
+
end
|
11
|
+
|
12
|
+
def recv(sock)
|
13
|
+
sock.receive
|
14
|
+
end
|
15
|
+
|
16
|
+
def heartbeat_loop(sock)
|
17
|
+
loop do
|
18
|
+
message = sock.receive
|
19
|
+
sock << message
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def socket_type_class(type_symbol)
|
26
|
+
case type_symbol
|
27
|
+
when :ROUTER, :PUB, :REP
|
28
|
+
CZTop::Socket.const_get(type_symbol)
|
29
|
+
else
|
30
|
+
if CZTop::Socket.const_defined?(type_symbol)
|
31
|
+
raise ArgumentError, "Unsupported ZMQ socket type: #{type_symbol}"
|
32
|
+
else
|
33
|
+
raise ArgumentError, "Invalid ZMQ socket type: #{type_symbol}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def make_socket(type_symbol, protocol, host, port)
|
39
|
+
uri = "#{protocol}://#{host}:#{port}"
|
40
|
+
socket_class = socket_type_class(type_symbol)
|
41
|
+
socket_class.new(uri)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module IRuby
|
2
|
+
module SessionAdapter
|
3
|
+
class FfirzmqAdapter < BaseAdapter
|
4
|
+
def self.load_requirements
|
5
|
+
require 'ffi-rzmq'
|
6
|
+
end
|
7
|
+
|
8
|
+
def send(sock, data)
|
9
|
+
data.each_with_index do |part, i|
|
10
|
+
sock.send_string(part, i == data.size - 1 ? 0 : ZMQ::SNDMORE)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def recv(sock)
|
15
|
+
msg = []
|
16
|
+
while msg.empty? || sock.more_parts?
|
17
|
+
begin
|
18
|
+
frame = ''
|
19
|
+
rc = sock.recv_string(frame)
|
20
|
+
ZMQ::Util.error_check('zmq_msg_recv', rc)
|
21
|
+
msg << frame
|
22
|
+
rescue
|
23
|
+
end
|
24
|
+
end
|
25
|
+
msg
|
26
|
+
end
|
27
|
+
|
28
|
+
def heartbeat_loop(sock)
|
29
|
+
@heartbeat_device = ZMQ::Device.new(sock, sock)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def make_socket(type, protocol, host, port)
|
35
|
+
case type
|
36
|
+
when :ROUTER, :PUB, :REP
|
37
|
+
type = ZMQ.const_get(type)
|
38
|
+
else
|
39
|
+
if ZMQ.const_defined?(type)
|
40
|
+
raise ArgumentError, "Unsupported ZMQ socket type: #{type_symbol}"
|
41
|
+
else
|
42
|
+
raise ArgumentError, "Invalid ZMQ socket type: #{type_symbol}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
zmq_context.socket(type).tap do |sock|
|
46
|
+
sock.bind("#{protocol}://#{host}:#{port}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def zmq_context
|
51
|
+
@zmq_context ||= ZMQ::Context.new
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module IRuby
|
2
|
+
module SessionAdapter
|
3
|
+
class PyzmqAdapter < BaseAdapter
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def load_requirements
|
7
|
+
require 'pycall'
|
8
|
+
import_pyzmq
|
9
|
+
end
|
10
|
+
|
11
|
+
def import_pyzmq
|
12
|
+
@zmq = PyCall.import_module('zmq')
|
13
|
+
rescue PyCall::PyError => error
|
14
|
+
raise LoadError, error.message
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :zmq
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_router_socket(protocol, host, port)
|
21
|
+
make_socket(:ROUTER, protocol, host, port)
|
22
|
+
end
|
23
|
+
|
24
|
+
def make_pub_socket(protocol, host, port)
|
25
|
+
make_socket(:PUB, protocol, host, port)
|
26
|
+
end
|
27
|
+
|
28
|
+
def heartbeat_loop(sock)
|
29
|
+
PyCall.sys.path.append(File.expand_path('../pyzmq', __FILE__))
|
30
|
+
heartbeat = PyCall.import_module('iruby.heartbeat')
|
31
|
+
@heartbeat_thread = heartbeat.Heartbeat.new(sock)
|
32
|
+
@heartbeat_thread.start
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def socket_type(type_symbol)
|
38
|
+
case type_symbol
|
39
|
+
when :ROUTER, :PUB, :REP
|
40
|
+
zmq[type_symbol]
|
41
|
+
else
|
42
|
+
raise ArgumentError, "Unknown ZMQ socket type: #{type_symbol}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def make_socket(type_symbol, protocol, host, port)
|
47
|
+
type = socket_type(type_symbol)
|
48
|
+
sock = zmq_context.socket(type)
|
49
|
+
bind_socket(sock, protocol, host, port)
|
50
|
+
sock
|
51
|
+
end
|
52
|
+
|
53
|
+
def bind_socket(sock, protocol, host, port)
|
54
|
+
iface = "#{protocol}://#{host}"
|
55
|
+
case protocol
|
56
|
+
when 'tcp'
|
57
|
+
if port <= 0
|
58
|
+
port = sock.bind_to_random_port(iface)
|
59
|
+
else
|
60
|
+
sock.bind("#{iface}:#{port}")
|
61
|
+
end
|
62
|
+
else
|
63
|
+
raise ArgumentError, "Unsupported protocol: #{protocol}"
|
64
|
+
end
|
65
|
+
[sock, port]
|
66
|
+
end
|
67
|
+
|
68
|
+
def zmq_context
|
69
|
+
zmq.Context.instance
|
70
|
+
end
|
71
|
+
|
72
|
+
def zmq
|
73
|
+
self.class.zmq
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|