#!/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>] [-bkl]

    -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

  By using the following options only the specified checks are performed:
    -k          check for obsolete kernel
    -l          check for obsolete libraries

    --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 => [],
    blacklist_rc => [],
    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;
our $opt_k;
our $opt_l;
unless(getopts('c:vr:nbkl')) {
    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(1) : needrestart_ui($nrconf{verbose}, $nrconf{ui}));
die "Error: no UI class available!\n" unless(defined($ui));

# enable/disable checks
unless(defined($opt_k) || defined($opt_l)) {
    $opt_k = $opt_l = 1;
}

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);
	();
    }
    elsif($rc eq q(systemd manager)) {
	(qw(systemctl daemon-reexec));
    }
    elsif($rc eq q(sysv init)) {
	(qw(telinit u));
    }
    else {
	(q(service), $rc, q(restart));
    }
}

my %restart;
if(defined($opt_l)) {
    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/);

		# skip special device paths
		next if($path =~ m@^/(SYSV00000000$|drm$|dev/)@);

		# 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) {
		# systemd manager
		if($pid == 1 && $exe =~ m@^/lib/systemd/systemd@) {
		    print STDERR "$LOGPREF #$pid is systemd manager\n" if($nrconf{verbose});
		    $restart{q(systemd manager)}++;
		    next;
		}

		# get unit name from /proc/<pid>/cgroup
		if(open(HCGROUP, qq(/proc/$pid/cgroup))) {
		    my ($rc) = map {
			chomp;
			my ($id, $type, $value) = split(/:/);
			if($type ne q(name=systemd)) {
			    ();
			}
			else {
			    if($value =~ m@/([^/]+\.service)$@) {
				($1);
			    }
			    else {
				print STDERR "$LOGPREF #$pid unexpected cgroup '$value'\n" if($nrconf{verbose});
				();
			    }
			}
		    } <HCGROUP>;
		    close(HCGROUP);

		    if($rc) {
			print STDERR "$LOGPREF #$pid is $rc\n" if($nrconf{verbose});
			$restart{$rc}++;
			next;
		    }
		}

		# did not get the unit name, yet - try systemctl status
		print STDERR "$LOGPREF /proc/#$pid/cgroup: $! - trying systemctl status\n" if($nrconf{verbose} && $!);
		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 =~ /([^.\s]+\.service) /) {
		    print STDERR "$LOGPREF #$pid is $2\n" if($nrconf{verbose});
		    $restart{$2}++;
		    next;
		}
	    }
	    else {
		# sysv init
		if($pid == 1 && $exe =~ m@^/sbin/init@) {
		    print STDERR "$LOGPREF #$pid is sysv init\n" if($nrconf{verbose});
		    $restart{q(sysv init)}++;
		    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;
    }
}

# Apply rc/service blacklist
foreach my $rc (keys %restart) {
    next unless(scalar grep { $rc =~ /$_/; } @{$nrconf{blacklist_rc}});

    print STDERR "$LOGPREF $rc is blacklisted -> ignored\n" if($nrconf{verbose});
    delete($restart{$rc});
}

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

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

    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.');
	    }
	}
    }
}

if(defined($opt_l)) {
    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 (sort { lc($a) cmp lc($b) } keys %restart) {
		if($opt_b) {
		    print "NEEDRESTART-SVC: $rc\n";
		    next;
		}

		if($opt_r eq 'a') {
		    my @cmd = restart_cmd($rc);
		    next unless($#cmd > -1);

		    system(@cmd);
		}
		else {
		    $ui->notice(" $rc");
		}
	    }
	
	    unless($opt_b || $#systemd_restart == -1) {
		my @cmd = (qq(systemctl), qq(restart), @systemd_restart);
		if($opt_r eq 'a') {
		    $ui->notice('Restarting services using systemd...');
		    system(@cmd);
		}
	    }
	}
	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) {
		$ui->notice('Restarting services using systemd...');
		system(qq(systemctl), qq(restart), @systemd_restart);
	    }
	}
    }
}
