#!/usr/bin/env ruby
# gl_tail.rb v0.01 - OpenGL visualization of your server traffic
# Copyright 2007 Erlend Simonsen <mr@fudgie.org>
#
# Licensed under the GPLv2
#
# I know, horrible code and global variables, and what not. But, it works,
# and it's too fun to watch my servers traffic in real time to clean up
# everything before releasing it.
#
# Installation instructions (Ubuntu/Debian):
#   sudo apt-get install libopengl-ruby rubygems
#   sudo gem install -y net-ssh -r
#
# Installation instructions (Mac OS/X):
#   <install ruby & rubygems>
#   sudo gem install -y ruby-opengl net-ssh -r
#   (You might need option 2 or older)
#   (You might need to replace
#      require 'glut'
#    with
#      require 'glut_prev'
#   )
#
# Configuration:
#   Modify $SERVERS, $BLOCKS & $PARSERS to your liking. Either include a
#   :password for each of your servers, or make sure your ssh-keys are
#   correctly set up.
#
# Running:
#   ./gl_tail.rb
#
# Changelog:
#   06 Oct 2007 - v0.01 Initial release
#   07 Oct 2007 - v0.02 Postfix parser
#                       IIS parser (Tucker Sizemore <w8emx@sera.org>)
#   08 Oct 2007 - v0.03 Update Apache Combined parser to handle hostnames
#                       Handle message rates > (60000/FPS)
#
#
# Further ideas:
#   Get rid of GLUT.BitmapCharacter and use textured polygons instead
#   Allow more indicators (pulsing color/size, cubes, teapots, etc)
#   Clickable links
#   Drag 'n drop organizing
#   Hide/show blocks with keypresses
#   Limit display to specific host
#   Background IP lookups
#   Geolocation on IPS
#

require 'rubygems'

require 'opengl'
if RUBY_PLATFORM == "i386-mswin32"
  require 'glut_prev'
else
  require 'glut'
end
#require 'cgi'
#require 'resolv'

require_gem 'net-ssh'


#################
### Configuration
###

ENV['__GL_SYNC_TO_VBLANK']="1"
$WINDOW_WIDTH = 1200
$WINDOW_HEIGHT = 760

# $MIN_BLOB_SIZE = 0.07
$MIN_BLOB_SIZE = 0.05
$MAX_BLOB_SIZE = 0.6

$SERVERS = [
            # List of machines to log in to via SSH, and which files to tail for traffic data.
            {:name => 'server1', :host => 'server1.example.com', :user => 'joeuser', :password => 'secret', :command => 'tail -f', :files => ['/var/log/apache/access_log'], :color => [0.6, 0.6, 1.0, 1.0], :parser => :apache },
            {:name => 'server2', :host => 'login.mycoolsite.com', :user => 'otheruser', :password => 'othersecret', :port => 22222, :command => 'xtail', :files => ['/usr/local/www/apps/myapp/current/log/production.log'], :color => [0.1, 0.6, 0.6, 1.0], :parser => :rails },
            {:name => 'mail', :host => 'mail.spamme.com', :user => 'otheruser', :password => 'othersecret', :command => 'tail -f', :files => ['/var/log/maillog'], :color => [0.8, 1.0, 0.0, 1.0], :parser => :postfix },
           ]

$BLOCKS = [
           # Sections with different information to display on the screen
           { :name => 'info',          :position => :left,  :order => 0, :size => 10, :auto_clean => false, :show => :total },
           { :name => 'sites',         :position => :left,  :order => 1, :size => 10 },
           { :name => 'content',       :position => :left,  :order => 2, :size => 5, :show => :total, :color => [1.0, 0.8, 0.4, 1.0] },
           { :name => 'status',        :position => :left,  :order => 3, :size => 10, :color => [1.0, 0.8, 0.4, 1.0] },
           { :name => 'users',         :position => :left,  :order => 4, :size => 10 },
           { :name => 'smtp',          :position => :left,  :order => 5, :size => 5 },

           { :name => 'urls',          :position => :right, :order => 0, :size => 15 },
           { :name => 'slow requests', :position => :right, :order => 1, :size => 5, :show => :average },
           { :name => 'referrers',     :position => :right, :order => 2, :size => 10 },
           { :name => 'user agents',   :position => :right, :order => 3, :size => 5, :color => [1.0, 1.0, 1.0, 1.0] },
           { :name => 'mail',        :position => :right, :order => 4, :size => 5 },
           ]

