#! /usr/bin/perl -w
## ----------------------------------------------------------------------------
#  tltail.pl
# -----------------------------------------------------------------------------
# Mastering programmed by YAMASHINA Hio
#
# Copyright 2007 YAMASHINA Hio
# -----------------------------------------------------------------------------
# $Id: tltail.pl,v 1.1 2007/05/09 07:08:29 hio Exp $
# -----------------------------------------------------------------------------
package Tripletail::Util::Tail;
use strict;
use warnings;
use Getopt::Long ();
use IO::Poll     qw(POLLIN);
use IO::Handle   (); # $io->blocking().

our $VERSION = '0.01';
$| = 1;

my $SEEK_SET = 0;
my $SEEK_END = 2;

our $ARG_SPECS = [
	"help|h"    => "print this message.",
	"version|V" => "print version info.",
	"follow|f"  => "follow.",
	"verbose|v" => "verbose.",
	"quiet|q"   => "quiet.",
];

# -----------------------------------------------------------------------------
# boot.
#
caller or __PACKAGE__->do_work(@ARGV);

# -----------------------------------------------------------------------------
# my $opts = $pkg->_arg_to_hash(@ARGV);
#
sub _arg_to_hash
{
	my $pkg = shift;
	local(@ARGV) = @_;
	my $opts = {};
	my $p = Getopt::Long::Parser->new();
	$p->configure("bundling");
	$p->getoptions($opts, keys %{{@$ARG_SPECS}});
	$opts->{_extra} = [@ARGV];
	$opts;
}

