#!/usr/bin/perl -w # # (c) Igor Chubin, igor@chub.in, 2004-2006 # ## Эта строчка добавлена из блокнота Windows ## Надо отдать должное, он каким-то образом научился понимать кодировку use strict; use POSIX; use Term::VT102; use Text::Iconv; use Time::Local 'timelocal_nocheck'; use IO::Socket; use lib "/usr/local/bin"; use l3config; our @Command_Lines; our @Command_Lines_Index; our %Diffs; our %Sessions; our %Script_Files; # Информация о позициях в скрипт-файлах, # до которых уже выполнен разбор # и информация о времени модификации файла # $Script_Files{$file}->{size} # $Script_Files{$file}->{tell} our $Killed =0; # В режиме демона -- процесс получил сигнал о завершении sub init_variables; sub main; sub load_diff_files; sub bind_diff; sub extract_commands_from_cline; sub load_command_lines; sub sort_command_lines; sub print_command_lines; sub printq; sub save_cache_stat; sub load_cache_stat; sub print_session; sub load_diff_files { my @pathes = @_; for my $path (@pathes) { my $template = "*.diff"; my @files = <$path/$template>; my $i=0; for my $file (@files) { next if defined($Diffs{$file}); my %diff; # Старый формат имени diff-файла # DEPRECATED if ($file=~m@/(D?[0-9][0-9]?[0-9]?)[^/]*?([0-9]*):([0-9]*):?([0-9]*)@) { $diff{"day"}=$1 || ""; $diff{"hour"}=$2; $diff{"min"}=$3; $diff{"sec"}=$4 || 0; $diff{"uid"} = 0 if $path =~ m@/root/@; print "diff loaded: $diff{day} $diff{hour}:$diff{min}:$diff{sec}\n"; } # Новый формат имени diff-файла elsif ($file =~ m@.*/([^_]*)_([0-9]+)(.*)@) { $diff{"local_session_id"} = $1; $diff{"time"} = $2; $diff{"filename"} = $3; $diff{"filename"} =~ s@_@/@g; $diff{"filename"} =~ s@//@_@g; print "diff loaded: $diff{filename} (time=$diff{time},session=$diff{local_session_id})\n"; } else { next; } # Чтение и изменение кодировки содержимого diff-файла local $/; open (F, "$file") or return "Can't open file $file ($_[0]) for reading"; my $text = <F>; if ($Config{"encoding"} && $Config{"encoding"} !~ /^utf-8$/i) { my $converter = Text::Iconv->new($Config{"encoding"}, "utf-8"); $text = $converter->convert($text); } close(F); $diff{"text"}=$text; $diff{"path"}=$path; $diff{"bind_to"}=""; $diff{"time_range"}=-1; $diff{"index"}=$i; $Diffs{$file} = \%diff; $i++; } } } sub bind_diff { print "Trying to bind diff...\n"; my $cl = shift; my $hour = $cl->{"hour"}; my $min = $cl->{"min"}; my $sec = $cl->{"sec"}; my $min_dt = 10000; for my $diff_key (keys %Diffs) { my $diff = $Diffs{$diff_key}; next if ($diff->{"local_session_id"} && $cl->{"local_session_id"} && ($cl->{"local_session_id"} ne $diff->{"local_session_id"})); print "diff of my session found\n"; next if ($diff->{"day"} && $cl->{"day"} && ($cl->{"day"} ne $diff->{"day"})); my $dt; if ($diff->{"time"} && $cl->{"time"}) { $dt = $diff->{"time"} - $cl->{"time"} } else { $dt=($diff->{"hour"}-$hour)*3600 +($diff->{"min"}-$min)*60 + ($diff->{"sec"}-$sec); } if ($dt >0 && $dt < $min_dt && ($diff->{"time_range"} <0 || $dt < $diff->{"time_range"})) { print "Approppriate diff found: dt=$dt\n"; if ($diff->{"bind_to"}) { undef $diff->{"bind_to"}->{"diff"}; }; $diff->{"time_range"}=$dt; $diff->{"bind_to"}=$cl; $cl->{"diff"} = $diff_key; $min_dt = $dt; } } } sub extract_commands_from_cline # Разобрать командную строку $_[1] и возвратить хэш, содержащий # номер первого появление команды в строке: # команда => первая позиция { my $cline = $_[0]; my @lists = split /\;/, $cline; my @commands = (); for my $list (@lists) { push @commands, split /\|/, $list; } my %commands; my %files; my $i=0; for my $command (@commands) { $command =~ /\s*(\S+)\s*(.*)/; if ($1 && $1 eq "sudo" ) { $commands{"$1"}=$i++; $command =~ s/\s*sudo\s+//; } $command =~ /\s*(\S+)\s*(.*)/; if ($1 && !defined $commands{"$1"}) { $commands{"$1"}=$i++; }; } return %commands; } sub load_command_lines { my $lab_scripts_path = $_[0]; my $lab_scripts_mask = $_[1]; my $cline_re_base = qq' ( (?:\\^?([0-9]*C?)) # exitcode (?:_([0-9]+)_)? # uid (?:_([0-9]+)_) # pid (...?) # day (.?.?) # lab \\s # space separator ([0-9][0-9]):([0-9][0-9]):([0-9][0-9]) # time .\\[50D.\\[K # killing symbols (.*?([\$\#]\\s?)) # prompt (.*) # command line ) '; my $cline_re = qr/$cline_re_base/sx; my $cline_re2 = qr/$cline_re_base$/sx; my $cline_re_v2_base = qq' ( v2[\#] # version ([0-9]+)[\#] # history line number ([0-9]+)[\#] # exitcode ([0-9]+)[\#] # uid ([0-9]+)[\#] # pid ([0-9]+)[\#] # time (.*?)[\#] # pwd .\\[1024D.\\[K # killing symbols (.*?([\$\#]\\s?)) # prompt (.*) # command line ) '; my $cline_re_v2 = qr/$cline_re_v2_base/sx; my $cline_re2_v2 = qr/$cline_re_v2_base$/sx; my $vt = Term::VT102->new ( 'cols' => $Config{"terminal_width"}, 'rows' => $Config{"terminal_height"}); my $cline_vt = Term::VT102->new ('cols' => $Config{"terminal_width"}, 'rows' => $Config{"terminal_height"}); my $converter = Text::Iconv->new($Config{"encoding"}, "utf-8") if ($Config{"encoding"} && $Config{"encoding"} !~ /^utf-8$/i); print "Parsing lab scripts...\n" if $Config{"verbose"} =~ /y/; my $file; my $skip_info; my $commandlines_loaded =0; my $commandlines_processed =0; my @lab_scripts = <$lab_scripts_path/$lab_scripts_mask>; for $file (@lab_scripts){ # Пропускаем файл, если он не изменялся со времени нашего предудущего прохода my $size = (stat($file))[7]; next if ($Script_Files{$file} && $Script_Files{$file}->{size} && $Script_Files{$file}->{size} >= $size); my $local_session_id; # Начальное значение идентификатора текущего сеанса определяем из имени скрипта # Впоследствии оно может быть уточнено $file =~ m@.*/([^/]*)\.script$@; $local_session_id = $1; #Если файл только что появился, #пытаемся найти и загрузить информацию о соответствующей ему сессии if (!$Script_Files{$file}) { my $session_file = $file; $session_file =~ s/\.script/.info/; if (open(SESSION, $session_file)) { local $/; my $data = <SESSION>; close(SESSION); for my $session_data ($data =~ m@<session>(.*?)</session>@sg) { my %session; while ($session_data =~ m@<([^>]*?)>(.*?)</\1>@sg) { $session{$1} = $2; } $local_session_id = $session{"local_session_id"} if $session{"local_session_id"}; $Sessions{$local_session_id}=\%session; } #Загруженную информацию сразу же отправляем в поток print_session($Config{cache}, $local_session_id); } } open (FILE, "$file"); binmode FILE; # Переходим к тому месту, где мы окончили разбор seek (FILE, $Script_Files{$file}->{tell}, 0) if $Script_Files{$file}->{tell}; $Script_Files{$file}->{size} = $size; $Script_Files{$file}->{tell} = 0 unless $Script_Files{$file}->{tell}; $file =~ m@.*/(.*?)-.*@; print "\n+- processing file $file\n" if $Config{"verbose"} =~/y/; my $tty = $1; my $first_pass = 1; my %cl; my $last_output_length=0; while (<FILE>) { $commandlines_processed++; next if s/^Script started on.*?\n//s; if (/[0-9][0-9]:[0-9][0-9]:[0-9][0-9].\[[0-9][0-9]D.\[K/ && m/$cline_re/) { s/.*\x0d(?!\x0a)//; m/$cline_re2/gs; $commandlines_loaded++; $last_output_length=0; # Previous command my %last_cl = %cl; my $err = $2 || ""; $cl{"local_session_id"} = $local_session_id; # Parse new command $cl{"uid"} = $3; #$cl{"euid"} = $cl{"uid"}; # Если в команде обнаружится sudo, euid поменяем на 0 $cl{"pid"} = $4; $cl{"day"} = $5; $cl{"lab"} = $6; $cl{"hour"} = $7; $cl{"min"} = $8; $cl{"sec"} = $9; #$cl{"fullprompt"} = $10; $cl{"prompt"} = $11; $cl{"raw_cline"} = $12; { use bytes; $cl{"raw_start"} = tell (FILE) - length($1); $cl{"raw_output_start"} = tell FILE; } $cl{"raw_file"} = $file; $cl{"err"} = 0; $cl{"output"} = ""; $cl{"tty"} = $tty; $cline_vt->process($cl{"raw_cline"}."\n"); $cl{"cline"} = $cline_vt->row_plaintext (1); $cl{"cline"} =~ s/\s*$//; $cline_vt->reset(); my %commands = extract_commands_from_cline($cl{"cline"}); #$cl{"euid"}=0 if defined $commands{"sudo"}; my @comms = sort { $commands{$a} cmp $commands{$b} } keys %commands; $cl{"last_command"} = $comms[$#comms] || ""; if ( $Config{"suppress_editors"} =~ /^y/i && grep ($_ eq $cl{"last_command"}, @{$Config{"editors"}}) || $Config{"suppress_pagers"} =~ /^y/i && grep ($_ eq $cl{"last_command"}, @{$Config{"pagers"}}) || $Config{"suppress_terminal"}=~ /^y/i && grep ($_ eq $cl{"last_command"}, @{$Config{"terminal"}}) ) { $cl{"suppress_output"} = "1"; } else { $cl{"suppress_output"} = "0"; } $skip_info = 0; print " ",$cl{"last_command"}; # Processing previous command line if ($first_pass) { $first_pass = 0; next; } # Error code $last_cl{"raw_end"} = $cl{"raw_start"}; $last_cl{"err"}=$err; $last_cl{"err"}=130 if $err eq "^C"; if (grep ($_ eq $last_cl{"last_command"}, @{$Config{"editors"}})) { bind_diff(\%last_cl); } # Output if (!$last_cl{"suppress_output"} || $last_cl{"err"}) { for (my $i=0; $i<$Config{"terminal_height"}; $i++) { my $line= $vt->row_plaintext($i); next if !defined ($line) ; #|| $line =~ /^\s*$/; $line =~ s/\s*$//; $line .= "\n" unless $line =~ /^\s*$/; $last_cl{"output"} .= $line; } } else { $last_cl{"output"}= ""; } $vt->reset(); # Save if (!$Config{"lab"} || $cl{"lab"} eq $Config{"lab"}) { # Changing encoding for (keys %last_cl) { next if /raw/; $last_cl{$_} = $converter->convert($last_cl{$_}) if ($Config{"encoding"} && $Config{"encoding"} !~ /^utf-8$/i); } push @Command_Lines, \%last_cl; # Сохранение позиции в файле, до которой выполнен # успешный разбор $Script_Files{$file}->{tell} = $last_cl{raw_end}; } next; } elsif (m/$cline_re_v2/) { # Разбираем командную строку версии 2 s/.*\x0d(?!\x0a)//; m/$cline_re2_v2/gs; $commandlines_loaded++; $last_output_length=0; # Previous command my %last_cl = %cl; $cl{"local_session_id"} = $local_session_id; # Parse new command $cl{"history"} = $2; my $err = $3; $cl{"uid"} = $4; #$cl{"euid"} = $cl{"uid"}; # Если в команде обнаружится sudo, euid поменяем на 0 $cl{"pid"} = $5; $cl{"time"} = $6; $cl{"pwd"} = $7; #$cl{"fullprompt"} = $8; $cl{"prompt"} = $9; $cl{"raw_cline"}= $10; { use bytes; $cl{"raw_start"} = tell (FILE) - length($1); $cl{"raw_output_start"} = tell FILE; } $cl{"raw_file"} = $file; $cl{"err"} = 0; $cl{"output"} = ""; #$cl{"tty"} = $tty; $cline_vt->process($cl{"raw_cline"}."\n"); $cl{"cline"} = $cline_vt->row_plaintext (1); $cl{"cline"} =~ s/\s*$//; $cline_vt->reset(); my %commands = extract_commands_from_cline($cl{"cline"}); #$cl{"euid"} = 0 if defined $commands{"sudo"}; my @comms = sort { $commands{$a} cmp $commands{$b} } keys %commands; $cl{"last_command"} = $comms[$#comms] || ""; if ( $Config{"suppress_editors"} =~ /^y/i && grep ($_ eq $cl{"last_command"}, @{$Config{"editors"}}) || $Config{"suppress_pagers"} =~ /^y/i && grep ($_ eq $cl{"last_command"}, @{$Config{"pagers"}}) || $Config{"suppress_terminal"}=~ /^y/i && grep ($_ eq $cl{"last_command"}, @{$Config{"terminal"}}) ) { $cl{"suppress_output"} = "1"; } else { $cl{"suppress_output"} = "0"; } $skip_info = 0; if ($Config{verbose} =~ /y/i) { print "| " if $commandlines_loaded % 15 == 1; print " ",$cl{"last_command"}; } # Processing previous command line if ($first_pass) { $first_pass = 0; next; } # Error code $last_cl{"err"}=$err; $last_cl{"raw_end"} = $cl{"raw_start"}; if (grep ($_ eq $last_cl{"last_command"}, @{$Config{"editors"}})) { bind_diff(\%last_cl); } # Output if (!$last_cl{"suppress_output"} || $last_cl{"err"}) { for (my $i=0; $i<$Config{"terminal_height"}; $i++) { my $line= $vt->row_plaintext($i); next if !defined ($line) ; #|| $line =~ /^\s*$/; $line =~ s/\s*$//; $line .= "\n" unless $line =~ /^\s*$/; $last_cl{"output"} .= $line; } } else { $last_cl{"output"}= ""; } $vt->reset(); # Changing encoding for (keys %last_cl) { next if /raw/; if ($Config{"encoding"} && $Config{"encoding"} !~ /^utf-8$/i) { $last_cl{$_} = $converter->convert($last_cl{$_}) } } push @Command_Lines, \%last_cl; # Сохранение позиции в файле, до которой выполнен # успешный разбор $Script_Files{$file}->{tell} = $last_cl{raw_end}; next; } # Иначе, это строка вывода $last_output_length+=length($_); #if (!$cl{"suppress_output"} || $last_output_length < 5000) { if ($last_output_length < 50000) { $vt->process("$_"."\n") } else { if (!$skip_info) { print "($cl{last_command})"; $skip_info = 1; } } } close(FILE); } if ($Config{"verbose"} =~ /y/) { print "\n`- finished.\n" ; print "Lines loaded: $commandlines_processed\n"; print "Command lines: $commandlines_loaded\n"; } } sub sort_command_lines { print "Sorting command lines..." if $Config{"verbose"} =~ /y/; # Sort Command_Lines # Write Command_Lines to Command_Lines_Index my @index; for (my $i=0;$i<=$#Command_Lines;$i++) { $index[$i]=$i; } @Command_Lines_Index = sort { $Command_Lines[$index[$a]]->{"time"} <=> $Command_Lines[$index[$b]]->{"time"} || $Command_Lines[$index[$a]]->{"day"} cmp $Command_Lines[$index[$b]]->{"day"} || $Command_Lines[$index[$a]]->{"hour"} <=> $Command_Lines[$index[$b]]->{"hour"} || $Command_Lines[$index[$a]]->{"min"} <=> $Command_Lines[$index[$b]]->{"min"} || $Command_Lines[$index[$a]]->{"sec"} <=> $Command_Lines[$index[$b]]->{"sec"} } @index; print "finished\n" if $Config{"verbose"} =~ /y/; } sub printq { my $TO = shift; my $text = join "", @_; $text =~ s/&/&/g; $text =~ s/</</g; $text =~ s/>/>/g; print $TO $text; } =cut Вывести результат обработки журнала. =cut sub print_command_lines { my $output_filename=$_[0]; my $mode = ">"; $mode =">>" if $Config{mode} eq "daemon"; open(OUT, $mode, $output_filename) or die "Can't open $output_filename for writing\n"; my $cl; my $in_range=0; for my $i (@Command_Lines_Index) { $cl = $Command_Lines[$i]; if ($Config{"from"} && $cl->{"cline"} =~ /$Config{"signature"}\s*$Config{"from"}/) { $in_range=1; next; } if ($Config{"to"} && $cl->{"cline"} =~ /$Config{"signature"}\s*$Config{"to"}/) { $in_range=0; next; } next if ($Config{"from"} && $Config{"to"} && !$in_range) || ($Config{"skip_empty"} =~ /^y/i && $cl->{"cline"} =~ /^\s*$/ ) || ($Config{"skip_wrong"} =~ /^y/i && $cl->{"err"} != 0) || ($Config{"skip_interrupted"} =~ /^y/i && $cl->{"err"} == 130); # Вырезаем из вывода только нужное количество строк my $output=""; if (!grep ($_ eq $cl->{"last_command"}, @{$Config{"full_output_commands"}}) && ($Config{"head_lines"} || $Config{"tail_lines"})) { # Partialy output my @lines = split '\n', $cl->{"output"}; # head my $mark=1; for (my $i=0; $i<= $#lines && $i < $Config{"cache_head_lines"}; $i++) { $output .= $lines[$i]."\n"; } # tail my $start=$#lines-$Config{"cache_tail_lines"}+1; if ($start < 0) { $start=0; $mark=0; } if ($start < $Config{"cache_head_lines"}) { $start=$Config{"cache_head_lines"}; $mark=0; } $output .= $Config{"skip_text"}."\n" if $mark; for ($i=$start; $i<= $#lines; $i++) { $output .= $lines[$i]."\n"; } } else { # Full output $output .= $cl->{"output"}; } # Совместимость с labmaker # Переводим в секунды Эпохи # В labmaker'е данные хранились в неудобной форме: hour, min, sec, day of year # Информация о годе отсутствовала # Её можно внести: # Декабрь 2004 год; остальные -- 2005 год. my $year = 2005; #$year = 2004 if ( $cl->{day} > 330 ); $year = $Config{year} if $Config{year}; # timelocal( $sec, $min, $hour, $mday,$mon,$year); $cl->{time} ||= timelocal_nocheck($cl->{sec},$cl->{min},$cl->{hour},$cl->{day},0,$year); # Начинаем вывод команды print OUT "<command>\n"; for my $element (qw( local_session_id history uid pid time pwd raw_start raw_output_start raw_end raw_file tty err last_command history )) { next unless defined($cl->{"$element"}); print OUT "<$element>".$cl->{$element}."</$element>\n"; } for my $element (qw( prompt cline )) { next unless defined($cl->{"$element"}); print OUT "<$element>"; printq(\*OUT,$cl->{"$element"}); print OUT "</$element>\n"; } #note #note_title print OUT "<output>"; printq(\*OUT,$output); print OUT "</output>\n"; if ($cl->{"diff"}) { print OUT "<diff>"; printq(\*OUT,${$Diffs{$cl->{"diff"}}}{"text"}); print OUT "</diff>\n"; } print OUT "</command>\n"; } close(OUT); } sub print_session { my $output_filename = $_[0]; my $local_session_id = $_[1]; return if not defined($Sessions{$local_session_id}); open(OUT, ">>", $output_filename) or die "Can't open $output_filename for writing\n"; print OUT "<session>\n"; my %session = %{$Sessions{$local_session_id}}; for my $key (keys %session) { print OUT "<$key>".$session{$key}."</$key>\n" } print OUT "</session>\n"; close(OUT); } sub send_cache { # Если в кэше что-то накопилось, # попытаемся отправить это на сервер # my $cache_was_sent=0; if (open(CACHE, $Config{cache})) { local $/; my $cache = <CACHE>; close(CACHE); my $socket = IO::Socket::INET->new( PeerAddr => $Config{backend_address}, PeerPort => $Config{backend_port}, proto => "tcp", Type => SOCK_STREAM ); if ($socket) { print $socket $cache; close($socket); $cache_was_sent = 1; } } return $cache_was_sent; } sub save_cache_stat { open (CACHE, ">$Config{cache_stat}"); for my $f (keys %Script_Files) { print CACHE "$f\t",$Script_Files{$f}->{size},"\t",$Script_Files{$f}->{tell},"\n"; } close(CACHE); } sub load_cache_stat { if (open (CACHE, "$Config{cache_stat}")) { while(<CACHE>) { chomp; my ($f, $size, $tell) = split /\t/; $Script_Files{$f}->{size} = $size; $Script_Files{$f}->{tell} = $tell; } close(CACHE); }; } main(); sub process_was_killed { $Killed = 1; } sub main { $| = 1; init_variables(); init_config(); if ($Config{"mode"} ne "daemon") { # В нормальном режиме работы нужно # считать скрипты, обработать их и записать # результат выполнения в результирующий файл. # После этого завершить работу. for my $lab_log (split (/\s+/, $Config{"diffs"} || $Config{"input"})) { load_diff_files($lab_log); } load_command_lines($Config{"input"}, $Config{"input_mask"}); sort_command_lines; #process_command_lines; print_command_lines($Config{"cache"}); } else { if (open(PIDFILE, $Config{agent_pidfile})) { my $pid = <PIDFILE>; close(PIDFILE); if ($^O eq 'linux' && $pid &&(! -e "/proc/$pid" || !`grep $Config{"l3-agent"} /proc/$pid/cmdline && grep "uid:.*\b$<\b" /proc/$pid/status`)) { print "Removing stale pidfile\n"; unlink $Config{agent_pidfile} or die "Can't remove stale pidfile ". $Config{agent_pidfile}. " : $!"; } elsif ($^O eq 'freebsd' && $pid && `ps axo uid,pid,command | grep '$<\\s*$pid\\s*$Config{"l3-agent"}' 2> /dev/null`) { print "Removing stale pidfile\n"; unlink $Config{agent_pidfile} or die "Can't remove stale pidfile ". $Config{agent_pidfile}. " : $!"; } elsif ($^O eq 'linux' || $^O eq 'freebsd' ) { print "l3-agent is already running: pid=$pid; pidfile=$Config{agent_pidfile}\n"; exit(0); } else { print "Unknown operating system"; exit(0); } } if ($Config{detach} =~ /^y/i) { #$Config{verbose} = "no"; my $pid = fork; exit if $pid; die "Couldn't fork: $!" unless defined ($pid); open(PIDFILE, ">", $Config{agent_pidfile}) or die "Can't open pidfile ". $Config{agent_pidfile}. " for wrting: $!"; print PIDFILE $$; close(PIDFILE); for my $handle (*STDIN, *STDOUT, *STDERR) { open ($handle, "+<", "/dev/null") or die "can't reopen $handle to /dev/null: $!" } POSIX::setsid() or die "Can't start a new session: $!"; $0 = $Config{"l3-agent"}; $SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&process_was_killed; } while (not $Killed) { @Command_Lines = (); @Command_Lines_Index = (); for my $lab_log (split (/\s+/, $Config{"diffs"} || $Config{"input"})) { load_diff_files($lab_log); } load_cache_stat(); load_command_lines($Config{"input"}, $Config{"input_mask"}); if (@Command_Lines) { sort_command_lines; #process_command_lines; print_command_lines($Config{"cache"}); } save_cache_stat(); if (-e $Config{cache} && (stat($Config{cache}))[7]) { send_cache() && unlink($Config{cache}); } sleep($Config{"daemon_sleep_interval"} || 1); } unlink $Config{agent_pidfile}; } } sub init_variables { }