#!/usr/bin/perl

# needrestart - Restart daemons after library updates.
#
# Authors:
#   Thomas Liske <thomas@fiasko-nw.net>
#
# Copyright Holder:
#   2013 - 2014 (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 NeedRestart::Interp;
use NeedRestart::Kernel;
use NeedRestart::Utils;

use warnings;
use strict;

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

my $LOGPREF = '[main]';
my $is_systemd = -d qq(/run/systemd/system);

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 - 2014 (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
#/
}

our %nrconf = (
    verbose => 0,
    hook_d => '/etc/needrestart/hook.d',
    restart => 'i',
    defno => 0,
    blacklist => [],
    interpscan => 1,
    kernelhints => 1,
);

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

our $opt_c = '/etc/needrestart/needrestart.conf';
our $opt_v;
our $opt_r;
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);

# slurp config file
eval `cat "$opt_c"`;
die "$@\n" if($@);

# fallback to stdio on verbose mode
$nrconf{verbose}++ if($opt_v);
$nrconf{ui} = qq(NeedRestart::UI::stdio) if($nrconf{verbose});

die "Hook directory '$nrconf{hook_d}' is invalid!\n" unless(-d $nrconf{hook_d} || $opt_b);
$opt_r = $nrconf{restart} unless(defined($opt_r));
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($nrconf{verbose}, $nrconf{ui}));
die "Error: no UI class available!\n" unless(defined($ui));

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

print STDERR "$LOGPREF detected systemd\n" if($nrconf{verbose} && $is_systemd);

my @systemd_restart;
sub restart_cmd($) {
    my $rc = shift;

    if($rc =~ /.+\.service$/) {
	push(@systemd_restart, $rc);
	();
    }
    else {
	(qq(service), $rc, qq(restart));
    }
}

my %restart;
my @ign_pids=($$, getppid());

# inspect only pids
my $ptable = nr_ptable();
$ui->progress_prep(scalar keys %$ptable, 'Scanning processes...');
my %stage2;
for my $pid (sort {$a <=> $b} keys %$ptable) {
    $ui->progress_step;

    # skip myself
    next if(grep {$pid == $_} @ign_pids);

    my $restart = 0;
    my $exe = nr_readlink($pid);

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

    # orphaned binary
    $restart++ if (defined($exe) && $exe =~ s/ \(deleted\)$//);  # Linux
    $restart++ if (defined($exe) && $exe =~ s/^\(deleted\)//);   # Linux VServer
    print STDERR "$LOGPREF #$pid uses obsolete binary $exe\n" if($restart && $nrconf{verbose});

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

    # read file mappings (Linux 2.0+)
    unless($restart) {
	open(HMAP, '<', "/proc/$pid/maps") || next;
	while(<HMAP>) {
	    chomp;
	    my ($maddr, $mperm, $moffset, $mdev, $minode, $path) = split(/\s+/);
	    
	    # skip special handles and non-executable mappings
	    next unless(defined($path) && $minode != 0 && $path ne '' && $mperm =~ /x/);
	    
	    # check for non-existing libs
	    unless(-e $path) {
		unless($path =~ m@^/tmp/@) {
		    print STDERR "$LOGPREF #$pid uses non-existing $path\n" if($nrconf{verbose});
		    $restart++;
		    last;
		}
	    }
	    
	    # get on-disk info
	    my ($sdev, $sinode) = stat($path);
	    last unless(defined($sinode));
	    my @sdevs = (
		# glibc gnu_dev_* definition from sysmacros.h
		sprintf("%02x:%02x", (($sdev >> 8) & 0xfff) | (($sdev >> 32) & ~0xfff), (($sdev & 0xff) | (($sdev >> 12) & ~0xff))),
		# Traditional definition of major(3) and minor(3)
		sprintf("%02x:%02x", $sdev >> 8, $sdev & 0xff),
		# kFreeBSD: /proc/<pid>/maps does not contain device IDs
		qq(00:00)
		);
	    
	    # compare maps content vs. on-disk
	    unless($minode eq $sinode && ((grep {$mdev eq $_} @sdevs) ||
					  # BTRFS breaks device ID mapping completely...
					  # ignoring unnamed device IDs for now
					  $mdev =~ /^00:/)) {
		print STDERR "$LOGPREF #$pid uses obsolete $path\n" if($nrconf{verbose});
		$restart++;
		last;
	    }
	}
	close(HMAP);
    }

    unless($restart || !$nrconf{interpscan}) {
	$restart++ if(needrestart_interp_check($nrconf{verbose}, $pid, $exe));
    }

    # restart needed?
    next unless($restart);

    # find parent process
    my $ppid = $ptable->{$pid}->{ppid};
    if($ppid != $pid && $ppid > 1) {
	print STDERR "$LOGPREF #$pid is a child of #$ppid\n" if($nrconf{verbose});

	unless(exists($stage2{$ppid})) {
	    my $pexe = nr_readlink($ppid);
	    # ignore kernel threads
	    next unless(defined($pexe));

	    $stage2{$ppid} = $pexe;
	}
    }
    else {
	print STDERR "$LOGPREF #$pid is not a child\n" if($nrconf{verbose});
	$stage2{$pid} = $exe;
    }
}
$ui->progress_fin;

if(scalar keys %stage2) {
    $ui->progress_prep(scalar keys %stage2, 'Scanning candidates...');
    foreach my $pid (sort {$a <=> $b} keys %stage2) {
	$ui->progress_step;

	# skip myself
	next if(grep {$pid == $_} @ign_pids);

	my $exe = nr_readlink($pid);
	$exe =~ s/ \(deleted\)$//;  # Linux
	$exe =~ s/^\(deleted\)//;   # Linux VServer
	print STDERR "$LOGPREF #$pid exe => $exe\n" if($nrconf{verbose});

	# try to find interpreter source file
	($exe) = (needrestart_interp_source($nrconf{verbose}, $pid, $exe), $exe);

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

	if($is_systemd) {
	    my $systemctl = nr_fork_pipe($nrconf{verbose}, qq(systemctl), qq(-n), qq(0), qq(--full), qq(status), $pid);
	    my $ret = <$systemctl>;
	    close($systemctl);

	    if(defined($ret) && $ret =~ /^([^.]+\.service) /) {
		print STDERR "$LOGPREF #$pid is $1\n" if($nrconf{verbose});
		$restart{$1}++;
		next;
	    }
	}

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

	    my $found = 0;
	    my $prun = nr_fork_pipe($nrconf{verbose}, $hook, ($nrconf{verbose} ? qw(-v) : ()), $exe);
	    my @nopids;
	    while(<$prun>) {
		chomp;
		my @v = split(/\|/);

		if($v[0] eq 'PACKAGE' && $v[1]) {
		    $pkg = $v[1];
		    print STDERR "$LOGPREF #$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.
			print STDERR "$LOGPREF no LSB headers found at $v[1]\n" if($nrconf{verbose});
			push(@nopids, $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 "$LOGPREF #$pid has been started by $v[1] - triggering\n" if($nrconf{verbose});
				    $restart{$v[1]}++;
				    $found++;
				    last;
				}
			    }
			}
			else {
 			    print STDERR "$LOGPREF no pidfile reference found at $v[1]\n" if($nrconf{verbose});
			    push(@nopids, $v[1]);
			}
		    }
		    else {
			print STDERR "$LOGPREF #$pid rc.d script $v[1] should not start in the current run-level($runlevel)\n" if($nrconf{verbose});
		    }
		}
	    }

	    # No perfect hit - call any rc scripts instead.
	    if(!$found && $#nopids > -1) {
		foreach my $rc (@nopids) {
		    $restart{$rc}++;
		}
		$found++;
	    }

	    last if($found);
	}
    }
    $ui->progress_fin;
}

