#!/usr/bin/perl

# needrestart - Restart daemons after library updates.
#
# Authors:
#   Thomas Liske <thomas@fiasko-nw.net>
#
# Copyright Holder:
#   2013 (C) Thomas Liske [http://fiasko-nw.net/~thomas/]
#
# License:
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this package; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#

use Getopt::Std;
use NeedRestart;
use NeedRestart::UI;

use warnings;
use strict;

$|++;
$Getopt::Std::STANDARD_HELP_VERSION++;

sub HELP_MESSAGE {
    print <<USG;
Usage:

  needrestart [-vn] [-c <cfg>] [-r <mode>]

    -v		be more verbose
    -n		set default answer to 'no'
    -c <cfg>	config filename
    -r <mode>	set restart mode
	l	(l)ist only
	i	(i)nteractive restart
	a	(a)utomatically restart
    -b		enable batch mode
    --help      show this help
    --version   show version information

USG
}

sub VERSION_MESSAGE {
    print <<LIC;

needrestart $NeedRestart::VERSION - Restart daemons after library updates.

Authors:
  Thomas Liske <thomas\@fiasko-nw.net>

Copyright Holder:
  2013 (C) Thomas Liske [http://fiasko-nw.net/~thomas/]

Upstream:
  https://github.com/liske/needrestart

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

LIC
#/
}

my %nrconf = (
    procfs => '/proc',
    verbose => 0,
    hook_d => '/etc/needrestart/hook.d',
    restart => 'i',
    defno => 0,
    blacklist => [],
);

# backup ARGV (required for Debconf)
my @argv = @ARGV;

our $opt_c = '/etc/needrestart/needrestart.conf';
our $opt_v;
our $opt_r = $nrconf{restart};
our $opt_n;
our $opt_b;
unless(getopts('c:vr:nb')) {
    HELP_MESSAGE;
    exit 1;
}

# restore ARGV
@ARGV = @argv;

die "ERROR: Could not read config file '$opt_c'!\n" unless(-r $opt_c || $opt_b);

unless($opt_b) {
    eval `cat "$opt_c"`;
    die "\n" if($@);
}

$nrconf{verbose}++ if($opt_v);
die "Hook directory '$nrconf{hook_d}' is invalid!\n" unless(-d $nrconf{hook_d} || $opt_b);
die "ERROR: Unknown restart option '$opt_r'!\n" unless($opt_r =~ /^(l|i|a)$/);

$nrconf{defno}++ if($opt_n);

warn "WARNING: This program should be run as root!\n" if($< != 0);

# get current runlevel, fallback to '2'
my $runlevel = `who -r` || '';
chomp($runlevel);
$runlevel = 2 unless($runlevel =~ s/^.+run-level (\S)\s.+$/$1/);

# get UI
my $ui = ($opt_b ? NeedRestart::UI->new() : needrestart_ui($opt_v));
die "Error: no UI class available!\n" unless(defined($ui));

sub fork_pipe(@) {
    my $pid = open(HPIPE, '-|');
    defined($pid) || die "Can't fork: $!\n";

    if($pid == 0) {
	close(STDIN);
	close(STDERR) unless($nrconf{verbose});

	undef $ENV{LANG};

	exec(@_);
	exit;
    }

    \*HPIPE
}

sub parse_lsbinit($) {
    my $rc = '/etc/init.d/'.shift;
    my %lsb;

    open(HLSB, '<', $rc) || die "Can't open $rc: $!\n";
    my $found;
    while(my $line = <HLSB>) {
	unless($found) {
	    $found++ if($line =~ /^### BEGIN INIT INFO/);
	    next;
	}
	elsif($line =~ /^### END INIT INFO/) {
	    last;
	}

	chomp($line);
	$lsb{lc($1)} = $2 if($line =~ /^# ([^:]+):\s+(.+)$/);
    }

    unless($found) {
	print STDERR "WARNING: $rc has no LSB tags!\n" unless(%lsb);
	return undef;
    }

    # pid file heuristic
    $found = 0;
    my %pidfiles;
    while(my $line = <HLSB>) {
	if($line =~ m@(\S*/run/[^/]+.pid)@ && -r $1) {
	    $pidfiles{$1}++;
	    $found++;
	}
    }
    $lsb{pidfiles} = [keys %pidfiles] if($found);
    close(HLSB);

    return %lsb;
}

my %restart;

# inspect only pids
my @pids = sort { $a <=> $b} map {/^$nrconf{procfs}\/(\d+)$/ ? ($1) : ()} <$nrconf{procfs}/*>;
$ui->progress_prep($#pids + 1, 'Scanning processes');
for my $pid (@pids) {
    my $restart = 0;

    # get executable (Linux 2.2+)
    my $bin = readlink("$nrconf{procfs}/$pid/exe");

    # orphaned binary
    $restart++ if (defined($bin) && $bin =~ s/ \(deleted\)$//);
    $ui->progress_step($bin);

    # ignore kernel threads
    next unless(defined($bin));

    # ignore blacklisted binaries
    next if(grep { $bin =~ /$_/; } @{$nrconf{blacklist}});

    # read file mappings (Linux 2.0+)
    unless($restart) {
	open(HMAP, '<', "$nrconf{procfs}/$pid/maps") || next;
	while(<HMAP>) {
	    chomp;
	    my ($maddr, $mperm, $moffset, $mdev, $minode, $path) = split(/\s+/);
	    
	    # skip special handles and non-executable mappings
	    next unless($minode != 0 && $path ne '' && $mperm =~ /x/);
	    
	    # check for non-existing libs
	    unless(-e $path) {
		unless($path =~ m@^/tmp/@) {
		    print STDERR "#$pid uses non-existing $path\n" if($nrconf{verbose});
		    $restart++;
		    last;
		}
	    }
	    
	    # get on-disk info
	    my ($sdev, $sinode) = stat($path);
	    last unless(defined($sinode));
	    $sdev = sprintf("%02x:%02x", $sdev >> 8, $sdev & 0xff);
	    
	    # compare maps content vs. on-disk
	    if($mdev ne $sdev || $minode ne $sinode) {
		print STDERR "#$pid uses obsolete $path\n" if($nrconf{verbose});
		$restart++;
		last;
	    }
	}
	close(HMAP);
    }

    # restart needed?
    next unless($restart);

    my $pkg;
    foreach my $hook (sort <$nrconf{hook_d}/*>) {
	print STDERR "#$pid running $hook\n" if($nrconf{verbose});

	my $prun = fork_pipe($hook, ($nrconf{verbose} ? qw(-v) : ()), $bin);
	while(<$prun>) {
	    chomp;
	    my @v = split(/\|/);

	    if($v[0] eq 'PACKAGE' && $v[1]) {
		$pkg = $v[1];
		print STDERR "#$pid package: $v[1]\n" if($nrconf{verbose});
		next;
	    }

	    if($v[0] eq 'RC') {
		my %lsb = parse_lsbinit($v[1]);

		unless(%lsb && exists($lsb{'default-start'})) {
		    # If the script has no LSB tags we consider to call it later - they
		    # are broken anyway.
		    $restart{$pkg}->{$v[1]}++
		}
		# In the run-levels S and 1 no daemons are being started (normaly).
		# We don't call any rc.d script not started in the current run-level.
		elsif($lsb{'default-start'} =~ /$runlevel/) {
		    # If a pidfile has been found, try to look for the daemon and ignore
		    # any forked/detached childs (just a heuristic due Debian Bug#721810).
		    if(exists($lsb{pidfiles})) {
			foreach my $pidfile (@{ $lsb{pidfiles} }) {
			    open(HPID, '<', "$pidfile") || next;
			    my $p = <HPID>;
			    close(HPID);

			    if(int($p) == $pid) {
				print STDERR "#$pid has been started by $v[1] - triggering\n" if($nrconf{verbose});
				$restart{$pkg}->{$v[1]}++;
				last;
			    }
			}
		    }
		    else {
			print STDERR "no pidfile reference found at $v[1] - triggering\n" if($nrconf{verbose});
			$restart{$pkg}->{$v[1]}++
		    }
		}
		else {
		    print STDERR "#$pid rc.d script $v[1] should not start in the current run-level($runlevel)\n" if($nrconf{verbose});
		}
	    }
	}

	last if(defined($pkg));
    }
}
$ui->progress_fin(1);

print "NEEDRESTART-VER: $NeedRestart::VERSION\n" if($opt_b);

unless(scalar %restart) {
    $ui->notice('No services required to be restarted.') unless($opt_b);
    exit 0;
}

if($opt_b || $opt_r ne 'i') {
    $ui->notice('Services to be restarted:');
    foreach my $pkg (keys %restart) {
	if($opt_b) {
	    print "NEEDRESTART-PKG: $pkg\n";
	    next;
	}

	print "\n$pkg:\n" unless($opt_r eq 'a');
	foreach my $rc (keys %{$restart{$pkg}}) {
	    if($opt_r eq 'a') {
		system("/etc/init.d/$rc", 'restart');
	    }
	    else {
		print "- $rc\n";
	    }
	}
    }
}
else {
    $ui->query_pkgs('Services to be restarted:', $nrconf{defno}, \%restart, sub { system('/etc/init.d/'.shift, 'restart'); });
}