# -----------------------------------------------------------------------------
# $pkg->_print_usage().
#
sub _print_usage
{
	my $pkg = shift;
	print "usage: tltail [OPTIONS] FILES ...\n";
	print "OPTIONS:\n";
	foreach my $i (grep{$_%2==0} 0..$#$ARG_SPECS)
	{
		my ($k,$v) = @$ARG_SPECS[$i,$i+1];
		$k = join(', ', sort{length($a)<=>length($b)} map{(length($_)==1?'-':'--').$_}split(/\|/, $k));
		my $padding = " "x(14-length($k));
		print "  $k$padding $v\n";
	}
}

# -----------------------------------------------------------------------------
# $pkg->do_work(@ARGV).
#
sub do_work
{
	my $pkg = shift;
	my $opts = $pkg->_arg_to_hash(@_);
	$opts->{help}    and return _print_usage();
	if( $opts->{version} )
	{
		print "tltail version $VERSION\n";
		return;
	}
	
	my $start_at = time;
	my $streams = $pkg->_setup({now=>$start_at, files=>$opts->{_extra}});
	my $data = {
		streams   => $streams,
		opts      => $opts,
		prev_file => '',
	};
	
	for(;;)
	{
		if( my $poll = $streams->{poll} )
		{
			my $ev = $poll->poll(0);
			$ev==-1 and die "poll: $!";
			foreach my $handle ($poll->handles(POLLIN))
			{
				my $item = $streams->{handles}{$handle};
				READ:{
					my $len = sysread($handle, $item->{buffer}, 1024, length $item->{buffer});
					if( $len )
					{
						_show($data, $item);
						redo READ;
					}
					if( defined($len) )
					{
						# eof.
						print "$item->{name}: eof\n";
						$poll->remove($handle);
						delete $streams->{handles}{$handle};
						last READ;
					}
					$!{EAGAIN} and last READ;
					die "$item->{name}: $!";
				}
			}
		}
		
		my $now = time;
		my $dirs  = $streams->{dirs};
		my $files = $streams->{files};
		my ($fdir, $fname, $open_at, $close_at);
		foreach my $item (@$dirs)
		{
			$item->{type} eq 'dir' or next;
			$item->{open_at} <= $now or next;
			$fdir or ($fdir, $fname) = _time2name($now);
			my $file = "$item->{dir}/$fdir/$fname";
			-e $file or next;
			
			$open_at  ||= $now - $now%3600 + 3600;
			$close_at ||= $now - $now%3600 + 3600 + 3;
			print "open: $file\n";
			open(my $fh, '<', $file) or die "$file: $!";
			my $pos = sysseek($fh, 0, $SEEK_END) || 0;
			push(@$files, +{
				name   => $file,
				type   => 'file',
				handle => $fh,
				pos    => $pos,
				close_at => $close_at,
				buffer => '',
			});
			$item->{open_at} = $open_at;
		}
		
		foreach my $item (@$files)
		{
			$item->{type} eq 'file' or next;
			if( $item->{close_at} && $item->{close_at} < $now )
			{
				print "close: $item->{name}\n";
				close $item->{handle};
				$item = undef;
				next;
			}
			my $pos = sysseek($item->{handle}, 0, $SEEK_END);
			defined($pos) or die "$item->{name}: $!";
			$pos==$item->{pos} and next;
			sysseek($item->{handle}, $item->{pos}, $SEEK_SET);
			
			my $len = sysread($item->{handle}, $item->{buffer}, $pos-$item->{pos}, length $item->{buffer});
			$item->{pos} = $pos;
			if( $len )
			{
				_show($data, $item);
			}elsif( defined($len) )
			{
				# eof.
				die "$item->{name}: eof";
			}else
			{
				$!{EAGAIN} and last READ;
				die "$item->{name}: $!";
			}
		}
		@$files = grep{$_} @$files;
		select(undef,undef,undef,0.1);
	}
}

sub _show
{
	my $data = shift;
	my $item = shift;
	
	my $opts = $data->{opts};
	my $show_file = !$opts->{quiet} && ($opts->{verbose} || @{$opts->{_extra}}>1);
	if( $show_file && $item->{name} ne $data->{prev_file} )
	{
		print "==> $item->{name} <==\n";
		$data->{prev_file} = $item->{name};
	}
	if( $item->{buffer} =~ s/^(.*\n)//s )
	{
		print $1;
	}
}

sub _time2name
{
	my $time = shift || time;
	my @st = localtime($time);
	my $fdir  = sprintf('%04d%02d', $st[5]+1900, $st[4]+1); # yyyymm
	my $fname = sprintf('%02d-%02d.log', $st[3], $st[2]);   # dd-hh.log
	($fdir, $fname);
}

sub _setup
{
	my $pkg  = shift;
	my $opts = shift;
	
	my $files = $opts->{files} || [];
	@$files or $files = ['-'];
	my $now = $opts->{now} || time;
	my ($fdir, $fname) = _time2name($now);
	
	my $close_at = $now - $now%3600 + 3600 + 3;
	my $poll;
	my %handles;
	my @files;
	my @dirs;
	foreach my $file (@$files)
	{
		if( $file eq '-' )
		{
			$poll ||= IO::Poll->new();
			my $fh = \*STDIN;
			$fh->blocking(0);
			$handles{$fh} = {
				name   => '-',
				type   => 'handle',
				handle => $fh,
			};
			$poll->mask($fh, POLLIN);
			next;
		}
		if( -d $file )
		{
			push(@dirs, +{
				name    => "$file/",
				type    => 'dir',
				open_at => $now,
				dir     => $file,
			});
			$dirs[-1]->{dir} =~ s{/$}{};
			next;
		}
		open(my $fh, '<', $file) or die "$file: $!";
		my $pos = sysseek($fh, 0, $SEEK_END) || 0;
		push(@files, +{
			name   => $file,
			type   => 'file',
			handle => $fh,
			pos    => $pos,
			close_at => $close_at,
		});
	}
	foreach my $item (values %handles, @files)
	{
		$item->{buffer} = '';
	}
	+{
		poll    => $poll,
		handles => \%handles,
		files   => \@files,
		dirs    => \@dirs,
	};
}

# -----------------------------------------------------------------------------
# End of File.
# -----------------------------------------------------------------------------
__END__

=encoding utf-8

=head1 NAME

tltail.pl - tailing separated log files.

=head1 NAME (ja)

tltail.pl - 分割されたログファイルのtail.

=head1 SYNOPSIS

  $ tltail.pl /path/to/logdir
  
=head1 DESCRIPTION

Tripletail で出力される, yyyymm/dd-hh.log 形式の
ログファイルを続けて読み込んでコンソールに出力します. 

=head1 SEE ALSO

=over 4

=item L<Tripletail>

=back

=head1 AUTHOR INFORMATION

=over 4

Copyright 2007 YMIRLINK Inc. All Rights Reserved.

This framework is free software; you can redistribute it and/or modify it under the same terms as Perl itself

このフレームワークはフリーソフトウェアです。あなたは Perl と同じライセンスの 元で再配布及び変更を行うことが出来ます。

Address bug reports and comments to: tl@tripletail.jp

HP : http://tripletail.jp/

=back

=cut