$PARSERS = {
  # Parser which handles access_logs in combined format from Apache
  :apache => Proc.new { |server,line|
    _, host, user, domain, date, url, status, size, referrer, useragent = /^([\d\S.]+) (\S+) (\S+) \[([^\]]+)\] \"(.+?)\" (\d+) ([\S]+) \"([^\"]+)\" \"([^\"]+)\"/.match(line).to_a

    unless host
      _, host, user, domain, date, url, status, size = /^([\d\S.]+) (\S+) (\S+) \[([^\]]+)\] \"(.+?)\" (\d+) ([\S]+)/.match(line).to_a
    end

    if host
      method, url, http_version = url.split(" ")

      url, parameters = url.split('?')

      server.add_activity(:block => 'sites', :name => server.name, :size => size.to_i/1000000.0) # Size of activity based on size of request
      server.add_activity(:block => 'urls', :name => url)
      server.add_activity(:block => 'users', :name => host, :size => size.to_i/1000000.0)
      server.add_activity(:block => 'referrers', :name => referrer) unless (referrer.nil? || referrer.include?(server.name) || referrer.include?(server.host))
      server.add_activity(:block => 'user agents', :name => useragent, :type => 3) unless useragent.nil?

      if( url.include?('.gif') || url.include?('.jpg') || url.include?('.png') || url.include?('.ico'))
        type = 'image'
      elsif url.include?('.css')
        type = 'css'
      elsif url.include?('.js')
        type = 'javascript'
      elsif url.include?('.swf')
        type = 'flash'
      elsif( url.include?('.avi') || url.include?('.ogm') || url.include?('.flv') || url.include?('.mpg') )
        type = 'movie'
      elsif( url.include?('.mp3') || url.include?('.wav') || url.include?('.fla') || url.include?('.aac') || url.include?('.ogg'))
        type = 'music'
      else
        type = 'page'
      end
      server.add_activity(:block => 'content', :name => type)
      server.add_activity(:block => 'status', :name => status, :type => 3) # don't show a blob

      # Events to pop up
      server.add_event(:block => 'info', :name => "Logins", :message => "Login...", :update_stats => true, :color => [1.5, 1.0, 0.5, 1.0]) if method == "POST" && url.include?('login')
      server.add_event(:block => 'info', :name => "Sales", :message => "$", :update_stats => true, :color => [1.5, 0.0, 0.0, 1.0]) if method == "POST" && url.include?('/checkout')
      server.add_event(:block => 'info', :name => "Signups", :message => "New User...", :update_stats => true, :color => [1.0, 1.0, 1.0, 1.0]) if( method == "POST" && (url.include?('/signup') || url.include?('/users/create')))
    end
  },

  :rails => Proc.new { |server,line|
    #Completed in 0.02100 (47 reqs/sec) | Rendering: 0.01374 (65%) | DB: 0.00570 (27%) | 200 OK [http://example.com/whatever/whatever]
    _, ms, url = /^Completed in ([\d.]+) .* \[([^\]]+)\]/.match(line).to_a


    if url
      _, host, url = /^http[s]?:\/\/([^\/]+)(.*)/.match(url).to_a

      server.add_activity(:block => 'sites', :name => host, :size => ms.to_f) # Size of activity based on request time.
      server.add_activity(:block => 'urls', :name => url, :size => ms.to_f)
      server.add_activity(:block => 'slow requests', :name => url, :size => ms.to_f)
      server.add_activity(:block => 'content', :name => 'page')

      # Events to pop up
      server.add_event(:block => 'info', :name => "Logins", :message => "Login...", :update_stats => true, :color => [0.5, 1.0, 0.5, 1.0]) if url.include?('/login')
      server.add_event(:block => 'info', :name => "Sales", :message => "$", :update_stats => true, :color => [1.5, 0.0, 0.0, 1.0]) if url.include?('/checkout')
      server.add_event(:block => 'info', :name => "Signups", :message => "New User...", :update_stats => true, :color => [1.0, 1.0, 1.0, 1.0]) if(url.include?('/signup') || url.include?('/users/create'))
    elsif line.include?('Processing ')
      #Processing TasksController#update_sheet_info (for 123.123.123.123 at 2007-10-05 22:34:33) [POST]
      _, host = /^Processing .* \(for (\d+.\d+.\d+.\d+) at .*\).*$/.match(line).to_a
      if host
        server.add_activity(:block => 'users', :name => host)
      end
    elsif line.include?('Error (')
      _, error, msg = /^([^ ]+Error) \((.*)\):/.match(line).to_a
      if error
        server.add_event(:block => 'info', :name => "Exceptions", :message => error, :update_stats => true, :color => [1.0, 0.0, 0.0, 1.0])
        server.add_event(:block => 'info', :name => "Exceptions", :message => msg, :update_stats => false, :color => [1.0, 0.0, 0.0, 1.0])
      end
    end
  },

  :postfix => Proc.new { |server,line|

    if line.include?(': connect from')
      _, host, ip = /: connect from ([^\[]+)\[(\d+.\d+.\d+.\d+)\]/.match(line).to_a
      if host
        host = ip if host == 'unknown'
        server.add_activity(:block => 'smtp', :name => host, :size => 0.03)
      end
    elsif line.include?(' from=<')
      _, from, size = /: from=<([^>]+)>, size=(\d+)/.match(line).to_a
      if from
        server.add_activity(:block => 'mail', :name => from, :size => size.to_f/100000.0)
      end
    elsif line.include?(' to=<')
      if line.include?('relay=local')
        # Incoming
        _, to, delay, status = /: to=<([^>]+)>, .*delay=([\d.]+).*status=([^ ]+)/.match(line).to_a
        server.add_activity(:block => 'mail', :name => to, :size => delay.to_f/10.0, :type => 5, :color => [1.0, 0.0, 1.0, 1.0])
        server.add_activity(:block => 'status', :name => 'received', :size => delay.to_f/10.0, :type => 3)
      else
        # Outgoing
        _, to, relay_host, delay, status = /: to=<([^>]+)>, relay=([^\[,]+).*delay=([\d.]+).*status=([^ ]+)/.match(line).to_a
        server.add_activity(:block => 'mail', :name => to, :size => delay.to_f/10.0)
        server.add_activity(:block => 'smtp', :name => relay_host, :size => delay.to_f/10.0)
        server.add_activity(:block => 'status', :name => status, :size => delay.to_f/10.0, :type => 3)
      end
    end

  },

  :iis => Proc.new { |server,line|
_, date, time,serverip, url, referrer, port, size, host, useragent, status = /^([\d-]+) ([\d:]+) ([\d.]+) (.+? .+?) (\S+) (.+?) (\S+) ([\d.]+) (.+?) (\d+) (.*)$/.match(line).to_a

    if host
      method, url, http_version = url.split(" ")

      url, parameters = url.split('?')

      server.add_activity(:block => 'sites', :name => server.name, :size => size.to_i/1000000.0) # Size of activity based on size of request
      server.add_activity(:block => 'urls', :name => url)
      server.add_activity(:block => 'users', :name => host, :size => size.to_i/1000000.0)
      server.add_activity(:block => 'referrers', :name => referrer) unless (referrer.include?(server.name) || referrer.include?(server.host))
      server.add_activity(:block => 'user agents', :name => useragent, :type => 3)

      if( url.include?('.gif') || url.include?('.jpg') || url.include?('.png') || url.include?('.ico'))
        type = 'image'
      elsif url.include?('.css')
        type = 'css'
      elsif url.include?('.js')
        type = 'javascript'
      elsif url.include?('.swf')
        type = 'flash'
      elsif( url.include?('.avi') || url.include?('.ogm') || url.include?('.flv') || url.include?('.mpg') )
        type = 'movie'
      elsif( url.include?('.mp3') || url.include?('.wav') || url.include?('.fla') || url.include?('.aac') || url.include?('.ogg'))
        type = 'music'
      else
        type = 'page'
      end
      server.add_activity(:block => 'content', :name => type)
      server.add_activity(:block => 'status', :name => status, :type => 3) # don't show a blob

      # Events to pop up
      server.add_event(:block => 'info', :name => "Logins", :message => "Login...", :update_stats => true, :color => [1.5, 1.0, 0.5, 1.0]) if method == "POST" && url.include?('login')
      server.add_event(:block => 'info', :name => "Sales", :message => "$", :update_stats => true, :color => [1.5, 0.0, 0.0, 1.0]) if method == "POST" && url.include?('/checkout')
      server.add_event(:block => 'info', :name => "Signups", :message => "New User...", :update_stats => true, :color => [1.0, 1.0, 1.0, 1.0]) if( method == "POST" && (url.include?('/signup') || url.include?('/users/create')))
    end
  }

}

###
### Configuration end
######################
# Lots of hacks and bad code below. :-)

$BLOBS = { }
$FPS = 50.0
$ASPECT = 0.6

$TOP = 11.0
$RIGHT_COL = 11.0
$LEFT_COL = -20.0
$LINE_SIZE = 0.3
$BLOB_OFFSET = 7.0
$STATS = []


class Item
  attr_accessor :message, :size, :color, :type

  def initialize(message, size, color, type)
    @message = message
    @size = size
    @color = color
    @type = type
  end

end

class Activity
  attr_accessor :x, :y, :z, :type, :wx, :wy, :wz

  def initialize(message, x,y,z, color, size, type=0)
    @message = message
    @x, @y, @z = x, y, z
    @xi, @yi, @zi = 0.18 + ( (rand(100)/100.0 - 0.5) * 0.01 ), (rand(100)/100.0 - 0.5) * 0.04, 0
#    @xi, @yi, @zi = 0.18 , 0.03, 0

    if @x >= 0.0
      @xi = -@xi
    end

    @xi = (rand(100)/100.0 * 0.02) - 0.01 if type == 2

    @color = color
    @size  = size
    @type  = type
  end

  def render

    if @type == 5
      dy = @wy - @y
      if dy.abs < 0.01
        @y = @wy
      else
        @y += dy / 20
      end

      dx = @wx - @x
      if dx.abs < 0.03
        @x = @wx
      else
        @x += dx / 20
      end

      if @x == @wx
        @x = 20.0
      end

    else
      @x += @xi
      @y += @yi

      if @y - @size/2 < -$TOP
        @y = -$TOP + @size/2
        @yi = -@yi * 0.7
        @x = 30.0 #if @type == 2
      end

      @yi -= 0.003

    end

    if @type == 0 || @type == 5
      GL.PushMatrix()
      GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, @color)
      GL.Translate(@x, @y, @z)


      if( $BLOBS[@size].nil? )
        list = GL.GenLists(1)
        GL.NewList(list, GL::COMPILE)
        GLUT.SolidSphere(@size, 10, 2)
        GL.EndList()
        $BLOBS[@size] = list
      end
      GL.CallList($BLOBS[@size])
      GL.PopMatrix()
    elsif @type == 1
      GL.PushMatrix()
      GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, @color)
      GL.Translate(@x, @y, @z)

      if( $BLOBS[@size].nil? )
        list = GL.GenLists(1)
        GL.NewList(list, GL::COMPILE)
        GLUT.SolidSphere(@size, 10, 2)
        GL.EndList()
        $BLOBS[@size] = list
      end
      GL.CallList($BLOBS[@size])
      GL.PopMatrix()
    elsif @type == 2
      GL.PushMatrix()
      GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, [@color[0]*10.0, @color[1]*10.0, @color[2]*10.0, @color[3]])
      GL.Translate(@x, @y, @z)
      GL.RasterPos(0.0, 0.0)

      if( $BLOBS[@message].nil? )
        list = GL.GenLists(1)
        GL.NewList(list, GL::COMPILE)
        @message.each_byte do |c| GLUT.BitmapCharacter(GLUT::BITMAP_8_BY_13, c) end
        GL.EndList()
        $BLOBS[@message] = list
      end
      GL.CallList($BLOBS[@message])
      GL.PopMatrix()
    end
  end
