iruby 0.3 → 0.7.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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ubuntu.yml +62 -0
  3. data/CHANGES.md +203 -0
  4. data/Gemfile +3 -1
  5. data/LICENSE +1 -1
  6. data/README.md +137 -87
  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 +14 -8
  15. data/lib/iruby/backend.rb +38 -10
  16. data/lib/iruby/command.rb +67 -15
  17. data/lib/iruby/display.rb +77 -41
  18. data/lib/iruby/event_manager.rb +40 -0
  19. data/lib/iruby/formatter.rb +3 -3
  20. data/lib/iruby/input.rb +6 -6
  21. data/lib/iruby/input/README.md +299 -0
  22. data/lib/iruby/input/autoload.rb +1 -1
  23. data/lib/iruby/input/builder.rb +4 -4
  24. data/lib/iruby/input/button.rb +2 -2
  25. data/lib/iruby/input/cancel.rb +1 -1
  26. data/lib/iruby/input/checkbox.rb +3 -3
  27. data/lib/iruby/input/date.rb +3 -3
  28. data/lib/iruby/input/field.rb +2 -2
  29. data/lib/iruby/input/file.rb +3 -3
  30. data/lib/iruby/input/form.rb +6 -6
  31. data/lib/iruby/input/label.rb +4 -4
  32. data/lib/iruby/input/multiple.rb +10 -10
  33. data/lib/iruby/input/popup.rb +2 -2
  34. data/lib/iruby/input/radio.rb +6 -6
  35. data/lib/iruby/input/select.rb +8 -8
  36. data/lib/iruby/input/textarea.rb +1 -1
  37. data/lib/iruby/input/widget.rb +2 -2
  38. data/lib/iruby/jupyter.rb +77 -0
  39. data/lib/iruby/kernel.rb +204 -36
  40. data/lib/iruby/ostream.rb +29 -8
  41. data/lib/iruby/session.rb +117 -0
  42. data/lib/iruby/session/cztop.rb +4 -0
  43. data/lib/iruby/session_adapter.rb +72 -0
  44. data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
  45. data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
  46. data/lib/iruby/session_adapter/pyzmq_adapter.rb +77 -0
  47. data/lib/iruby/session_adapter/test_adapter.rb +49 -0
  48. data/lib/iruby/utils.rb +13 -2
  49. data/lib/iruby/version.rb +1 -1
  50. data/run-test.sh +12 -0
  51. data/tasks/ci.rake +65 -0
  52. data/test/helper.rb +136 -0
  53. data/test/integration_test.rb +22 -11
  54. data/test/iruby/backend_test.rb +37 -0
  55. data/test/iruby/command_test.rb +207 -0
  56. data/test/iruby/event_manager_test.rb +92 -0
  57. data/test/iruby/jupyter_test.rb +27 -0
  58. data/test/iruby/kernel_test.rb +185 -0
  59. data/test/iruby/mime_test.rb +50 -0
  60. data/test/iruby/multi_logger_test.rb +1 -5
  61. data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
  62. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
  63. data/test/iruby/session_adapter/session_adapter_test_base.rb +27 -0
  64. data/test/iruby/session_adapter_test.rb +91 -0
  65. data/test/iruby/session_test.rb +48 -0
  66. data/test/run-test.rb +19 -0
  67. metadata +120 -50
  68. data/.travis.yml +0 -16
  69. data/CHANGES +0 -143
  70. data/CONTRIBUTORS +0 -19
  71. data/lib/iruby/session/rbczmq.rb +0 -68
  72. data/test/test_helper.rb +0 -5
data/lib/iruby/kernel.rb CHANGED
@@ -1,36 +1,126 @@
1
1
  module IRuby
2
+ ExecutionInfo = Struct.new(:raw_cell, :store_history, :silent)
3
+
2
4
  class Kernel
3
5
  RED = "\e[31m"
4
- WHITE = "\e[37m"
5
6
  RESET = "\e[0m"
6
7
 
7
- class<< self
8
+ @events = EventManager.new([:initialized])
9
+
10
+ class << self
11
+ # Return the event manager defined in the `IRuby::Kernel` class.
12
+ # This event manager can handle the following event:
13
+ #
14
+ # - `initialized`: The event occurred after the initialization of
15
+ # a `IRuby::Kernel` instance is finished
16
+ #
17
+ # @example Registering initialized event
18
+ # IRuby::Kernel.events.register(:initialized) do |result|
19
+ # STDERR.puts "IRuby kernel has been initialized"
20
+ # end
21
+ #
22
+ # @see IRuby::EventManager
23
+ # @see IRuby::Kernel#events
24
+ attr_reader :events
25
+
26
+ # Returns the singleton kernel instance
8
27
  attr_accessor :instance
