#!/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 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)utomaticly restart
    -b		enable batch mode

USG
}

sub VERSION_MESSAGE {
    print <<LIC;

needrestart 0.2 - 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,
);

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

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

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});
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/);

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

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

	exec(@_);
	exit;
    }

    \*HPIPE
}

sub query($$) {
    my($query, $def) = @_;
    my @def = ($def eq 'Y' ? qw(yes no) : qw(no yes));

    my $i;
    do {
	print "$query [", ($def eq 'Y' ? 'Yn' : 'yN'), '] ';;
	$i = lc(<STDIN>);
	chomp($i);
	$i =~ s/^\s+//;
	$i =~ s/\s+$//;
    } while(!( ($i) = map { (substr($_, 0, length($i)) eq $i ? ($_) : ())} @def ));

    return $i;
}

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{$1} = $2 if($line =~ /^# ([^:]+):\s+(.+)$/);
    }
    close(HLSB);

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

    return %lsb;
}

my %restart;

# inspect only pids
for my $pid (map {/^$nrconf{procfs}\/(\d+)$/ ? ($1) : ()} <$nrconf{procfs}/*>) {
    # read file mappings (Linux 2.0+)
    open(HMAP, '<', "$nrconf{procfs}/$pid/maps") || next;
    my $restart = 0;
    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/);

	# 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);

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

    unless(-x $bin && $bin =~ / \(deleted\)$/) {
	$bin =~ s/ \(deleted\)$//;
	print STDERR "#$pid binary $bin is obsolete, too\n" if($nrconf{verbose});
    }

    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]);
		# -=[ HACK HACK HACK ]=-
		# In the run-levels S and 1 no daemons are being started (normaly).
		# We don't call any rc script not started in the current run-level.
		# If the script has no LSB tags we consider to call it later - they
		# are broken anyway.
		if(!%lsb || $lsb{'Default-Start'} =~ /$runlevel/) {
		# -=[ HACK HACK HACK ]=-
		    $restart{$pkg}->{$v[1]}++
		}
		else {
		    print STDERR "#$pid rc script $v[1] should not start in the current run-level($runlevel)\n" if($nrconf{verbose});
		}
	    }
	}

	last if(defined($pkg));
    }
}

unless(scalar %restart) {
    print "No services needed to be restarted.\n" unless($opt_b);
    exit 0;
}

print "Services needed to be restarted:\n" unless($opt_b);
foreach my $pkg (keys %restart) {
    if($opt_b) {
	print "NEEDRESTART: $pkg\n";
	next;
    }

    print "\n$pkg:\n";

    foreach my $rc (keys %{$restart{$pkg}}) {
	print " - ";

	my $r = 0;
	if($nrconf{restart} eq 'i') {
	    $r = query("Restart $rc?", ($nrconf{defno} ? 'N' : 'Y')) eq 'yes';
	}
	elsif($nrconf{restart} eq 'a') {
	    print "auto-restart $rc\n";
	    $r++;
	}
	else {
	    print "$rc\n";
	}

	system("/etc/init.d/$rc", 'restart') if($r);
    }
}
