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