iruby 0.3 → 0.4.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 (41) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +35 -10
  3. data/Gemfile +4 -0
  4. data/README.md +115 -84
  5. data/Rakefile +26 -0
  6. data/ci/Dockerfile.base.erb +41 -0
  7. data/ci/Dockerfile.main.erb +9 -0
  8. data/ci/requirements.txt +1 -0
  9. data/docker/setup.sh +15 -0
  10. data/docker/test.sh +7 -0
  11. data/iruby.gemspec +4 -2
  12. data/lib/iruby.rb +13 -7
  13. data/lib/iruby/command.rb +67 -11
  14. data/lib/iruby/input/README.md +299 -0
  15. data/lib/iruby/jupyter.rb +76 -0
  16. data/lib/iruby/kernel.rb +49 -9
  17. data/lib/iruby/ostream.rb +4 -0
  18. data/lib/iruby/session.rb +116 -0
  19. data/lib/iruby/session/cztop.rb +4 -0
  20. data/lib/iruby/session/rbczmq.rb +5 -1
  21. data/lib/iruby/session_adapter.rb +68 -0
  22. data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
  23. data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
  24. data/lib/iruby/session_adapter/pyzmq_adapter.rb +76 -0
  25. data/lib/iruby/session_adapter/rbczmq_adapter.rb +33 -0
  26. data/lib/iruby/utils.rb +1 -2
  27. data/lib/iruby/version.rb +1 -1
  28. data/run-test.sh +12 -0
  29. data/tasks/ci.rake +65 -0
  30. data/test/integration_test.rb +22 -10
  31. data/test/iruby/command_test.rb +208 -0
  32. data/test/iruby/jupyter_test.rb +28 -0
  33. data/test/iruby/multi_logger_test.rb +1 -1
  34. data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
  35. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
  36. data/test/iruby/session_adapter/rbczmq_adapter_test.rb +37 -0
  37. data/test/iruby/session_adapter/session_adapter_test_base.rb +29 -0
  38. data/test/iruby/session_adapter_test.rb +116 -0
  39. data/test/iruby/session_test.rb +53 -0
  40. data/test/test_helper.rb +44 -1
  41. metadata +72 -12
@@ -0,0 +1,76 @@
1
+ module IRuby
2
+ module Jupyter
3
+ class << self
4
+ # User's default kernelspec directory is described here:
5
+ # https://jupyter.readthedocs.io/en/latest/projects/jupyter-directories.html
6
+ def default_data_dir
7
+ case
8
+ when windows?
9
+ appdata = windows_user_appdata
10
+ if !appdata.empty?
11
+ File.join(appdata, 'jupyter')
12
+ else
13
+ jupyter_config_dir = ENV.fetch('JUPYTER_CONFIG_DIR', File.expand_path('~/.jupyter'))
14
+ File.join(jupyter_config_dir, 'data')
15
+ end
16
+ when apple?
17
+ File.expand_path('~/Library/Jupyter')
18
+ else
19
+ xdg_data_home = ENV.fetch('XDG_DATA_HOME', '')
20
+ data_home = xdg_data_home[0] ? xdg_data_home : File.expand_path('~/.local/share')
21
+ File.join(data_home, 'jupyter')
22
+ end
23
+ end
24
+
25
+ def kernelspec_dir(data_dir=nil)
26
+ data_dir ||= default_data_dir
27
+ File.join(data_dir, 'kernels')
28
+ end
29
+
30
+ private
31
+
32
+ # returns %APPDATA%
33
+ def windows_user_appdata
34
+ check_windows
35
+ path = Fiddle::Pointer.malloc(2 * 300) # uint16_t[300]
36
+ csidl_appdata = 0x001a
37
+ case call_SHGetFolderPathW(Fiddle::NULL, csidl_appdata, Fiddle::NULL, 0, path)
38
+ when 0
39
+ len = (1 ... (path.size/2)).find {|i| path[2*i, 2] == "\0\0" }
40
+ path = path.to_str(2*len).encode(Encoding::UTF_8, Encoding::UTF_16LE)
41
+ else
42
+ ENV.fetch('APPDATA', '')
43
+ end
44
+ end
45
+
46
+ def call_SHGetFolderPathW(hwnd, csidl, hToken, dwFlags, pszPath)
47
+ require 'fiddle/import'
48
+ shell32 = Fiddle::Handle.new('shell32')
49
+ func = Fiddle::Function.new(
50
+ shell32['SHGetFolderPathW'],
51
+ [
52
+ Fiddle::TYPE_VOIDP,
53
+ Fiddle::TYPE_INT,
54
+ Fiddle::TYPE_VOIDP,
55
+ Fiddle::TYPE_INT,
56
+ Fiddle::TYPE_VOIDP
57
+ ],
58
+ Fiddle::TYPE_INT,
59
+ Fiddle::Importer.const_get(:CALL_TYPE_TO_ABI)[:stdcall])
60
+ func.(hwnd, csidl, hToken, dwFlags, pszPath)
61
+ end
62
+
63
+ def check_windows
64
+ raise 'the current platform is not Windows' unless windows?
65
+ end
66
+
67
+ def windows?
68
+ /mingw|mswin/ =~ RUBY_PLATFORM
69
+ end
70
+
71
+ def apple?
72
+ /darwin/ =~ RUBY_PLATFORM
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,7 +1,6 @@
1
1
  module IRuby
