iruby 0.3 → 0.4.0

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