end

class Element
  attr_accessor :wy, :active
  attr_reader   :rate, :messages, :name, :activities, :queue, :updates, :average, :total

  def initialize(name, color, type = 0, right = false, start_position = -$TOP)
    @name = name
    @right = right
    @x = (right ? $RIGHT_COL : $LEFT_COL)
    @y = start_position
    @z = 0
    @wy = start_position

    @color = color
    @size = 0.01
    @queue = []
    @pending = []
    @activities = []
    @messages = 0
    @rate = 0
    @total = 0
    @sum = 0
    @average = 0.0
    @last_time = 0
    @step = 0, @updates = 0
    @active = false
    @type = type
  end

  def add_activity(message, size, type)
    @pending.push Item.new(message, size, @color, type) unless type == 3
    @total += 1
    @messages += 1
    @sum += size
    @average = @sum / @total

    if @rate == 0
      @rate = 1.0 / 60
      @messages = 0
    end
  end

  def add_event(message, update_stats)
    @pending.push Item.new(message, 0.01, @color, 2)
    if update_stats
      @total += 1
      @messages += 1
      if @rate == 0
        @rate = 1.0 / 60
        @messages = 0
      end
    end
  end


  def update
    @active = true if @total > 0
    @updates += 1
    @rate = (@rate.to_f * 59 + @messages) / 60
    @messages = 0
    if @pending.size + @queue.size> 0
      if @pending.size + @queue.size == 1
        @step = rand(1000) * 1.0
      else
        @step = 1.0 / (@queue.size + @pending.size) * 1000.0
      end
      @queue = @queue + @pending
      @pending = []
    else
      @step = 0
    end
    @last_time = GLUT.Get(GLUT::ELAPSED_TIME)
