iruby 0.2.7 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|