9
28
  end
10
29
 
30
+ # Returns a session object
11
31
  attr_reader :session
12
32
 
13
- def initialize(config_file)
33
+ EVENTS = [
34
+ :pre_execute,
35
+ :pre_run_cell,
36
+ :post_run_cell,
37
+ :post_execute
38
+ ].freeze
39
+
40
+ def initialize(config_file, session_adapter_name=nil)
14
41
  @config = MultiJson.load(File.read(config_file))
15
42
  IRuby.logger.debug("IRuby kernel start with config #{@config}")
16
43
  Kernel.instance = self
17
44
 
18
- @session = Session.new(@config)
45
+ @session = Session.new(@config, session_adapter_name)
19
46
  $stdout = OStream.new(@session, :stdout)
20
47
  $stderr = OStream.new(@session, :stderr)
21
48
 
49
+ init_parent_process_poller
50
+
51
+ @events = EventManager.new(EVENTS)
22
52
  @execution_count = 0
23
- @backend = create_backend
53
+ @backend = PlainBackend.new
24
54
  @running = true
55
+
56
+ self.class.events.trigger(:initialized, self)
25
57
  end
26
58
 
27
- def create_backend
28
- PryBackend.new
29
- rescue Exception => e
30
- IRuby.logger.warn "Could not load PryBackend: #{e.message}\n#{e.backtrace.join("\n")}" unless LoadError === e
31
- PlainBackend.new
59
+ # Returns the event manager defined in a `IRuby::Kernel` instance.
60
+ # This event manager can handle the following events:
61
+ #
62
+ # - `pre_execute`: The event occurred before running the code
63
+ #
64
+ # - `pre_run_cell`: The event occurred before running the code and
65
+ # if the code execution is not silent
66
+ #
67
+ # - `post_execute`: The event occurred after running the code
68
+ #
69
+ # - `post_run_cell`: The event occurred after running the code and
70
+ # if the code execution is not silent
71
+ #
72
+ # The callback functions of `pre_run_cell` event must take one argument
73
+ # to get an `ExecutionInfo` object.
74
+ # The callback functions of `post_run_cell` event must take one argument
75
+ # to get the result of the code execution.
76
+ #
77
+ # @example Registering post_run_cell event
78
+ # IRuby::Kernel.instance.events.register(:post_run_cell) do |result|
79
+ # STDERR.puts "The result of the last execution: %p" % result
80
+ # end
81
+ #
82
+ # @see IRuby::EventManager
83
+ # @see IRuby::ExecutionInfo
84
+ # @see IRuby::Kernel.events
85
+ attr_reader :events
86
+
87
+ # Switch the backend (interactive shell) system
88
+ #
89
+ # @param backend [:irb,:plain,:pry] Specify the backend name switched to
90
+ #
91
+ # @return [true,false] true if the switching succeeds, otherwise false
92
+ def switch_backend!(backend)
93
+ name = case backend
94
+ when String, Symbol
95
+ name = backend.downcase
96
+ else
97
+ name = backend
98
+ end
99
+
100
+ backend_class = case name
101
+ when :irb, :plain
102
+ PlainBackend
103
+ when :pry
104
+ PryBackend
105
+ else
106
+ raise ArgumentError,
107
+ "Unknown backend name: %p" % backend
108
+ end
109
+
110
+ begin
111
+ new_backend = backend_class.new
112
+ @backend = new_backend
113
+ true
114
+ rescue Exception => e
115
+ unless LoadError === e
116
+ IRuby.logger.warn "Could not load #{backend_class}: " +
117
+ "#{e.message}\n#{e.backtrace.join("\n")}"
118
+ end
119
+ return false
120
+ end
32
121
  end
33
122
 
123
+ # @private
34
124
  def run
35
125
  send_status :starting
36
126
  while @running
@@ -38,8 +128,10 @@ module IRuby
38
128
  end
39
129
  end
40
130
 
131
+ # @private
41
132
  def dispatch
42
133
  msg = @session.recv(:reply)
134
+ IRuby.logger.debug "Kernel#dispatch: msg = #{msg}"
43
135
  type = msg[:header]['msg_type']
44
136
  raise "Unknown message type: #{msg.inspect}" unless type =~ /comm_|_request\Z/ && respond_to?(type)
45
137
  begin