#    @last_time -= @step unless @queue.size == 1
  end

  def render(options = { })
    @x = (@right ? $RIGHT_COL : $LEFT_COL)

    d = @wy - @y
    if d.abs < 0.001
      @y = @wy
    else
      @y += d / 20
    end

    GL.PushMatrix()
    GL.Material(GL::FRONT, GL::AMBIENT_AND_DIFFUSE, ( @queue.size > 0 ? [10.0, 1.0, 1.0, 1.0] : @color ))
    GL.Translate(@x, @y, @z)
    GL.RasterPos(0.0, 0.0)

    if @type == 0
      if @rate < 0.0001
        txt = "   r/m "
      else
        txt = "#{sprintf("%6.2f",@rate * 60)} "
      end
    elsif @type == 1
      if @total == 0
        txt = " total "
      else
        txt = "#{sprintf("%6d",@total)} "
      end
    elsif @type == 2
      if @average == 0
        txt = "   avg "
      else
        txt = "#{sprintf("%6.2f",@average)} "
      end
    end

    if( $BLOBS[txt].nil? )
      list = GL.GenLists(1)
      GL.NewList(list, GL::COMPILE)
      txt.each_byte do |c| GLUT.BitmapCharacter(GLUT::BITMAP_8_BY_13, c) end
