last コマンドを作る

現在は Linux のほとんどは glibc 対応になりました。 ということで、 「 last コマンドを作る Linux glibc version 」 をどうぞ。

Ruby で last コマンドを作成しました。 これは、TurboLinux 2.0J にシステムを移行したら、libc 5 と glibc(libc 6) の混在環境のため /var/log/wtmp ないの整合性がとれなくなっているためです。 そのため、本来の last コマンドでは、情報がちゃんと表示されません。 そこで、last.rb をに作成して使うことにしました。

ただ、残念なことにこれも整合性があっていないためと思われるのですが、X 関係のコマンド kterm などでは、使用開始時の情報はあるのですが、使用修了時の情報が残らず、本来の出力が得られない部分があります。 xdm を普段から使っている方の場合、この情報がないため十分な結果が得られない状態です。 残念。 でも、存在しない情報なのでどうすることもできないのです。

■ 使い方

対応システムは、予定では? libc 5 と libc 6(glibc) と HP-UX 10.20 です。

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

$ last.rb | head
tetsu    ttyp1        :0.0             Sun Jul 19 01:10   still logged in
tetsu    ttyp0        :0.0             Sun Jul 19 01:10   still logged in
tetsu    tty1                          Sun Jul 19 01:09   still logged in
tetsu    ttyS0                         Sat Jul 18 22:59 - 23:10  (00:10)
tetsu    tty1                          Sat Jul 18 19:11 - 19:12  (00:00)
reboot   system boot                   Sat Jul 18 19:11
root     tty1                          Sat Jul 18 10:58 - down   (00:00)
tetsu    ttyp2        :0.0             Sat Jul 18 10:50 - down   (00:00)
tetsu    ttyp1        :0.0             Sat Jul 18 08:36 - down   (00:00)
tetsu    ttyp0        :0.0             Sat Jul 18 08:36 - down   (00:00)
表示形式は、Linux の last コマンドにあわせました。 X のターミナルウィンドウでは、うまく終了を確認できません。 終了情報がないためにいまのところ対応できていません。

■ ソースコード

スクリプトをセーブして、使用する場合には「テキスト」でセーブしてください。 これで使えるようになるはずです。

#! /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.3 1998/07/18 00:57:17 tetsu Exp $
# usage:

$last = []

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

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

def reset_entry(w_hash)
  for k, v in w_hash
    if v.status == 'start'
      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

LTWTMP_FMT = 'A44A32A*'

if File.exist? '/vmlinuz'
  WTMP = '/var/log/wtmp'
  if File.exist? '/lib/libc.so.6'
    LNAME_IN = 5
    LHOST_IN = 6
    LTIME_IN = 10
    LWTMP_FMT = 'ssiA32A4A32A256ssllliiiiA20'
    LWTMP_LEN = 384
  end
  TYPE_IN = 0
  PID_IN = 2
  LINE_IN = 3
  TIME_IN = 5
  NAME_IN = 6
  HOST_IN = 7
  WTMP_FMT = 'ssiA12A4LA8A16L'
  TWTMP_FMT = 'A24LA*'
  WTMP_LEN = 56
elsif File.exist? '/stand/vmunix'
  WTMP = '/var/adm/wtmp'

  TYPE_IN = 4
  PID_IN = 3
  LINE_IN = 2
  TIME_IN = 8
  NAME_IN = 0
  HOST_IN = 9
  WTMP_FMT = 'A8A4A12issssLA16l'
  WTMP_LEN = 60
else
  fail 'unknown system'
end

wtmp = File.open(WTMP)
w_hash = {}
w_start_time = 0

while wtmp_line = wtmp.read(WTMP_LEN)
  user = wtmp_line.unpack(LTWTMP_FMT).indexes(1)[0]

  if /^\w+$/ =~ user
    wtmp_line += wtmp.read(LWTMP_LEN - WTMP_LEN)
    arr = wtmp_line.unpack(LWTMP_FMT).indexes(TYPE_IN, LNAME_IN, LINE_IN, LHOST_IN, LTIME_IN)
  else
    arr = wtmp_line.unpack(WTMP_FMT).indexes(TYPE_IN, NAME_IN, LINE_IN, HOST_IN, TIME_IN)
  end

  key = arr[2]

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

  case arr.shift
  when 7
    w = Wtmp.new
    w.w_start(arr)
    w_hash[key] = w
  when 8, 6
    if w_hash.key?(key)
      w = w_hash[key]
      w.w_end(arr[3])
      w_hash.delete(key)
    end
  when 2
    reboot(w_hash, 'reboot', 'system boot', '', arr[3])
    w_hash = {}
  when 1
    reset_entry(w_hash)
    w_hash = {}
  end
end

wtmp.close

for k, v in w_hash
  if v.status == 'start'
    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, Time.at(w_start_time).strftime('%c'))

exit

■ 解説

「解説」よりは、言い訳だったりします。

ちょっとグチャグチャしています。 まだ練り込みが足りないですね。 基本的には、一つのセッションを「オブジェクト」として扱っています。 セッションは「開始」「終了」で一つの情報になります。 オブジェクトは、この情報を表示するためのものです。 表示可能な条件ができたら配列に格納しています。

今回の方法でよかった点は、「オブジェクト」の対象を一つのセッションで扱うので、「開始」「終了」が単純に扱えること。 例外的にシステムがリブートするなどが起きた場合には、現在のステータスで「開始」の情報だけが入っているものを拾いだして(結局は残り全部だけれど)まとめ処理すればいいこと。 など、処理を単純化できました。 これがオブジェクト単位に考えない場合? データ構造をハッシュ(連想配列)にして、キーの処理を... などとよく分からない状態に陥りやすくなります。

よりよくするために、もうちょっと最後まで(つまり表示の部分まで)オブジェクトとしてセッションを扱うと、よかったかなとおもっています。 ちょっと単目的にしたので、作りやすかったということはあるのですが、扱うオブジェクトをちゃんと最後まで...

wtmp のフォーマットについてはオンラインマニュアル wtmp(5) を参照ください。 libc やシステムによりフォーマットが違うので注意してください。 また内容確認には hd.rbRuby で hex dump 」 が活躍します。

■ その他

wtmp には、セッションの記録が残ります。 私は、わりとこの情報を重要視しているので、C でも Perl でも似たようなものを作ってきました。 最初は、wtmp の情報から、last コマンドと同じ出力を得るために悩みました。 いまとなっては、なれもあって作成するのに短時間で済みましたが、今回は Ruby のおかげでより単純に考えることができて、とても楽でした。 Ruby のような手軽に扱える OOPL(OOSL?) があるのは、とても助かります。


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