@@ -50,31 +142,54 @@ module IRuby
50
142
  end
51
143
  rescue Exception => e
52
144
  IRuby.logger.debug "Kernel error: #{e.message}\n#{e.backtrace.join("\n")}"
53
- @session.send(:publish, :error, error_message(e))
145
+ @session.send(:publish, :error, error_content(e))
54
146
  end
55
147
 
148
+ # @private
56
149
  def kernel_info_request(msg)
57
150
  @session.send(:reply, :kernel_info_reply,
58
151
  protocol_version: '5.0',
59
152
  implementation: 'iruby',
60
- banner: "IRuby #{IRuby::VERSION}",
61
153
  implementation_version: IRuby::VERSION,
62
154
  language_info: {
63
155
  name: 'ruby',
64
156
  version: RUBY_VERSION,
65
157
  mimetype: 'application/x-ruby',
66
158
  file_extension: '.rb'
67
- })
159
+ },
160
+ banner: "IRuby #{IRuby::VERSION} (with #{@session.description})",
161
+ help_links: [
162
+ {
163
+ text: "Ruby Documentation",
164
+ url: "https://ruby-doc.org/"
165
+ }
166
+ ],
167
+ status: :ok)
68
168
  end
69
169
 
170
+ # @private
70
171
  def send_status(status)
172
+ IRuby.logger.debug "Send status: #{status}"
71
173
  @session.send(:publish, :status, execution_state: status)
72
174
  end
73
175
 
176
+ # @private
74
177
  def execute_request(msg)
75
178
  code = msg[:content]['code']
76
- @execution_count += 1 if msg[:content]['store_history']
77
- @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
179
+ store_history = msg[:content]['store_history']
180
+ silent = msg[:content]['silent']
181
+
182
+ @execution_count += 1 if store_history
183
+
184
+ unless silent
185
+ @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
186
+ end
187
+
188
+ events.trigger(:pre_execute)
189
+ unless silent
190
+ exec_info = ExecutionInfo.new(code, store_history, silent)
191
+ events.trigger(:pre_run_cell, exec_info)
192
+ end
78
193
 
79
194
  content = {
80
195
  status: :ok,
@@ -82,30 +197,49 @@ module IRuby
82
197
  user_expressions: {},
83
198
  execution_count: @execution_count
84
199
  }
200
+
85
201
  result = nil
86
202
  begin
87
- result = @backend.eval(code, msg[:content]['store_history'])
203
+ result = @backend.eval(code, store_history)
88
204
  rescue SystemExit
89
205
  content[:payload] << { source: :ask_exit }
90
206
  rescue Exception => e
91
- content = error_message(e)
207
+ content = error_content(e)
92
208
  @session.send(:publish, :error, content)
209
+ content[:status] = :error
210
+ content[:execution_count] = @execution_count
211
+ end
212
+
213
+ unless result.nil? || silent
214
+ @session.send(:publish, :execute_result,
215
+ data: Display.display(result),
216
+ metadata: {},
217
+ execution_count: @execution_count)
93
218
  end
219
+
220
+ events.trigger(:post_execute)
221
+ events.trigger(:post_run_cell, result) unless silent
222
+
94
223
  @session.send(:reply, :execute_reply, content)
95
- @session.send(:publish, :execute_result,
96
- data: Display.display(result),
97
- metadata: {},
98
- execution_count: @execution_count) unless result.nil? || msg[:content]['silent']
99
224
  end
100
225
 
101
- def error_message(e)
102
- { status: :error,
103
- ename: e.class.to_s,
226
+ # @private
227
+ def error_content(e)
228
+ rindex = e.backtrace.rindex{|line| line.start_with?(@backend.eval_path)} || -1
229
+ backtrace = SyntaxError === e && rindex == -1 ? [] : e.backtrace[0..rindex]
230
+ { ename: e.class.to_s,
104
231
  evalue: e.message,
105
- traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *e.backtrace.map { |l| "#{WHITE}#{l}#{RESET}" }],
106
- execution_count: @execution_count }
232
+ traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *backtrace] }
233
+ end
234
+
235
+ # @private
236
+ def is_complete_request(msg)
237
+ # FIXME: the code completeness should be judged by using ripper or other Ruby parser
238
+ @session.send(:reply, :is_complete_reply,
239
+ status: :unknown)
107
240
  end
108
241
 
242
+ # @private
109
243
  def complete_request(msg)
110
244
  # HACK for #26, only complete last line
111
245
  code = msg[:content]['code']
