last コマンドを作る Linux glibc version

Ruby で last コマンドを作りました。 以前、TurboLinux 2.0J で last コマンドに不具合があったのがきっかけですが、使いなれてしまったのでいまでも使い続けています。 今回のものは、最近 Linux では glibc が標準的になってきたので glibc-2.0.x 用の last コマンドです。 基本的な部分をうまく活用すれば、他のシステムへの移植も可能と思います。

普段(2007/4/28 現在)からこのコマンドを使っています。

■ 使い方

使い方といっても特にオプションなどはなく、次のように実行してください。

$ last.rb
tetsu    ttyp2        :0.0             Mon May 10 20:40   still logged in
tetsu    ttyp1        :0.0             Mon May 10 20:40   still logged in
tetsu    ttyp0        :0.0             Mon May 10 20:35   still logged in
tetsu    tty1                          Mon May 10 20:35   still logged in
reboot   system boot                   Mon May 10 20:14
root     tty1                          Mon May 10 08:31 - down   (00:00)
tetsu    ttyp2        :0.0             Fri May 07 19:32 - 08:29  (2+12:56)
tetsu    ttyp1        :0.0             Fri May 07 19:32 - 08:29  (2+12:56)
tetsu    ttyp0        :0.0             Fri May 07 19:31 - 08:31  (2+12:59)
tetsu    tty1                          Fri May 07 19:31 - 08:31  (2+12:59)
reboot   system boot                   Fri May 07 19:31
root     tty1                          Fri May 07 06:11 - down   (00:00)
tetsu    ttyp2        :0.0             Thu May 06 19:34 - 06:10  (10:36)
tetsu    ttyp1        :0.0             Thu May 06 19:34 - 06:10  (10:36)
tetsu    ttyp0        :0.0             Thu May 06 19:34 - 06:11  (10:36)
tetsu    tty1                          Thu May 06 19:33 - 06:11  (10:37)
reboot   system boot                   Thu May 06 19:33
root     tty1                          Thu May 06 07:47 - down   (00:00)
tetsu    ttyp3        :0.0             Wed May 05 22:36 - 22:46  (00:09)

/var/log/wtmp begins Wed May  5 22:36:21 1999

■ ソースコード

Web ブラウザでの表示上は Ruby のスクリプトですが、HTML の特殊文字をエスケープしています。 テキストとしてセーブしてご利用ください。

#! /usr/local/bin/ruby
# /home/tetsu/src/ruby/toolbox/last.rb
# Created: July 13,1998 Monday 00:26:49
# Author: tetsu(WATANABE Tetsuya)
# $Id: last.rb,v 1.9 2003/05/08 03:00:53 tetsu Exp $
# usage:

class Wtmp
  def initialize
    @status = ''
    @wtmp = []
  end
  attr :status

  def w_start(wtmp)
    @wtmp = wtmp
    @status = 'start'
  end
  
  # 0: still logged in
  # nil: down
  # other: normal
  def w_end(time = nil)
    retval = []
    if @status == 'start'
      @status = 'end'
      @wtmp.push(time)
      retval = @wtmp
      @wtmp = []
    end
    retval
  end
end

def reset_entry(w_hash)
  for k, v in w_hash
    if v.status == 'start'
      $last.push(v.w_end)
    end
  end
end

def reboot(w_hash, *wtmp)
  $last.push(wtmp)
  reset_entry(w_hash)
end

def delta_time(t)
  d = 0
  if t > 24 * 3600
    d = t / (24 * 3600)
    t %= 24 * 3600
  end
  h = t / 3600
  m = t % 3600 / 60
  if d == 0
    return format("%02d:%02d", h, m)
  else
    return format("%d+%02d:%02d", d, h, m)
  end
end

WTMP = '/var/log/wtmp'
TYPE_IN = 0
PID_IN = 2
LINE_IN = 3
NAME_IN = 5
HOST_IN = 6
TIME_IN = 10
WTMP_FMT = 'ssiA32A4A32A256ssllliiiiA20'
WTMP_LEN = 384

$last = []

w_hash = {}
w_start_time = 0

wtmp_file = 
  if ARGV.size > 0
    ARGV[0]
  else
    WTMP
  end
  
wtmp = File.open(wtmp_file)

while wtmp_line = wtmp.read(WTMP_LEN)
  arr = wtmp_line.unpack(WTMP_FMT).values_at(TYPE_IN, NAME_IN, LINE_IN, HOST_IN, TIME_IN)
  key = arr[2]

  if w_start_time == 0
    w_start_time = arr[4]
  end

  case arr.shift
  when 7                        # USER_PROCESS
    w = Wtmp.new
    w.w_start(arr)
    w_hash[key] = w
  when 8, 6                     # DEAD_PROCESS LOGIN_PROCESS
    if w_hash.key?(key)
      w = w_hash[key]
      $last.push(w.w_end(arr[3]))
      w_hash.delete(key)
    end
  when 2                        # BOOT_TIME
    reboot(w_hash, 'reboot', 'system boot', '', arr[3])
    w_hash = {}
  when 1                        # RUN_LVL
    reset_entry(w_hash)
    w_hash = {}
  end
end

wtmp.close

for k, v in w_hash
  if v.status == 'start'
    $last.push(v.w_end(0))      # 0 is "still logged in"
  end
end

$last.sort {|a, b|
  (b[3] <=> a[3]).nonzero? or (a[1] <=> b[1]).nonzero? or a[0] <=> b[0]
}.each {|v|
  printf("%-8s %-12s %-16s %s",
         v[0],
         v[1],
         v[2],
         Time.at(v[3]).strftime('%a %b %d %H:%M'))

  if v[0] == 'reboot'
    print "\n"
  elsif v[4] == 0
    print "   still logged in\n"
  elsif v[4] == nil
    print " - down   (00:00)\n"
  else
    printf(" - %s  (%s)\n",
           Time.at(v[4]).strftime('%H:%M'),
           delta_time(v[4] - v[3])
           )
  end
}

printf("\n%s begins %s\n", wtmp_file, Time.at(w_start_time).strftime('%c'))

exit

■ 解説

last コマンドは、データファイルになる /var/log/wtmp の形式に依存しています。 この形式の定義は /usr/include/utmp.h にあります。 この定義をもとにデータをとりだすための unpack の定義を決めます。 これを手作業で実施すると大変なので、pstruct というコマンドを利用します。 これは、Perl システムが提供するコマンド(Perl スクリプト)ですが、wtmp ファイルの「utmp 構造体」の形式を確認しやすくなります。 Ruby スクリプト中で使用している、構造体の大きさ(WTMP_LEN)や unpack のフォーマット(WTMP_FMT)などを、この情報をもとに決めています。 この pstruct というツールは、便利ですね。

ファイルフォーマットの情報がわかり、データを扱えるようになったらログインしている状況を last コマンドと同じように扱えるようにします。 この処理は少々ややこしいです。 データの蓄積は、ログイン、ログアウトのタイミングでそれぞれ独立に、wtmp ファイルに追加されていきます。 このため、ログインの状況を示すのには、「ログイン」と「ログアウト」の関連を見つけなければなりません。 これを「ターミナルデバイス」名から判断して、処理を行っています。 途中リブートなどが入ったら、そのための整合性を合わせるなどの処理も行います。

同じような内容を扱っている 「 last コマンドを作る 」 も参考にしてください。

■ 履歴

1.9 2003/5/8

Array#values_at へ修正。


渡辺哲也(WATANABE Tetsuya): Tetsuya.WATANABE atmark nifty.com