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.
- checksums.yaml +5 -5
- data/.travis.yml +35 -10
- data/Gemfile +4 -0
- data/README.md +115 -84
- data/Rakefile +26 -0
- data/ci/Dockerfile.base.erb +41 -0
- data/ci/Dockerfile.main.erb +9 -0
- data/ci/requirements.txt +1 -0
- data/docker/setup.sh +15 -0
- data/docker/test.sh +7 -0
- data/iruby.gemspec +4 -2
- data/lib/iruby.rb +13 -7
- data/lib/iruby/command.rb +67 -11
- data/lib/iruby/input/README.md +299 -0
- data/lib/iruby/jupyter.rb +76 -0
- data/lib/iruby/kernel.rb +49 -9
- data/lib/iruby/ostream.rb +4 -0
- data/lib/iruby/session.rb +116 -0
- data/lib/iruby/session/cztop.rb +4 -0
- data/lib/iruby/session/rbczmq.rb +5 -1
- data/lib/iruby/session_adapter.rb +68 -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 +76 -0
- data/lib/iruby/session_adapter/rbczmq_adapter.rb +33 -0
- data/lib/iruby/utils.rb +1 -2
- data/lib/iruby/version.rb +1 -1
- data/run-test.sh +12 -0
- data/tasks/ci.rake +65 -0
- data/test/integration_test.rb +22 -10
- data/test/iruby/command_test.rb +208 -0
- data/test/iruby/jupyter_test.rb +28 -0
- data/test/iruby/multi_logger_test.rb +1 -1
- 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/rbczmq_adapter_test.rb +37 -0
- data/test/iruby/session_adapter/session_adapter_test_base.rb +29 -0
- data/test/iruby/session_adapter_test.rb +116 -0
- data/test/iruby/session_test.rb +53 -0
- data/test/test_helper.rb +44 -1
- 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
|
data/lib/iruby/kernel.rb
CHANGED
@@ -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,
|
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 =
|
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
|
102
|
-
{
|
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
|
106
|
-
|
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
|
data/lib/iruby/ostream.rb
CHANGED
@@ -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
|
data/lib/iruby/session/cztop.rb
CHANGED
data/lib/iruby/session/rbczmq.rb
CHANGED
@@ -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
|
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
|