# ./ppp-time.rb | less
./ppp-time.rb:335: [BUG] gc_sweep(): unknown data type 121
#--------------------------
# ppp-time.rb
#--------------------------

Report generated at May 26 15:54

Abort (core dumped)
# uname -a
Linux shoo.to.you 2.2.5-15 #13 Thu Jan 6 10:59:30 CST 2000 i586 unknown
# ruby -v
ruby 1.4.3 (1999-12-08) [i586-linux-gnu]
#

The code is longish, but here it is....

#!/usr/bin/ruby

# ppp-time.rb
#
# PURPOSE: Scan through ppp.auth and
#          produce statistics and billing
#          reports
#
# DATE: Sat Apr 15 09:17:23 CDT 2000
# PROGRAMMER: David Douthitt

# ppp.auth format:
#
# 948840929:peer1:tty1:in
# 948840933:peer1:tty1:out
# 948852042:ddouthitt:/dev/ttyG0_19:in
# 948852203:ddouthitt:/dev/ttyG0_19:out
# 948853385:ddouthitt:/dev/ttyG0_20:in
# 948859439:jforsmo:/dev/ttyG0_22:in
# 948860584:jforsmo:/dev/ttyG0_22:out

#---------------------------------------------
# GENERIC CLASSES
#---------------------------------------------

class Array
   def each_line
      self.each { |line|
         yield line.chomp
         }
   end
end

class TextFile < File
   def each_line
      super { |line|
         yield line.chomp
         }
   end

   alias each each_line
end

class String
   def fields
      split(":")
   end
end

class Numeric
   def to_mins
      self / 60
   end

   def to_mins_rounded
#     (self + 30) / 60    # round to nearest minute
      (self / 60) + 1     # round to next minute up
   end

   def to_hrs
      self / 60 / 60
   end

   def format_time
      raise ("bad time format!") if self <= 0;

      if (self > 3600)
         sprintf("%d:%02d:%02d", self / 3600, (self / 60) % 60, self % 60);
      else
         sprintf("%d:%02d", self / 60, self % 60);
      end
   end
end

#---------------------------------------------
# SPECIFIC CLASSES
#---------------------------------------------

class PPPAuth < TextFile
   def PPPAuth.open
      super("/var/log/ppp.auth")
   end
end

class Report
   def Report.header
      print "#--------------------------\n";
      print "# ppp-time.rb\n";
      print "#--------------------------\n";

      print "\nReport generated at ",
         Time.now.strftime("%b %d %H:%M"), "\n\n";
   end

end

class System
   attr_accessor :online, :uptime;

   def initialize
      @online = 0;
      @uptime = 0;
   end

   def upsince (time)
      @uptime = (Time.now - Time.at(time)).to_i;
   end

   def summary
      printf("   Total online time:  %10s\n", @online.to_mins.format_time);
      printf("   Total uptime:       %10s  * since first recorded login\n\n", @uptime.to_mins.format_time);
      printf("   Total online usage: %10.2f%%\n\n", @online * 100.0 / @uptime);
   end
end

class Passwd

# /etc/passwd file format:
#
# Go look for yourself :-)

   Userdata = Hash.new

   def initialize
      TextFile.open("/etc/passwd") { |f|
         f.readlines.each_line { |line|
#           user, pwd, uid, gid, info, home, shell = line.fields
            user, *data = line.fields
            Userdata[user] = data;
            }
         }
   end

   def Passwd.uid (str)
      raise("user #{str} not in passwd file!") if Userdata[str] == nil;

      (Userdata[str])[1]
   end

   def Passwd.gid (str)
      raise("user #{str} not in passwd file!") if Userdata[str] == nil;

      (Userdata[str])[2]
   end

   def Passwd.info (str)
      raise("user #{str} not in passwd file!") if Userdata[str] == nil;

      (Userdata[str])[3]
   end

   def Passwd.home (str)
      raise("user #{str} not in passwd file!") if Userdata[str] == nil;

      (Userdata[str])[4]
   end

   def Passwd.shell (str)
      raise("user #{str} not in passwd file!") if Userdata[str] == nil;

      (Userdata[str])[5]
   end

   def Passwd.user? (str)
      (Userdata[user] != nil)
   end

   def Passwd.login? (str)
      ! (Passwd.shell(str) === "/bin/false")
   end
