iruby 0.2.7 → 0.5.0
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 +62 -0
- data/Gemfile +3 -1
- data/LICENSE +1 -1
- data/README.md +148 -27
- 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 +14 -18
- data/lib/iruby.rb +19 -3
- data/lib/iruby/backend.rb +22 -2
- data/lib/iruby/command.rb +76 -13
- data/lib/iruby/display.rb +69 -39
- data/lib/iruby/formatter.rb +5 -4
- data/lib/iruby/input.rb +41 -0
- data/lib/iruby/input/README.ipynb +502 -0
- data/lib/iruby/input/README.md +299 -0
- data/lib/iruby/input/autoload.rb +25 -0
- data/lib/iruby/input/builder.rb +67 -0
- data/lib/iruby/input/button.rb +47 -0
- data/lib/iruby/input/cancel.rb +32 -0
- data/lib/iruby/input/checkbox.rb +74 -0
- data/lib/iruby/input/date.rb +37 -0
- data/lib/iruby/input/field.rb +31 -0
- data/lib/iruby/input/file.rb +57 -0
- data/lib/iruby/input/form.rb +77 -0
- data/lib/iruby/input/label.rb +27 -0
- data/lib/iruby/input/multiple.rb +76 -0
- data/lib/iruby/input/popup.rb +41 -0
- data/lib/iruby/input/radio.rb +59 -0
- data/lib/iruby/input/select.rb +59 -0
- data/lib/iruby/input/textarea.rb +23 -0
- data/lib/iruby/input/widget.rb +34 -0
- data/lib/iruby/jupyter.rb +77 -0
- data/lib/iruby/kernel.rb +67 -22
- data/lib/iruby/ostream.rb +24 -8
- data/lib/iruby/session.rb +85 -67
- data/lib/iruby/session/cztop.rb +70 -0
- data/lib/iruby/session/ffi_rzmq.rb +87 -0
- data/lib/iruby/session/mixin.rb +47 -0
- data/lib/iruby/session_adapter.rb +66 -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/utils.rb +5 -2
- data/lib/iruby/version.rb +1 -1
- data/run-test.sh +12 -0
- data/tasks/ci.rake +65 -0
- data/test/helper.rb +90 -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/jupyter_test.rb +27 -0
- data/test/iruby/mime_test.rb +32 -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 +47 -0
- data/test/run-test.rb +18 -0
- metadata +130 -46
- data/.travis.yml +0 -16
- data/CONTRIBUTORS +0 -19
- data/test/test_helper.rb +0 -5
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'cztop'
|
2
|
+
|
3
|
+
module IRuby
|
4
|
+
class Session
|
5
|
+
include SessionSerialize
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
connection = "#{config['transport']}://#{config['ip']}:%d"
|
9
|
+
|
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'])
|
13
|
+
|
14
|
+
Thread.new do
|
15
|
+
begin
|
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
|
21
|
+
rescue Exception => e
|
22
|
+
IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@sockets = {
|
27
|
+
publish: pub_socket,
|
28
|
+
reply: reply_socket,
|
29
|
+
stdin: stdin_socket,
|
30
|
+
}
|
31
|
+
|
32
|
+
@session = SecureRandom.uuid
|
33
|
+
unless config['key'].to_s.empty? || config['signature_scheme'].to_s.empty?
|
34
|
+
raise 'Unknown signature scheme' unless config['signature_scheme'] =~ /\Ahmac-(.*)\Z/
|
35
|
+
@hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new($1))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def description
|
40
|
+
'old-stle session using cztop'
|
41
|
+
end
|
42
|
+
|
43
|
+
# Build and send a message
|
44
|
+
def send(socket, type, content)
|
45
|
+
idents =
|
46
|
+
if socket == :reply && @last_recvd_msg
|
47
|
+
@last_recvd_msg[:idents]
|
48
|
+
else
|
49
|
+
type == :stream ? "stream.#{content[:name]}" : type
|
50
|
+
end
|
51
|
+
header = {
|
52
|
+
msg_type: type,
|
53
|
+
msg_id: SecureRandom.uuid,
|
54
|
+
username: 'kernel',
|
55
|
+
session: @session,
|
56
|
+
version: '5.0'
|
57
|
+
}
|
58
|
+
@sockets[socket] << serialize(idents, header, content)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Receive a message and decode it
|
62
|
+
def recv(socket)
|
63
|
+
@last_recvd_msg = unserialize(@sockets[socket].receive)
|
64
|
+
end
|
65
|
+
|
66
|
+
def recv_input
|
67
|
+
unserialize(@sockets[:stdin].receive)[:content]["value"]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'ffi-rzmq'
|
2
|
+
|
3
|
+
module IRuby
|
4
|
+
class Session
|
5
|
+
include SessionSerialize
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
c = ZMQ::Context.new
|
9
|
+
|
10
|
+
connection = "#{config['transport']}://#{config['ip']}:%d"
|
11
|
+
reply_socket = c.socket(ZMQ::XREP)
|
12
|
+
reply_socket.bind(connection % config['shell_port'])
|
13
|
+
|
14
|
+
pub_socket = c.socket(ZMQ::PUB)
|
15
|
+
pub_socket.bind(connection % config['iopub_port'])
|
16
|
+
|
17
|
+
stdin_socket = c.socket(ZMQ::XREP)
|
18
|
+
stdin_socket.bind(connection % config['stdin_port'])
|
19
|
+
|
20
|
+
Thread.new do
|
21
|
+
begin
|
22
|
+
hb_socket = c.socket(ZMQ::REP)
|
23
|
+
hb_socket.bind(connection % config['hb_port'])
|
24
|
+
ZMQ::Device.new(hb_socket, hb_socket)
|
25
|
+
rescue Exception => e
|
26
|
+
IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
@sockets = {
|
31
|
+
publish: pub_socket, reply: reply_socket, stdin: stdin_socket
|
32
|
+
}
|
33
|
+
|
34
|
+
@session = SecureRandom.uuid
|
35
|
+
unless config['key'].to_s.empty? || config['signature_scheme'].to_s.empty?
|
36
|
+
raise 'Unknown signature scheme' unless config['signature_scheme'] =~ /\Ahmac-(.*)\Z/
|
37
|
+
@hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new($1))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Build and send a message
|
42
|
+
def send(socket, type, content)
|
43
|
+
idents =
|
44
|
+
if socket == :reply && @last_recvd_msg
|
45
|
+
@last_recvd_msg[:idents]
|
46
|
+
else
|
47
|
+
type == :stream ? "stream.#{content[:name]}" : type
|
48
|
+
end
|
49
|
+
header = {
|
50
|
+
msg_type: type,
|
51
|
+
msg_id: SecureRandom.uuid,
|
52
|
+
username: 'kernel',
|
53
|
+
session: @session,
|
54
|
+
version: '5.0'
|
55
|
+
}
|
56
|
+
socket = @sockets[socket]
|
57
|
+
list = serialize(idents, header, content)
|
58
|
+
list.each_with_index do |part, i|
|
59
|
+
socket.send_string(part, i == list.size - 1 ? 0 : ZMQ::SNDMORE)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Receive a message and decode it
|
64
|
+
def recv(socket)
|
65
|
+
socket = @sockets[socket]
|
66
|
+
msg = []
|
67
|
+
while msg.empty? || socket.more_parts?
|
68
|
+
begin
|
69
|
+
frame = ''
|
70
|
+
rc = socket.recv_string(frame)
|
71
|
+
ZMQ::Util.error_check('zmq_msg_send', rc)
|
72
|
+
msg << frame
|
73
|
+
rescue
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
@last_recvd_msg = unserialize(msg)
|
78
|
+
end
|
79
|
+
|
80
|
+
def recv_input
|
81
|
+
last_recvd_msg = @last_recvd_msg
|
82
|
+
input = recv(:stdin)[:content]["value"]
|
83
|
+
@last_recvd_msg = last_recvd_msg
|
84
|
+
input
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module IRuby
|
2
|
+
module SessionSerialize
|
3
|
+
DELIM = '<IDS|MSG>'
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def serialize(idents, header, content)
|
8
|
+
msg = [MultiJson.dump(header),
|
9
|
+
MultiJson.dump(@last_recvd_msg ? @last_recvd_msg[:header] : {}),
|
10
|
+
'{}',
|
11
|
+
MultiJson.dump(content || {})]
|
12
|
+
frames = ([*idents].compact.map(&:to_s) << DELIM << sign(msg)) + msg
|
13
|
+
IRuby.logger.debug "Sent #{frames.inspect}"
|
14
|
+
frames
|
15
|
+
end
|
16
|
+
|
17
|
+
def unserialize(msg)
|
18
|
+
raise 'no message received' unless msg
|
19
|
+
frames = msg.to_a.map(&:to_s)
|
20
|
+
IRuby.logger.debug "Received #{frames.inspect}"
|
21
|
+
|
22
|
+
i = frames.index(DELIM)
|
23
|
+
idents, msg_list = frames[0..i-1], frames[i+1..-1]
|
24
|
+
|
25
|
+
minlen = 5
|
26
|
+
raise 'malformed message, must have at least #{minlen} elements' unless msg_list.length >= minlen
|
27
|
+
s, header, parent_header, metadata, content, buffers = *msg_list
|
28
|
+
raise 'Invalid signature' unless s == sign(msg_list[1..-1])
|
29
|
+
{
|
30
|
+
idents: idents,
|
31
|
+
header: MultiJson.load(header),
|
32
|
+
parent_header: MultiJson.load(parent_header),
|
33
|
+
metadata: MultiJson.load(metadata),
|
34
|
+
content: MultiJson.load(content),
|
35
|
+
buffers: buffers
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sign using HMAC
|
40
|
+
def sign(list)
|
41
|
+
return '' unless @hmac
|
42
|
+
@hmac.reset
|
43
|
+
list.each {|m| @hmac.update(m) }
|
44
|
+
@hmac.hexdigest
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,66 @@
|
|
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 initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
self.class.name[/::(\w+)Adapter\Z/, 1].downcase
|
19
|
+
end
|
20
|
+
|
21
|
+
def make_router_socket(protocol, host, port)
|
22
|
+
socket, port = make_socket(:ROUTER, protocol, host, port)
|
23
|
+
[socket, port]
|
24
|
+
end
|
25
|
+
|
26
|
+
def make_pub_socket(protocol, host, port)
|
27
|
+
socket, port = make_socket(:PUB, protocol, host, port)
|
28
|
+
[socket, port]
|
29
|
+
end
|
30
|
+
|
31
|
+
def make_rep_socket(protocol, host, port)
|
32
|
+
socket, port = make_socket(:REP, protocol, host, port)
|
33
|
+
[socket, port]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require_relative 'session_adapter/ffirzmq_adapter'
|
38
|
+
require_relative 'session_adapter/cztop_adapter'
|
39
|
+
require_relative 'session_adapter/pyzmq_adapter'
|
40
|
+
|
41
|
+
def self.select_adapter_class(name=nil)
|
42
|
+
classes = {
|
43
|
+
'ffi-rzmq' => SessionAdapter::FfirzmqAdapter,
|
44
|
+
'cztop' => SessionAdapter::CztopAdapter,
|
45
|
+
# 'pyzmq' => SessionAdapter::PyzmqAdapter
|
46
|
+
}
|
47
|
+
if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil))
|
48
|
+
cls = classes[name]
|
49
|
+
unless cls.available?
|
50
|
+
if ENV['IRUBY_SESSION_ADAPTER']
|
51
|
+
raise SessionAdapterNotFound,
|
52
|
+
"Session adapter `#{name}` from IRUBY_SESSION_ADAPTER is unavailable"
|
53
|
+
else
|
54
|
+
raise SessionAdapterNotFound,
|
55
|
+
"Session adapter `#{name}` is unavailable"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
return cls
|
59
|
+
end
|
60
|
+
classes.each_value do |cls|
|
61
|
+
return cls if cls.available?
|
62
|
+
end
|
63
|
+
raise SessionAdapterNotFound, "No session adapter is available"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
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
|