1#
2#   irb/multi-irb.rb - multiple irb module
3#   	$Release Version: 0.9.6$
4#   	$Revision: 38515 $
5#   	by Keiju ISHITSUKA(keiju@ruby-lang.org)
6#
7# --
8#
9#
10#
11IRB.fail CantShiftToMultiIrbMode unless defined?(Thread)
12require "thread"
13
14module IRB
15  class JobManager
16    @RCS_ID='-$Id: multi-irb.rb 38515 2012-12-21 05:45:50Z zzak $-'
17
18    # Creates a new JobManager object
19    def initialize
20      # @jobs = [[thread, irb],...]
21      @jobs = []
22      @current_job = nil
23    end
24
25    # The active irb session
26    attr_accessor :current_job
27
28    # The total number of irb sessions, used to set +irb_name+ of the current
29    # Context.
30    def n_jobs
31      @jobs.size
32    end
33
34    # Returns the thread for the given +key+ object, see #search for more
35    # information.
36    def thread(key)
37      th, = search(key)
38      th
39    end
40
41    # Returns the irb session for the given +key+ object, see #search for more
42    # information.
43    def irb(key)
44      _, irb = search(key)
45      irb
46    end
47
48    # Returns the top level thread.
49    def main_thread
50      @jobs[0][0]
51    end
52
53    # Returns the top level irb session.
54    def main_irb
55      @jobs[0][1]
56    end
57
58    # Add the given +irb+ session to the jobs Array.
59    def insert(irb)
60      @jobs.push [Thread.current, irb]
61    end
62
63    # Changes the current active irb session to the given +key+ in the jobs
64    # Array.
65    #
66    # Raises an IrbAlreadyDead exception if the given +key+ is no longer alive.
67    #
68    # If the given irb session is already active, an IrbSwitchedToCurrentThread
69    # exception is raised.
70    def switch(key)
71      th, irb = search(key)
72      IRB.fail IrbAlreadyDead unless th.alive?
73      IRB.fail IrbSwitchedToCurrentThread if th == Thread.current
74      @current_job = irb
75      th.run
76      Thread.stop
77      @current_job = irb(Thread.current)
78    end
79
80    # Terminates the irb sessions specified by the given +keys+.
81    #
82    # Raises an IrbAlreadyDead exception if one of the given +keys+ is already
83    # terminated.
84    #
85    # See Thread#exit for more information.
86    def kill(*keys)
87      for key in keys
88	th, _ = search(key)
89	IRB.fail IrbAlreadyDead unless th.alive?
90	th.exit
91      end
92    end
93
94    # Returns the associated job for the given +key+.
95    #
96    # If given an Integer, it will return the +key+ index for the jobs Array.
97    #
98    # When an instance of Irb is given, it will return the irb session
99    # associated with +key+.
100    #
101    # If given an instance of Thread, it will return the associated thread
102    # +key+ using Object#=== on the jobs Array.
103    #
104    # Otherwise returns the irb session with the same top-level binding as the
105    # given +key+.
106    #
107    # Raises a NoSuchJob exception if no job can be found with the given +key+.
108    def search(key)
109      job = case key
110      when Integer
111	@jobs[key]
112      when Irb
113	@jobs.find{|k, v| v.equal?(key)}
114      when Thread
115	@jobs.assoc(key)
116      else
117	@jobs.find{|k, v| v.context.main.equal?(key)}
118      end
119      IRB.fail NoSuchJob, key if job.nil?
120      job
121    end
122
123    # Deletes the job at the given +key+.
124    def delete(key)
125      case key
126      when Integer
127	IRB.fail NoSuchJob, key unless @jobs[key]
128	@jobs[key] = nil
129      else
130	catch(:EXISTS) do
131	  @jobs.each_index do
132	    |i|
133	    if @jobs[i] and (@jobs[i][0] == key ||
134			     @jobs[i][1] == key ||
135			     @jobs[i][1].context.main.equal?(key))
136	      @jobs[i] = nil
137	      throw :EXISTS
138	    end
139	  end
140	  IRB.fail NoSuchJob, key
141	end
142      end
143      until assoc = @jobs.pop; end unless @jobs.empty?
144      @jobs.push assoc
145    end
146
147    # Outputs a list of jobs, see the irb command +irb_jobs+, or +jobs+.
148    def inspect
149      ary = []
150      @jobs.each_index do
151	|i|
152	th, irb = @jobs[i]
153	next if th.nil?
154
155	if th.alive?
156	  if th.stop?
157	    t_status = "stop"
158	  else
159	    t_status = "running"
160	  end
161	else
162	  t_status = "exited"
163	end
164	ary.push format("#%d->%s on %s (%s: %s)",
165			i,
166			irb.context.irb_name,
167			irb.context.main,
168			th,
169			t_status)
170      end
171      ary.join("\n")
172    end
173  end
174
175  @JobManager = JobManager.new
176
177  # The current JobManager in the session
178  def IRB.JobManager
179    @JobManager
180  end
181
182  # The current Context in this session
183  def IRB.CurrentContext
184    IRB.JobManager.irb(Thread.current).context
185  end
186
187  # Creates a new IRB session, see Irb.new.
188  #
189  # The optional +file+ argument is given to Context.new, along with the
190  # workspace created with the remaining arguments, see WorkSpace.new
191  def IRB.irb(file = nil, *main)
192    workspace = WorkSpace.new(*main)
193    parent_thread = Thread.current
194    Thread.start do
195      begin
196	irb = Irb.new(workspace, file)
197      rescue
198	print "Subirb can't start with context(self): ", workspace.main.inspect, "\n"
199	print "return to main irb\n"
200	Thread.pass
201	Thread.main.wakeup
202	Thread.exit
203      end
204      @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
205      @JobManager.insert(irb)
206      @JobManager.current_job = irb
207      begin
208	system_exit = false
209	catch(:IRB_EXIT) do
210	  irb.eval_input
211	end
212      rescue SystemExit
213	system_exit = true
214	raise
215	#fail
216      ensure
217	unless system_exit
218	  @JobManager.delete(irb)
219	  if @JobManager.current_job == irb
220	    if parent_thread.alive?
221	      @JobManager.current_job = @JobManager.irb(parent_thread)
222	      parent_thread.run
223	    else
224	      @JobManager.current_job = @JobManager.main_irb
225	      @JobManager.main_thread.run
226	    end
227	  end
228	end
229      end
230    end
231    Thread.stop
232    @JobManager.current_job = @JobManager.irb(Thread.current)
233  end
234
235#   class Context
236#     def set_last_value(value)
237#       @last_value = value
238#       @workspace.evaluate "_ = IRB.JobManager.irb(Thread.current).context.last_value"
239#       if @eval_history #and !@__.equal?(@last_value)
240# 	@eval_history_values.push @line_no, @last_value
241# 	@workspace.evaluate "__ = IRB.JobManager.irb(Thread.current).context.instance_eval{@eval_history_values}"
242#       end
243#       @last_value
244#     end
245#   end
246
247#  module ExtendCommand
248#     def irb_context
249#       IRB.JobManager.irb(Thread.current).context
250#     end
251# #    alias conf irb_context
252#   end
253
254  @CONF[:SINGLE_IRB_MODE] = false
255  @JobManager.insert(@CONF[:MAIN_CONTEXT].irb)
256  @JobManager.current_job = @CONF[:MAIN_CONTEXT].irb
257
258  class Irb
259    def signal_handle
260      unless @context.ignore_sigint?
261	print "\nabort!!\n" if @context.verbose?
262	exit
263      end
264
265      case @signal_status
266      when :IN_INPUT
267	print "^C\n"
268	IRB.JobManager.thread(self).raise RubyLex::TerminateLineInput
269      when :IN_EVAL
270	IRB.irb_abort(self)
271      when :IN_LOAD
272	IRB.irb_abort(self, LoadAbort)
273      when :IN_IRB
274	# ignore
275      else
276	# ignore other cases as well
277      end
278    end
279  end
280
281  trap("SIGINT") do
282    @JobManager.current_job.signal_handle
283    Thread.stop
284  end
285
286end
287