iruby 0.2.7 → 0.5.0

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