##################  threads.rb -- brent@mbari.org  #####################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/threads.rb,v $
# $Id: threads.rb,v 1.34 2005/11/02 19:26:31 brent Exp $
#
#  Mixes into Thread class support for managing heirarchies of threads.
#  Enhanced threads store references to their parents and children.
#  parents are the thread(s) that created them (normally only one).
#  children are the thread(s) they in turn create.
#  Just before a thread dies due to an unhandled exception,
#    it raises ChildDied exceptions in all its parents and
#    ParentDied exceptions in all its children
#  Also includes methods to report in detail on threads' status
#    listing Locks they are awaiting and Locks they are holding
#
#  Selected Thread Class Methods:
#    create => a new enhanced thread
#    Thread (:name) {block} => synonymous with create
#    Thread[:name] => newest thread named :name
#
#  Selected Thread Instance Methods:
#    parents => array of parent threads
#    children => array of child threads
#    birthdate => Time at which thread was created
#    name => Name assigned when thread was created
#
##########################################################################

require 'mbari'
require 'thread'

class Exception
  def raiseInAll threads
    threads.shift.raise self until threads.empty?
  end
end

class Thread

  def self.exclusive  #faster than the version in thread.rb
    old = self.critical
    begin
      self.critical = true
      yield
    ensure
      self.critical = old
    end
  end

  if defined?(IRB) && !defined? (MaxExceptionDepth)
    MaxExceptionDepth = 4  #max # of unhandled exceptions to enqueue
  end

  class Died < ThreadError
    def initialize (msg="Thread Died", cause=nil, thread=Thread.current)
      @threadId = thread.id  #this is a poor man's WeakRef
      @cause = cause
      super (msg)
    end  
    attr_reader :cause

    def thread
      begin
        ObjectSpace._id2ref @threadId
      rescue RangeError
        raise ThreadError, "Garbage Collected"
      end
    end
    
    def rootCause
    #return the root cause of thread's death
      root = @cause
      root = root.cause while root.respond_to?(:cause) &&
                              root.cause.kind_of?(Exception)
      root
    end
  end
  class ChildDied < Died; end
  class ParentDied < Died; end
  class Aborted < Died; end


# we do not redefine :initialize because we must cope with threads
# that were created previously.  So, the new instance variables are
# created when first accessed.  (Ain't ruby grand :-)

  def parents
    return @parents if @parents
    @parents = []
  end
  
  def children
    return @children if @children
    @children = []
  end
  
  def eachChild (&childProc)
    #invoke childProc on each thread indirectly created by this one
    children.each {|child| 
      childProc[child]
      child.eachChild (&childProc)
    }
  end
  
  def name
    return @name unless @name.nil?
    id
  end
    
  def name= threadName
    @name = threadName
  end

  @@lastBirthdate = 0.0  #seconds since the start of epoch
  def birthdate
    unless @birthdate
      @birthdate = Time.now.to_f
      #to insure that thread birthdate are monotonically increasing
      @birthdate += Time::EPS if @birthdate == @@lastBirthdate
      @@lastBirthdate = @birthdate
    end
    Time.at @birthdate
  end
      
  def inspect
  #replaces the builtin inspect method
    "#<%s:%s priority=%d %s>" % [type, name.inspect, priority, status] 
  end
  alias_method :to_s, :inspect
  
  def <=> other
    birthdate <=> other.birthdate
  end
    
  def abort (msg="Aborted by "+Thread.current.name.to_s)
  #abort thread (somewhat gracefully) 
    raise Thread::Aborted.new (msg, name, self)
  end


  if defined? MaxExceptionDepth
  
    def exception
      return @exception if @exception
      @exception = []
    end

    def exception= err
      exception << err
      @exception.shift if @exception.size > MaxExceptionDepth
    end
    def self.exception= err
      current.exception = err
    end
    def lastErr
    #the root cause of the last exception recorded for this thread
      last=exception.last
      last.rootCause if last
    end
    def self.lastErr
      current.lastErr
    end
    
  else
  
    def exception= err
    end
    def self.exception= err
    end
    
  end

  def finish
  #like value, but works properly even if another thread runs 
  #the current thread while it is waiting for this one to finish
    while (result = value; alive?)
    end
    result
  end


  def install parent, childName=id
  #install this thread as a child of indicated parent
    birthdate  #side effect is to set the birthdate if it hasn't already been
    self.name=childName
    parents << parent
    parent.children << self
  end

  def runChild procedure, args=[], log=type
  #initialize thread, execute procedure with args and handle exceptions
  #executes the required initialization block before the procedure parameter
    begin
      yield
      procedure[*args]
    rescue Exception => unhandled
      children.delete my=Thread.current
      myName = my.name.to_s
      begin
        my.exception = unhandled
        log.recordException unhandled, my
      rescue Exception #ignore exceptions that occur while trying to report one
        $stderr.puts caller, $!  #last ditch effort to give log the error
      end
      begin
        ParentDied.new(myName, unhandled).raiseInAll my.children
        ChildDied.new (myName, unhandled).raiseInAll my.parents
      rescue
#$stderr.puts $!
        retry
      end
    ensure
      children.delete my=Thread.current
      my.children.clear
      my.parents.clear
      type.critical=false
    end
  end


  class << self  #class methods

    def create (name=nil, *args, &block)
    #start a new thread after linking it into the inheritence tree
      self.critical=true #don't allow parent to continue until child is running
      new(current) do |parent|
        parent.runChild(block,args) do
          current.install parent, name
          self.critical=false
        end
      end
    end

    def recordException unhandled, thread=Thread.current, console=$stderr
    #log an unhandled exception for specified thread on console
      thread.exception=unhandled    
      console.puts "#{unhandled.type} in #{thread.name} -- #{unhandled}"
    end
 
    rename_method :unsortedList, :list
    def list (key=nil)
    #return the Threads named key sorted by birthdates
    #if no key supplied, return all runnable threads
      return unsortedList.sort unless key 
      threads = unsortedList.find_all {|t| t.name == key}
      if threads.empty?
        ObjectSpace.each_object (Thread) {|dead|threads<<dead}
        threads = threads.find_all {|t| t.name == key}
      end
      threads.sort
    end
  
    def [] (key=nil)
    # the matching thread with most recent birthdate
    # if no key supplied, the most recently created thread
      list(key).last
    end
    
    def details
    #display detailed status of all threads
      Thread.list.each {|t| t.details}
      nil
    end
  
    def parents
      Thread.current.parents
    end
    def children
      Thread.current.children
    end
  end #Thread class methods
    
  
  def details
  #display detailed thread status including all locks in which it is referenced
    puts inspect
    print "Born: ", birthdate, "\n" if @birthdate
    print "Parents: ", parents.inspect, "\n" if @parents
    print "Children: ", children.inspect, "\n" if @children
    if defined? Lock
      lcks = locks 
      unless lcks.empty?     
        lcks.each {|lock| print "Referenced by: "; lock.details}
      end
    end
    nil
  end
  
end

Thread.main.birthdate
Thread.main.name=:MAIN

module Kernel
  def Thread (name, *args, &block)
    Thread.create (name, *args, &block)
  end
end

