1# Idea based on queryresume.pl by Stefan Tomanek 2 3### NOTES/BUGS 4# - /set logresume_query_lines 5# - /set logresume_channel_lines (set to 0 to make this script act more like queryresume.pl) 6# - Coloured logs (/set autolog_colors ON) work perfectly well, and are recommended if you want it to look like you never left 7# - bonus feature: /logtail 10 will print the last 10 lines of a log 8# - bonus feature: /logview will open the log in your PAGER, or do e.g. /logview screen vim -R. You'll need to be using irssi in screen. Running the program without screen is possible, but you need to ^L to redraw after closing it, and if you look at it too long irssi blocks on output and all your connections will ping out 9# - behaviour on channel join fail is potentially a little odd. Unmotivated to test or fix this. 10 11use strict; 12use Irssi; 13use Fcntl qw( :seek O_RDONLY ); 14 15our $VERSION = "0.6"; 16our %IRSSI = ( 17 name => "logresume", 18 description => "print last n lines of logs when opening queries/channels", 19 url => "http://explodingferret.com/linux/irssi/logresume.pl", 20 authors => "ferret", 21 contact => "ferret(tA)explodingferret(moCtoD), ferret on irc.freenode.net", 22 licence => "Public Domain", 23 changed => "2012-10-08", 24 changes => "0.6: added memory of windows that have been logresumed already" 25 . "0.5: added /logtail and /logview" 26 . "0.4: fixed problem with lines containing %, removed use warnings" 27 . "0.3: swapped File::ReadBackwards for internal tail implementation", 28 modules => "", 29 commands => "logtail, logview", 30 settings => "logresume_channel_lines, logresume_query_lines", 31); 32 33Irssi::print "$IRSSI{name} version $VERSION loaded, see the top of the script for help"; 34if ( ! Irssi::settings_get_bool('autolog') ) { 35 Irssi::print( "$IRSSI{name}: /set autolog is currently OFF. This script probably won't work well unless it's ON" ); 36} 37 38Irssi::settings_add_int($IRSSI{name}, 'logresume_channel_lines', 15); 39Irssi::settings_add_int($IRSSI{name}, 'logresume_query_lines', 20); 40 41my $debug = 0; 42sub debug_print { $debug and Irssi::print $IRSSI{name} . ' %RDEBUG%n: ' . $_[0]; } 43sub prettyprint { Irssi::print $IRSSI{name} . ' %Winfo%n: ' . $_[0]; } 44 45# This hash of hashes maps servertag -> item names -> _irssi. The point of this is so that 46# we don't print the last n log entries into a window that just recently had that item in it 47# (e.g. on server reconnect), since that content is like right there already. 48# the _irssi hash key is used as a unique identifier for windows (although they get reused). 49# Was using refnum for this originally, but it's very difficult to implement with that due to 50# the way the 'window refnum changed' and 'window destroyed' signals work (mostly the order 51# they run in). 52my %haveprinted; 53 54# initial fill of hash 55sub inithash { 56 for my $win ( Irssi::windows() ) { 57 for my $winitem ( $win->items() ) { 58 next unless $winitem->{type} eq 'QUERY' or $winitem->{type} eq 'CHANNEL'; 59 next unless defined $winitem->{server} and defined $winitem->{name}; 60 $haveprinted{$winitem->{server}{tag}}{$winitem->{name}} = $win->{_irssi}; 61 } 62 } 63} 64 65inithash(); 66 67# a new log was opened! initiate the process of printing some stuff to the screen 68Irssi::signal_add_last 'log started' => sub { 69 my ( $log ) = @_; 70 my $lines; 71 72 for my $logitem ( @{ $log->{items} } ) { 73 my $server = Irssi::server_find_tag( $logitem->{servertag} ); 74 next unless defined $server; 75 76 next unless defined $logitem->{name}; 77 my $winitem = $server->window_item_find( $logitem->{name} ); 78 next unless defined $winitem; 79 80 my $irssiref = $winitem->window()->{_irssi}; 81 my $servertag = $server->{tag}; 82 my $itemname = $winitem->{name}; 83 84 debug_print( "log started | servertag='$servertag' itemname='$itemname' irssiref='$irssiref'" ); 85 86 if( $winitem->{type} eq 'QUERY' ) { 87 $lines = Irssi::settings_get_int('logresume_query_lines'); 88 } elsif( $winitem->{type} eq 'CHANNEL' ) { 89 $lines = Irssi::settings_get_int('logresume_channel_lines'); 90 } else { 91 next; # other window types not implemented 92 } 93 94 # don't print log output if we already did for this window, as that would indicate the 95 # item was recently in this window, so the scrollback contains this stuff already 96 if( $haveprinted{$servertag}{$itemname} ne $irssiref ) { 97 $haveprinted{$servertag}{$itemname} = $irssiref; 98 debug_print( "log started || not recorded as already printed, will do print_tail" ); 99 print_tail( $winitem, $lines ); 100 } 101 } 102}; 103 104# when windows are destroyed we need to remove entries from %haveprinted 105Irssi::signal_add 'window destroyed' => sub { 106 my ( $win ) = @_; 107 my $irssiref = $win->{_irssi}; 108 debug_print( "window destroyed | refnum='$win->{refnum}' irssiref='$irssiref'" ); 109 110 for my $servertag (keys %haveprinted) { 111 for my $itemname (keys %{$haveprinted{$servertag}}) { 112 if ( $haveprinted{$servertag}{$itemname} eq $irssiref ) { 113 debug_print( "window destroyed || removed servertag='$servertag' itemname='$itemname'" ); 114 $haveprinted{$servertag}{$itemname} = ''; 115 } 116 } 117 } 118}; 119 120Irssi::signal_add 'window item moved' => sub { 121 my ( $to_win, $winitem, $from_win ) = @_; 122 my $servertag = $winitem->{server}{tag}; 123 my $itemname = $winitem->{name}; 124 125 debug_print( "window item moved | servertag='$servertag' itemname='$itemname' was='$haveprinted{$servertag}{$itemname}' fromref='$from_win->{_irssi}' toref='$to_win->{_irssi}'" ); 126 $haveprinted{$servertag}{$itemname} = $to_win->{_irssi}; 127}; 128 129Irssi::signal_add 'query nick changed' => sub { 130 my ( $win, $oldnick ) = @_; 131 132 debug_print( "query nick changed | oldnick='$oldnick' newnick='$win->{name}' transferring='$haveprinted{$win->{server}{tag}}{$oldnick}'" ); 133 $haveprinted{$win->{server}{tag}}{$win->{name}} = $haveprinted{$win->{server}{tag}}{$oldnick}; 134 $haveprinted{$win->{server}{tag}}{$oldnick} = ''; 135}; 136 137sub print_tail { 138 my ( $winitem, $lines ) = @_; # winitem is a channel or query or whatever 139 140 return unless $lines > 0; 141 142 my $log = get_log_filename( $winitem ); 143 return unless defined $log; 144 145 my $winrec = $winitem->window(); # need to print to the window, not the window item 146 147 for( tail( $lines, $log ) ) { # sub tail is defined below 148 s/%/%%/g; # prevent irssi format notation being expanded 149 $winrec->print( $_, MSGLEVEL_NEVER ); 150 } 151 152 $winrec->print( '%K[%Clogresume%n ' . $log . '%K]%n' ); 153} 154 155 156sub get_log_filename { 157 my ( $winitem ) = @_; 158 my ( $tag, $name ) = ( $winitem->{server}{tag}, $winitem->{name} ); 159 160 my @logs = map { $_->{real_fname} } grep { 161 grep { 162 $_->{name} eq $name and $_->{servertag} eq $tag 163 } @{ $_->{items} } 164 } Irssi::logs(); 165 166 unless( @logs ) { 167 debug_print( "no logfile for $tag, $name" ); 168 return undef; 169 } 170 171 debug_print( "surplus logfile for $tag, $name: $_" ) for @logs[1..$#logs]; 172 return $logs[0]; 173} 174 175 176Irssi::command_bind 'logtail' => sub { 177 my ( $lines ) = @_; 178 if ( not $lines =~ /[1-9][0-9]*/ ) { 179 prettyprint( 'usage: /logtail <number>' ); 180 } 181 182 print_tail( Irssi::active_win()->{active}, $lines ); 183}; 184 185 186# irssi will NOT communicate in any way with the server while the command is running, unless the command returns immediately (e.g. running screen in screen, or backgrounded X11 text editor). So use screen. 187# usage: /logview foo bar baz 188# will run: foo bar baz filename.log 189Irssi::command_bind 'logview' => sub { 190 my ( $args, $server, $winitem ) = @_; 191 192 my $log = get_log_filename( $winitem ); 193 return unless defined $log; 194 195 my $pager = $ENV{PAGER} || "less"; 196 my $program = $_[0] || "screen $pager"; 197 198 system( split( / /, $program ), $log ) == 0 or do { 199 if ( $? == -1 ) { 200 prettyprint( "logview: running command '$program $log' failed: $!" ); 201 } elsif ( $? & 127 ) { 202 prettyprint( "logview: running command '$program $log' died with signal " . ( $? & 127 ) ); 203 } else { 204 prettyprint( "logview: running command '$program $log' exited with status " . ( $? >> 8 ) ); 205 } 206 }; 207}; 208 209 210sub tail { 211 my ( $needed_lines, $filename ) = @_; 212 return unless $needed_lines > 0; 213 214 my @lines = (); 215 216 sysopen( my $fh, $filename, O_RDONLY ) or return; 217 binmode $fh; 218 my $blksize = (stat $fh)[11]; 219 220 # start at the end of the file 221 my $pos = sysseek( $fh, 0, SEEK_END ) or return; 222 223 # for the first chunk read a trailing partial block, so we start on what's probably a natural disk boundary 224 # if there's no trailing partial block read a full one 225 # Also guarantees that $pos will become zero before it becomes negative 226 $pos -= $pos % $blksize || $blksize; 227 228 # - 1 is because $lines[0] is partial 229 while ( @lines - 1 < $needed_lines ) { 230 # go to top of this chunk 231 sysseek( $fh, $pos, SEEK_SET ) or last; # partial output better than none 232 233 sysread( $fh, my $buf, $blksize ); 234 last if $!; 235 236 # ruin my lovely generic tail function 237 $buf =~ s/^--- Log.*\n//mg; 238 239 if ( @lines ) { 240 splice @lines, 0, 1, split( /\n/, $buf . $lines[0], -1 ); 241 } else { 242 @lines = split( /\n/, $buf, -1 ); 243 # unix philosophy (as tail, wc, etc.): trailing newline is not a line for counting purposes 244 pop @lines if @lines and $lines[-1] eq ""; 245 } 246 247 last if $pos == 0; 248 249 $pos -= $blksize; 250 } 251 252 return ( $needed_lines >= @lines ? @lines : @lines[ -$needed_lines .. -1 ] ); 253} 254