my ($kresult, %kvars) = ($nrconf{kernelhints} || $opt_b ? nr_kernel_check($nrconf{verbose}, $ui) : ());

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

if(defined($kresult)) {
    if($opt_b) {
	print "NEEDRESTART-KCUR: $kvars{KVERSION}\n";
	print "NEEDRESTART-KEXP: $kvars{EVERSION}\n" if(defined($kvars{EVERSION}));
	print "NEEDRESTART-KSTA: $kresult\n";
    }
    else {
	if($kresult == NRK_NOUPGRADE) {
	    $ui->notice('Running kernel seems to be up-to-date.');
	}
	elsif($kresult == NRK_ABIUPGRADE) {
	    $ui->announce_abi(%kvars);
	}
	elsif($kresult == NRK_VERUPGRADE) {
	    $ui->announce_ver(%kvars);
	}
	else {
	    $ui->notice('Failed to retrieve available kernel versions.');
	}
    }
}

unless(scalar %restart) {
    $ui->notice('No services need to be restarted.') unless($opt_b);
}
else {
    if($opt_b || $opt_r ne 'i') {
	$ui->notice('Services to be restarted:');
	
	foreach my $rc (keys %restart) {
	    my @cmd = restart_cmd($rc);
	    
	    if($opt_b) {
		print "NEEDRESTART-SVC: $rc\n";
		next;
	    }
	    
	    next unless($#cmd > -1);
	    
	    if($opt_r eq 'a') {
		system(@cmd);
	    }
	    else {
		print join(' ', @cmd)."\n";
	    }
	}
	
	unless($opt_b || $#systemd_restart == -1) {
	    my @cmd = (qq(systemctl), qq(restart), @systemd_restart);
	    if($opt_r eq 'a') {
		print "Restarting services using systemd...\n";
		system(@cmd);
	    }
	    else {
		print join(' ', @cmd)."\n";
	}
	}
    }
    else {
	my $o = 0;

	$ui->query_pkgs('Services to be restarted:', $nrconf{defno}, \%restart,
			sub {
			    my @cmd = restart_cmd(shift);
			    system(@cmd) if($#cmd > -1);
			});
	
	if($#systemd_restart > -1) {
	    print "Restarting services using systemd...\n";
	    system(qq(systemctl), qq(restart), @systemd_restart);
	}
    }
}
