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.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ubuntu.yml +62 -0
  3. data/CHANGES +62 -0
  4. data/Gemfile +3 -1
  5. data/LICENSE +1 -1
  6. data/README.md +148 -27
  7. data/Rakefile +36 -10
  8. data/ci/Dockerfile.base.erb +41 -0
  9. data/ci/Dockerfile.main.erb +7 -0
  10. data/ci/requirements.txt +1 -0
  11. data/docker/setup.sh +15 -0
  12. data/docker/test.sh +7 -0
  13. data/iruby.gemspec +14 -18
  14. data/lib/iruby.rb +19 -3
  15. data/lib/iruby/backend.rb +22 -2
  16. data/lib/iruby/command.rb +76 -13
  17. data/lib/iruby/display.rb +69 -39
  18. data/lib/iruby/formatter.rb +5 -4
  19. data/lib/iruby/input.rb +41 -0
  20. data/lib/iruby/input/README.ipynb +502 -0
  21. data/lib/iruby/input/README.md +299 -0
  22. data/lib/iruby/input/autoload.rb +25 -0
  23. data/lib/iruby/input/builder.rb +67 -0
  24. data/lib/iruby/input/button.rb +47 -0
  25. data/lib/iruby/input/cancel.rb +32 -0
  26. data/lib/iruby/input/checkbox.rb +74 -0
  27. data/lib/iruby/input/date.rb +37 -0
  28. data/lib/iruby/input/field.rb +31 -0
  29. data/lib/iruby/input/file.rb +57 -0
  30. data/lib/iruby/input/form.rb +77 -0
  31. data/lib/iruby/input/label.rb +27 -0
  32. data/lib/iruby/input/multiple.rb +76 -0
  33. data/lib/iruby/input/popup.rb +41 -0
  34. data/lib/iruby/input/radio.rb +59 -0
  35. data/lib/iruby/input/select.rb +59 -0
  36. data/lib/iruby/input/textarea.rb +23 -0
  37. data/lib/iruby/input/widget.rb +34 -0
  38. data/lib/iruby/jupyter.rb +77 -0
  39. data/lib/iruby/kernel.rb +67 -22
  40. data/lib/iruby/ostream.rb +24 -8
  41. data/lib/iruby/session.rb +85 -67
  42. data/lib/iruby/session/cztop.rb +70 -0
  43. data/lib/iruby/session/ffi_rzmq.rb +87 -0
  44. data/lib/iruby/session/mixin.rb +47 -0
  45. data/lib/iruby/session_adapter.rb +66 -0
  46. data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
  47. data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
  48. data/lib/iruby/session_adapter/pyzmq_adapter.rb +77 -0
  49. data/lib/iruby/utils.rb +5 -2
  50. data/lib/iruby/version.rb +1 -1
  51. data/run-test.sh +12 -0
  52. data/tasks/ci.rake +65 -0
  53. data/test/helper.rb +90 -0
  54. data/test/integration_test.rb +22 -11
  55. data/test/iruby/backend_test.rb +37 -0
  56. data/test/iruby/command_test.rb +207 -0
  57. data/test/iruby/jupyter_test.rb +27 -0
  58. data/test/iruby/mime_test.rb +32 -0
  59. data/test/iruby/multi_logger_test.rb +1 -2
  60. data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
  61. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
  62. data/test/iruby/session_adapter/session_adapter_test_base.rb +27 -0
  63. data/test/iruby/session_adapter_test.rb +91 -0
  64. data/test/iruby/session_test.rb +47 -0
  65. data/test/run-test.rb +18 -0
  66. metadata +130 -46
  67. data/.travis.yml +0 -16
  68. data/CONTRIBUTORS +0 -19
  69. 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