2
2
  class Kernel
3
3
  RED = "\e[31m"
4
- WHITE = "\e[37m"
5
4
  RESET = "\e[0m"
6
5
 
7
6
  class<< self
@@ -19,6 +18,8 @@ module IRuby
19
18
  $stdout = OStream.new(@session, :stdout)
20
19
  $stderr = OStream.new(@session, :stderr)
21
20
 
21
+ init_parent_process_poller
22
+
22
23
  @execution_count = 0
23
24
  @backend = create_backend
24
25
  @running = true
@@ -40,6 +41,7 @@ module IRuby
40
41
 
41
42
  def dispatch
42
43
  msg = @session.recv(:reply)
44
+ IRuby.logger.debug "Kernel#dispatch: msg = #{msg}"
43
45
  type = msg[:header]['msg_type']
44
46
  raise "Unknown message type: #{msg.inspect}" unless type =~ /comm_|_request\Z/ && respond_to?(type)
45
47
  begin
@@ -50,14 +52,14 @@ module IRuby
50
52
  end
51
53
  rescue Exception => e
52
54
  IRuby.logger.debug "Kernel error: #{e.message}\n#{e.backtrace.join("\n")}"
53
- @session.send(:publish, :error, error_message(e))
55
+ @session.send(:publish, :error, error_content(e))
54
56
  end
55
57
 
56
58
  def kernel_info_request(msg)
57
59
  @session.send(:reply, :kernel_info_reply,
58
60
  protocol_version: '5.0',
59
61
  implementation: 'iruby',
60
- banner: "IRuby #{IRuby::VERSION}",
62
+ banner: "IRuby #{IRuby::VERSION} (with #{@session.description})",
61
63
  implementation_version: IRuby::VERSION,
62
64
  language_info: {
63
65
  name: 'ruby',
@@ -68,6 +70,7 @@ module IRuby
68
70
  end
69
71
 
70
72
  def send_status(status)
73
+ IRuby.logger.debug "Send status: #{status}"
71
74
  @session.send(:publish, :status, execution_state: status)
72
75
  end
73
76
 
@@ -88,8 +91,9 @@ module IRuby
88
91
  rescue SystemExit
89
92
  content[:payload] << { source: :ask_exit }
90
93
  rescue Exception => e
91
- content = error_message(e)
94
+ content = error_content(e)
92
95
  @session.send(:publish, :error, content)
96
+ content[:status] = :error
93
97
  end
94
98
  @session.send(:reply, :execute_reply, content)
95
99
  @session.send(:publish, :execute_result,
@@ -98,12 +102,16 @@ module IRuby
98
102
  execution_count: @execution_count) unless result.nil? || msg[:content]['silent']
99
103
  end
100
104
 
101
- def error_message(e)
102
- { status: :error,
103
- ename: e.class.to_s,
105
+ def error_content(e)
106
+ { ename: e.class.to_s,
104
107
  evalue: e.message,
105
- traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *e.backtrace.map { |l| "#{WHITE}#{l}#{RESET}" }],
106
- execution_count: @execution_count }
108
+ traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *e.backtrace] }
109
+ end
110
+
111
+ def is_complete_request(msg)
112
+ # FIXME: the code completeness should be judged by using ripper or other Ruby parser
113
+ @session.send(:reply, :is_complete_reply,
114
+ status: :unknown)
107
115
  end