@@ -115,51 +249,85 @@ module IRuby
115
249
  end
116
250
  @session.send(:reply, :complete_reply,
117
251
  matches: @backend.complete(code),
118
- status: :ok,
119
252
  cursor_start: start.to_i,
120
- cursor_end: msg[:content]['cursor_pos'])
253
+ cursor_end: msg[:content]['cursor_pos'],
254
+ metadata: {},
255
+ status: :ok)
121
256
  end
122
257
 
258
+ # @private
123
259
  def connect_request(msg)
124
260
  @session.send(:reply, :connect_reply, Hash[%w(shell_port iopub_port stdin_port hb_port).map {|k| [k, @config[k]] }])
125
261
  end
126
262
 
263
+ # @private
127
264
  def shutdown_request(msg)
128
265
  @session.send(:reply, :shutdown_reply, msg[:content])
129
266
  @running = false
130
267
  end
131
268
 
269
+ # @private
132
270
  def history_request(msg)
133
271
  # we will just send back empty history for now, pending clarification
134
272
  # as requested in ipython/ipython#3806
135
273
  @session.send(:reply, :history_reply, history: [])
136
274
  end
137
275
 
276
+ # @private
138
277
  def inspect_request(msg)
139
- result = @backend.eval(msg[:content]['code'])
140
- @session.send(:reply, :inspect_reply,
141
- status: :ok,
142
- data: Display.display(result),
143
- metadata: {})
144
- rescue Exception => e
145
- IRuby.logger.warn "Inspection error: #{e.message}\n#{e.backtrace.join("\n")}"
146
- @session.send(:reply, :inspect_reply, status: :error)
278
+ # not yet implemented. See (#119).
279
+ @session.send(:reply, :inspect_reply, status: :ok, found: false, data: {}, metadata: {})
147
280
  end
148
281
 
282
+ # @private
149
283
  def comm_open(msg)
150
284
  comm_id = msg[:content]['comm_id']
151
285
  target_name = msg[:content]['target_name']
152
286
  Comm.comm[comm_id] = Comm.target[target_name].new(target_name, comm_id)
153
287
  end
154
288
 
289
+ # @private
155
290
  def comm_msg(msg)
156
291
  Comm.comm[msg[:content]['comm_id']].handle_msg(msg[:content]['data'])
157
292
  end
158
293
 
294
+ # @private
159
295
  def comm_close(msg)
160
296
  comm_id = msg[:content]['comm_id']
161
297
  Comm.comm[comm_id].handle_close(msg[:content]['data'])
162
298
  Comm.comm.delete(comm_id)
163
299
  end
300
+
301
+ private
302
+
303
+ def init_parent_process_poller
304
+ pid = ENV.fetch('JPY_PARENT_PID', 0).to_i
305
+ return unless pid > 1
306
+
307
+ case RUBY_PLATFORM
308
+ when /mswin/, /mingw/
309
+ # TODO
310
+ else
311
+ @parent_poller = start_parent_process_pollar_unix
312
+ end
313
+ end
314
+
315
+ def start_parent_process_pollar_unix
316
+ Thread.start do
317
+ IRuby.logger.warn("parent process poller thread started.")
318
+ loop do
319
+ begin
320
+ current_ppid = Process.ppid
321
+ if current_ppid == 1
322
+ IRuby.logger.warn("parent process appears to exited, shutting down.")
323
+ exit!(1)
324
+ end
325
+ sleep 1
326
+ rescue Errno::EINTR
327
+ # ignored
328
+ end
329
+ end
330
+ end
331
+ end
164
332
  end
165
333
  end
data/lib/iruby/ostream.rb CHANGED
@@ -25,22 +25,43 @@ module IRuby
25
25
  alias_method :next, :read
26
26
  alias_method :readline, :read
27
27
 
28
- def write(s)
29
- raise 'I/O operation on closed file' unless @session
30
- @session.send(:publish, :stream, name: @name, text: s.to_s)
31
- nil
28
+ def write(*obj)
29
+ str = build_string { |sio| sio.write(*obj) }
30
+ session_send(str)
32
31
  end
33
32
  alias_method :<<, :write
34
33
  alias_method :print, :write
35
34
 
36
- def puts(*lines)
37
- lines = [''] if lines.empty?
38
- lines.each { |s| write("#{s}\n")}
39
- nil
35
+ def printf(format, *obj)
36
+ str = build_string { |sio| sio.printf(format, *obj) }
37
+ session_send(str)
38
+ end
39
+
40
+ def puts(*obj)
41
+ str = build_string { |sio| sio.puts(*obj) }
42
+ session_send(str)
40
43
  end