#        txt.each_byte do |c| GLUT.BitmapCharacter(GLUT::BITMAP_HELVETICA_10, c) end
      GL.EndList()
      $BLOBS[txt] = list
    end

    if( $BLOBS[@name].nil? )
      list = GL.GenLists(1)
      GL.NewList(list, GL::COMPILE)
      if @x < 0
        sprintf("%35s ", @name).each_byte do |c| GLUT.BitmapCharacter(GLUT::BITMAP_8_BY_13, c) end
      else
        @name.each_byte do |c| GLUT.BitmapCharacter(GLUT::BITMAP_8_BY_13, c) end
      end
      GL.EndList()
      $BLOBS[@name] = list
    end

    if @x < 0
      GL.CallList($BLOBS[@name])
      GL.CallList($BLOBS[txt])
    else
      GL.CallList($BLOBS[txt])
      GL.CallList($BLOBS[@name])
    end

    GL.PopMatrix()

    t = GLUT.Get(GLUT::ELAPSED_TIME)
    num = 0
    while( (@queue.size > 0) && (@last_time < t ) )
#    if( (@queue.size > 0) && (@last_time < t ) )
#      if num > 0
#        puts "num[#{num}], q[#{@queue.size}], time[#{@last_time}] + step[#{@step}] < t[#{t}]"
#      end

      @last_time += @step
      item = @queue.pop
      url = item.message
      color = item.color
      size = item.size
      type = item.type

      if size < $MIN_BLOB_SIZE
        size = $MIN_BLOB_SIZE
      elsif size > $MAX_BLOB_SIZE
        size = $MAX_BLOB_SIZE
      end

      if type == 2
        @activities.push Activity.new(url, 0.0 - (0.043 * url.length), $TOP, 0.0, color, size, type)
      elsif type == 5
        a = Activity.new(url, 0.0, $TOP, 0.0, color, size, type)
        a.wx = @x
        a.wy = @y + 0.05
        @activities.push a
      elsif type != 4
        if @x >= 0
          @activities.push Activity.new(url, @x, @y, @z, color, size, type)
        else
          @activities.push Activity.new(url, @x + $BLOB_OFFSET, @y, @z, color, size, type)
        end
      end
      num += 1
    end
