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