end
class Login
   attr_accessor :name, :time, :tty, :login, :logout, :action;

   def initialize (str, time, tty, action)
      @time = 0;
      @login = 0;

      @name = str;
      @time = time;
      @tty = tty;
      @action = action;
      @login = time;
      @logout = 0;
   end

   def online?
      @logout == 0
   end

   def login_s
      Time.at(@login).strftime("%b %d %H:%M")
   end

   def logout_s
      if (self.logout == 0)
         "--"
      else
         Time.at(@logout).strftime("%b %d %H:%M")
      end
   end

   def online_summary
      printf("   %s %s %s\n",
         @name, login_s, @tty.tty_short);
   end

   def summary
      if (@logout == 0)
         printf("   %-15s %8s %6s   %s\n",
            login_s, "----", "----",
            @tty.tty_short);
      else
         printf("   %-15s %8s %6.2f%%  %s\n",
            login_s, @time.to_mins_rounded.format_time,
            @time * 100.0 / $sys.online,
            @tty.tty_short);
      end
   end
end

class Tty
   attr_accessor :online, :name

   def initialize (tty)
      @online = 0
      @name = tty
   end

   def online (time_secs)
      @online += time_secs
   end

   def print_summary
      printf("   %-8s %8s %8.2f%%\n", @name.tty_short, @online.to_mins.format_time,
         @online * 100 / $sys.online)
   end
end

class User
   attr_accessor :name, :actual_secs, :total_mins, :lastlogin, :charge;

   def initialize (str)
      @name = str
      @total_mins = 0
      @actual_secs = 0
      @lastlogin = 0
      @charge = 0.02;
   end

   def summary
      printf("   %-15s %8s %8.2f%%\n", @name, @total_mins.format_time,
         @actual_secs * 100.0 / $sys.online);
   end

   def charges
      printf("\n   %-15s %8s @ $%.2f/min. = $%.2f\n\n",
         "Total", @total_mins.format_time,
         @charge, @total_mins * @charge);
   end

   def reset_lastlogin
      @lastlogin = 0;
   end
   def charge (secs)
      @actual_secs += secs;
      @total_mins += secs.to_mins_rounded;
   end

   def user_info
      print "User: ", Passwd.info(@name), "\n";

      if Passwd.login? (@name)
         print "Login Shell: ", Passwd.shell (@name), "\n"
      end

      print "\n";
   end
end

class String
   def tty_short
      self =~ /.*(tty.*)/
      $1
   end
end

class Array
   def usersort
      sort { |a,b| a.name <=> b.name }
   end
end

#---------------------------------------------
# MAIN PROGRAM
#---------------------------------------------

$sys = System.new;

$charge = .02;
ttys = Hash.new
Passwd.new;

Report.header

users = Hash.new ; logins = Hash.new

PPPAuth.open { |auth|
   auth.readlines.each_line { |line|
      time, user, tty, action = line.fields
      time_secs = time.to_i

      raise("invalid online time!") if (time_secs <= 0);

      $sys.upsince(time_secs) if $sys.uptime == 0;

      users[user] = User.new(user) unless users.include?(user);
      ttys[tty] = Tty.new(tty) unless ttys.include?(tty);

      case action

      when "in"
         logins[time_secs] = Login.new(user, time_secs, tty, action);
         users[user].lastlogin = time_secs;   # - - - this line appears to be suspect....

      when "out"
         lastlogin_secs = users[user].lastlogin;       # temp_var for speed concessions

         raise("logout time without login time!") if (lastlogin_secs == 0);
         raise("login data lost!") if logins[lastlogin_secs] == nil;

         logins[lastlogin_secs].logout = time_secs;
         online_secs = time_secs - lastlogin_secs;

         users[user].reset_lastlogin;
         users[user].charge(online_secs);

         logins[lastlogin_secs].time = online_secs;
         ttys[tty].online(online_secs);
         $sys.online += online_secs;
      else
         raise("bad data: action = #{action}");
      end
      }
   }

#---------------------------------------------
# REPORTS
#---------------------------------------------

#----------------------------
# User Summary Report

print "User Summary:\n\n";

users.values.usersort.each { |userdata|
   userdata.summary
   }
#----------------------------
# System Summary Report

print "\nSystem Summary:\n\n";

$sys.summary

#----------------------------
# Online Summary Report

if (logins.values.find{ |login| login.online? } != nil)
   print "Currently Online:\n\n";

   logins.values.select{ |login| login.online? }.each { |login|
      login.online_summary
      }

   print "\n"
   end

#----------------------------
# TTY Usage Summary Report

print "TTY Summary:\n\n";

ttys.values.sort { |a,b| a.name <=> b.name }.each { |tty|
   tty.print_summary
   }

print "\n";

#----------------------------
# User Login Detail Report

users.values.usersort.each { |userdata|
   userdata.user_info

   logins.values.select { |login|
      userdata.name == login.name }.each { |login|
      login.summary
      }

   userdata.charges;
   }