#    @last_time = GLUT.Get(GLUT::ELAPSED_TIME)

    @activities.each do |a|
      if a.x > 18.0 || a.x < -18.0
        @activities.delete a
      else
        a.wy = @y + 0.05 if a.type == 5
        a.render
        $STATS[1] += 1
      end
    end

  end
end

class Block
  attr_reader :name, :position, :order, :bottom_position

  def initialize(options)
    @name = options[:name]
    @position = options[:position] || :left
    @size = options[:size] || 10
    @clean = options[:auto_clean] || true
    @order = options[:order] || 100
    @color = options[:color]

    @show = case options[:show]
             when :rate: 0
             when :total: 1
             when :average: 2
            else
              0
            end

    @header = Element.new(@name.upcase , [1.0, 1.0, 1.0, 1.0], @show, @position == :right)

    @elements = { }
    @bottom_position = -$TOP
  end

  def render(num)
    return num if @elements.size == 0

    @header.wy = $TOP - (num * $LINE_SIZE)
    @header.render
    num += 1

    sorted = case @show
               when 0: @elements.values.sort { |k,v| v.rate <=> k.rate}[0..@size-1]
               when 1: @elements.values.sort { |k,v| v.total <=> k.total}[0..@size-1]
               when 2: @elements.values.sort { |k,v| v.average <=> k.average}[0..@size-1]
             end

    sorted.each do |e|
      e.wy = $TOP - (num * $LINE_SIZE)
      e.render
      $STATS[0] += 1
      if e.rate <= 0.0001 && e.active && e.updates > 4
        @elements.delete(e.name)
      end
      num += 1
    end
    (@elements.values - sorted).each do |e|
      $STATS[0] += 1
      e.activities.each do |a|
        a.render
        if a.x > 18.0 || a.x < -18.0
          e.activities.delete a
        end
      end
      if e.activities.size == 0 && @clean && e.updates > 4
        @elements.delete(e.name)
      end
    end
    @elements.delete_if { |k,v| (!sorted.include? v) && v.active && v.activities.size == 0} if @clean
    @bottom_position = $TOP - ((sorted.size > 0 ? (num-1) : num) * $LINE_SIZE)
    num + 1
  end

  def add_activity(options = { })
    return if options[:name].nil?
    @elements[options[:name]] ||= Element.new(options[:name], @color || options[:color], @show, @position == :right, @bottom_position)
    @elements[options[:name]].add_activity(options[:message], options[:size] || 0.01, options[:type] || 0 )
  end

  def add_event(options = { })
    @elements[options[:name]] ||= Element.new(options[:name], options[:color], @show, @position == :right)
    @elements[options[:name]].add_event(options[:message], options[:update_stats] || false)
  end

  def update
    @elements.each_value do |e|
      e.update
    end
  end