41
44
 
42
45
  def writelines(lines)
43
46
  lines.each { |s| write(s) }
44
47
  end
48
+
49
+ # Called by irb
50
+ def set_encoding(extern, intern)
51
+ a = extern
52
+ end
53
+
54
+ private
55
+
56
+ def build_string
57
+ StringIO.open { |sio| yield(sio); sio.string }
58
+ end
59
+
60
+ def session_send(str)
61
+ raise 'I/O operation on closed file' unless @session
62
+
63
+ @session.send(:publish, :stream, name: @name, text: str)
64
+ nil
65
+ end
45
66
  end
46
67
  end
@@ -0,0 +1,117 @@
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
+ @last_recvd_msg = nil
14
+
15
+ setup
16
+ setup_sockets
17
+ setup_heartbeat
18
+ setup_security
19
+ end
20
+
21
+ attr_reader :adapter, :config
22
+
23
+ def description
24
+ "#{@adapter.name} session adapter"
25
+ end
26
+
27
+ def setup
28
+ end
29
+
30
+ def setup_sockets
31
+ protocol, host = config.values_at('transport', 'ip')
32
+ shell_port = config['shell_port']
33
+ iopub_port = config['iopub_port']
34
+ stdin_port = config['stdin_port']
35
+
36
+ @shell_socket, @shell_port = @adapter.make_router_socket(protocol, host, shell_port)
37
+ @iopub_socket, @iopub_port = @adapter.make_pub_socket(protocol, host, iopub_port)
38
+ @stdin_socket, @stdin_port = @adapter.make_router_socket(protocol, host, stdin_port)
39
+
40
+ @sockets = {
41
+ publish: @iopub_socket,
42
+ reply: @shell_socket,
43
+ stdin: @stdin_socket
44
+ }
45
+ end
46
+
47
+ def setup_heartbeat
48
+ protocol, host = config.values_at('transport', 'ip')
49
+ hb_port = config['hb_port']
50
+ @hb_socket, @hb_port = @adapter.make_rep_socket(protocol, host, hb_port)
51
+ @heartbeat_thread = Thread.start do
52
+ begin
53
+ # NOTE: this loop is copied from CZTop's old session code
54
+ @adapter.heartbeat_loop(@hb_socket)
55
+ rescue Exception => e
56
+ IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
57
+ end
58
+ end
59
+ end
60
+
61
+ def setup_security
62
+ @session_id = SecureRandom.uuid
63
+ unless config['key'].empty? || config['signature_scheme'].empty?
64
+ unless config['signature_scheme'] =~ /\Ahmac-/
65
+ raise "Unknown signature_scheme: #{config['signature_scheme']}"
66
+ end
67
+ digest_algorithm = config['signature_scheme'][/\Ahmac-(.*)\Z/, 1]
68
+ @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new(digest_algorithm))
69
+ end
70
+ end
71
+
72
+ def send(socket_type, message_type, content)
73
+ sock = check_socket_type(socket_type)
74
+ idents = if socket_type == :reply && @last_recvd_msg
75
+ @last_recvd_msg[:idents]
76
+ else
77
+ message_type == :stream ? "stream.#{content[:name]}" : message_type
78
+ end
79
+ header = {
80
+ msg_type: message_type,
81
+ msg_id: SecureRandom.uuid,
82
+ username: 'kernel',
83
+ session: @session_id,
84
+ version: '5.0'
85
+ }
86
+ @adapter.send(sock, serialize(idents, header, content))
87
+ end
88
+
89
+ def recv(socket_type)
90
+ sock = check_socket_type(socket_type)
91
+ data = @adapter.recv(sock)
92
+ @last_recvd_msg = unserialize(data)
93
+ end
94
+
95
+ def recv_input
96
+ sock = check_socket_type(:stdin)
97
+ data = @adapter.recv(sock)
98
+ unserialize(data)[:content]["value"]
99
+ end
100
+
101
+ private
102
+
103
+ def check_socket_type(socket_type)
104
+ case socket_type
105
+ when :publish, :reply, :stdin
106
+ @sockets[socket_type]
107
+ else
108
+ raise ArgumentError, "Invalid socket type #{socket_type}"
109
+ end
110
+ end
111
+
112
+ def create_session_adapter(config, adapter_name)
113
+ adapter_class = SessionAdapter.select_adapter_class(adapter_name)
114
+ adapter_class.new(config)
115
+ end
116
+ end
117
+ end