108
116
 
109
117
  def complete_request(msg)
@@ -161,5 +169,37 @@ module IRuby
161
169
  Comm.comm[comm_id].handle_close(msg[:content]['data'])
162
170
  Comm.comm.delete(comm_id)
163
171
  end
172
+
173
+ private
174
+
175
+ def init_parent_process_poller
176
+ pid = ENV.fetch('JPY_PARENT_PID', 0).to_i
177
+ return unless pid > 1
178
+
179
+ case RUBY_PLATFORM
180
+ when /mswin/, /mingw/
181
+ # TODO
182
+ else
183
+ @parent_poller = start_parent_process_pollar_unix
184
+ end
185
+ end
186
+
187
+ def start_parent_process_pollar_unix
188
+ Thread.start do
189
+ IRuby.logger.warn("parent process poller thread started.")
190
+ loop do
191
+ begin
192
+ current_ppid = Process.ppid
193
+ if current_ppid == 1
194
+ IRuby.logger.warn("parent process appears to exited, shutting down.")
195
+ exit!(1)
196
+ end
197
+ sleep 1
198
+ rescue Errno::EINTR
199
+ # ignored
200
+ end
201
+ end
202
+ end
203
+ end
164
204
  end
165
205
  end
@@ -33,6 +33,10 @@ module IRuby
33
33
  alias_method :<<, :write
34
34
  alias_method :print, :write
35
35
 
36
+ def printf(*fmt)
37
+ write sprintf(*fmt)
38
+ end
39
+
36
40
  def puts(*lines)
37
41
  lines = [''] if lines.empty?
38
42
  lines.each { |s| write("#{s}\n")}
@@ -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
@@ -36,6 +36,10 @@ module IRuby
36
36
  end
37
37
  end
38
38
 
39
+ def description
40
+ 'old-stle session using cztop'
41
+ end
42
+
39
43
  # Build and send a message
40
44
  def send(socket, type, content)
41
45
  idents =
@@ -38,6 +38,10 @@ module IRuby
38
38
  end
39
39
  end
40
40
 
41
+ def description
42
+ 'old-stle session using rbczmq'
43
+ end
44
+
41
45
  # Build and send a message
42
46
  def send(socket, type, content)
43
47
  idents =
@@ -53,7 +57,7 @@ module IRuby
53
57
  session: @session,
54
58
  version: '5.0'
55
59
  }
56
- @sockets[socket].send_message(ZMQ::Message(*serialize(idents, header, content)))
60
+ @sockets[socket].send_message(ZMQ.Message(*serialize(idents, header, content)))
57
61
  end
58
62
 
59
63
  # Receive a message and decode it
@@ -0,0 +1,68 @@
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/rbczmq_adapter'
40
+ require_relative 'session_adapter/pyzmq_adapter'
41
+
42
+ def self.select_adapter_class(name=nil)
43
+ classes = {
44
+ 'ffi-rzmq' => SessionAdapter::FfirzmqAdapter,
45
+ 'cztop' => SessionAdapter::CztopAdapter,
46
+ 'rbczmq' => SessionAdapter::RbczmqAdapter,
47
+ 'pyzmq' => SessionAdapter::PyzmqAdapter
48
+ }
49
+ if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil))
50
+ cls = classes[name]
51
+ unless cls.available?
52
+ if ENV['IRUBY_SESSION_ADAPTER']
53
+ raise SessionAdapterNotFound,
54
+ "Session adapter `#{name}` from IRUBY_SESSION_ADAPTER is unavailable"
55
+ else
56
+ raise SessionAdapterNotFound,
57
+ "Session adapter `#{name}` is unavailable"
58
+ end
59
+ end
60
+ return cls
61
+ end
62
+ classes.each_value do |cls|
63
+ return cls if cls.available?
64
+ end
65
+ raise SessionAdapterNotFound, "No session adapter is available"
66
+ end
67
+ end
68
+ 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