end

class Server
  attr_reader :name, :host, :color, :parser

  def initialize(options)
    @name = options[:name] || options[:host]
    @host = options[:host]
    @color = options[:color] || [1.0, 1.0, 1.0, 1.0]
    @parser = $PARSERS[options[:parser]] || $PARSERS[:apache]
    @blocks = options[:blocks]
  end

  #block, message, size
  def add_activity(options = { })
    block = @blocks[options[:block]].add_activity( { :name => @name, :color => @color, :size => 0.03 }.update(options) )
  end

  #block, message
  def add_event(options = { })
    block = @blocks[options[:block]].add_event( { :name => @name, :color => @color, :size => 0.03}.update(options) )
  end

end

class GlTail

  def draw
    GL.Clear(GL::COLOR_BUFFER_BIT);
#    GL.Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);

    GL.PushMatrix()

    positions = Hash.new

    $STATS = [0,0]

    @blocks.values.sort { |k,v| k.order <=> v.order}.each do |block|
      positions[block.position] = block.render( positions[block.position] || 0 )
    end

    GL.PopMatrix()
    GLUT.SwapBuffers()

    @frames = 0 if not defined? @frames
    @t0 = 0 if not defined? @t0

    @frames += 1
    t = GLUT.Get(GLUT::ELAPSED_TIME)
    if t - @t0 >= 5000
      seconds = (t - @t0) / 1000.0
      $FPS = @frames / seconds
      printf("%d frames in %6.3f seconds = %6.3f FPS\n",
             @frames, seconds, $FPS)
      @t0, @frames = t, 0
      puts "Elements[#{$STATS[0]}], Activities[#{$STATS[1]}]"
    end
  end

  def idle
    GLUT.PostRedisplay()
    do_process
  end

  # Change view angle, exit upon ESC
  def key(k, x, y)
    case k
    when 27 # Escape
      exit
    end
    GLUT.PostRedisplay()
  end

  # Change view angle
  def special(k, x, y)
    GLUT.PostRedisplay()
  end

  # New window size or exposure
  def reshape(width, height)
    $ASPECT = height.to_f / width.to_f

    puts "Reshape: #{width}x#{height} = #{$ASPECT}"

    GL.Viewport(0, 0, width, height)
    GL.MatrixMode(GL::PROJECTION)
    GL.LoadIdentity()

    GL.Frustum(-2.0, 2.0, -$ASPECT*2, $ASPECT*2, 5.0, 60.0)

    $TOP = 19.0 * $ASPECT
    $LINE_SIZE = 0.5 * (1122/height.to_f) * $ASPECT
    $BLOB_OFFSET = 11.6 * (1122/height.to_f) * $ASPECT
    $RIGHT_COL = 18.3 * $ASPECT

    GL.MatrixMode(GL::MODELVIEW)
    GL.LoadIdentity()
    GL.Translate(0.0, 0.0, -50.0)
  end

  def init
    GL.Lightfv(GL::LIGHT0, GL::POSITION, [5.0, 5.0, 10.0, 0.0])
    GL.Disable(GL::CULL_FACE)
    GL.Enable(GL::LIGHTING)
    GL.Enable(GL::LIGHT0)

    GL.Disable(GL::DEPTH_TEST)
    GL.Disable(GL::NORMALIZE)

    @channels = Array.new
    @sessions = Array.new
    @servers = Hash.new
    @blocks = Hash.new
    @mode = 0

    $BLOCKS.each do |b|
      @blocks[b[:name]] = Block.new b
    end

    $SERVERS.each do |s|
      puts "Connecting to #{s[:host]}..."
      session_options = { }
      session_options[:port] = s[:port] if s[:port]
#      session_options[:verbose] = :debug

      if s[:password]
        session = Net::SSH.start(s[:host], s[:user], s[:password], session_options)
      else
        session = Net::SSH.start(s[:host], s[:user], session_options)
      end
      do_tail session, s[:name], s[:color], s[:files].join(" "), s[:command]
      session.connection.process
      @sessions.push session
      @servers[s[:name]] ||= Server.new(:name => s[:name] || s[:host], :host => s[:host], :color => s[:color], :parser => s[:parser], :blocks => @blocks )
    end

    @since = GLUT.Get(GLUT::ELAPSED_TIME)
  end

  def visible(vis)
    GLUT.IdleFunc((vis == GLUT::VISIBLE ? method(:idle).to_proc : nil))
  end

  def mouse(button, state, x, y)
    @mouse = state
    @x0, @y0 = x, y
  end

  def motion(x, y)
    if @mouse == GLUT::DOWN then
    end
    @x0, @y0 = x, y
  end

  def initialize
    GLUT.Init()
    GLUT.InitDisplayMode(GLUT::RGB | GLUT::DOUBLE)
    GLUT.InitDisplayMode(GLUT::RGB | GLUT::DEPTH | GLUT::DOUBLE)

    GLUT.InitWindowPosition(0, 0)
    GLUT.InitWindowSize($WINDOW_WIDTH, $WINDOW_HEIGHT)
    GLUT.CreateWindow('glTail')
    init()

    GLUT.DisplayFunc(method(:draw).to_proc)
    GLUT.ReshapeFunc(method(:reshape).to_proc)
    GLUT.KeyboardFunc(method(:key).to_proc)
    GLUT.SpecialFunc(method(:special).to_proc)
    GLUT.VisibilityFunc(method(:visible).to_proc)
    GLUT.MouseFunc(method(:mouse).to_proc)
    GLUT.MotionFunc(method(:motion).to_proc)
  end

  def start
    GLUT.MainLoop()
  end

  def parse_line( ch, data )
    ch[:buffer].gsub(/\r\n/,"\n").gsub(/\n/, "\n\n").each("") do |line|

      unless line.include? "\n\n"
        ch[:buffer] = "#{line}"
        next
      end

      line.gsub!(/\n\n/, "\n")
      line.gsub!(/\n\n/, "\n")

      server = @servers.values.find { |v| (v.host == ch[:host]) && (v.name == ch[:name]) }
      server.parser.call(server, line)
    end
    ch[:buffer] = "" if ch[:buffer].include? "\n"
  end

  def do_tail( session, name, color, file, command )
    session.open_channel do |channel|
      puts "Channel opened on #{session.host}...\n"
      channel[:host] = session.host
      channel[:name] = name
      channel[:color] = color
      channel[:buffer] = ""
      channel.request_pty :want_reply => true

      channel.on_data do |ch, data|
        ch[:buffer] << data
        parse_line(ch, data)
      end

      channel.on_success do |ch|
        channel.exec "#{command} #{file}  "
      end

      channel.on_failure do |ch|
        ch.close
      end

      channel.on_extended_data do |ch, data|
        puts "STDERR: #{data}\n"
      end

      channel.on_close do |ch|
        ch[:closed] = true
      end

      puts "Pushing #{channel[:host]}\n"
      @channels.push(channel)
    end
  end

  def do_process
    active = 0
    @channels.each do |ch|
      active += 1
      ch.connection.process(true)
    end

    break if active == 0

    if GLUT.Get(GLUT::ELAPSED_TIME) - @since >= 1000
      @since = GLUT.Get(GLUT::ELAPSED_TIME)
      @channels.each { |ch| ch.connection.ping! }

      @blocks.each_value do |b|
        b.update
      end
    end
    self
  end

end

GlTail.new.start
