#!/usr/bin/perl -w

# Copyright (C) 2007-2012 Christoph Berg <myon@debian.org>
# Copyright (C) 2009-2010 Emil Larsson <emil.larsson@qbranch.se>
# Copyright (C) 2010-2011 Axel Beckert <abe@debian.org>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

my $ext_apt_config = "/etc/hobbit";
use strict;
use IPC::Open3;
use Hobbit qw(file_to_list_of_regexps);

my $no_repo_accept_file = $ext_apt_config."/apt_no_repo_accept";
my $reject_file = $ext_apt_config."/apt_reject";
my @no_repo_accept = file_to_list_of_regexps($no_repo_accept_file);
my @reject = file_to_list_of_regexps($reject_file);

$ENV{'PATH'} = '/bin:/sbin:/usr/bin:/usr/sbin';
$ENV{'LC_ALL'} = 'C';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

my $bb = new Hobbit('apt');

my %packages;

open P, "dpkg --get-selections |" or die "dpkg: $!";
while (<P>) {
	if (/^([^\s:]+)(?::\S+)?\s+(\S+)/) { # pkg[:arch] install/hold/...
		next unless $2 eq "install" or $2 eq "hold";
		$packages{$1} = $2;
	}
}
close P;

if (-x '/usr/bin/aptitude') {
	open P, "/usr/bin/aptitude search '~ahold' |" or die "aptitude: $!";
	while (<P>) {
		if (/^.h.\s+(\S+)/) {
			$packages{$1} = 'hold';
		}
	}
	close P;
}

my $pid = open3(\*IN, \*P, \*ERR, qw/xargs -r apt-cache policy/);
die "open2: $!" unless $pid;

if (!fork()) {
	close P;
	foreach my $p (sort keys %packages) {
		print IN "$p\n";
	}
	close IN;
	exit 0;
}
close IN;
close ERR;

my ($pkg, $inst, $cand, $pin, $pinprio, $in_dist, $dist, $has_repo);
my (@up, @upgrades, @sec, @security, @holdup, @holdupgrades, @holdsec, @holdsecurity, @no_repo, @no_repo_pinned, @no_repo_accepted, @rejected);

sub try_pkg ()
{
	if ($inst ne "(none)" and grep { $pkg =~ $_ } @reject) {
		push @rejected, "$pkg ($inst)";
		return
	}
	if ($inst ne "(none)" and not $has_repo) {
		if (defined $pin and $pinprio > 0) {
			push @no_repo_pinned, "$pkg ($inst) $pinprio";
		} elsif ($packages{$pkg} eq "hold") {
			push @no_repo_pinned, "$pkg ($inst) hold";
		} else {
			if (grep { $pkg =~ $_ } @no_repo_accept) {
				push @no_repo_accepted, "$pkg ($inst)";
			} else {
				push @no_repo, "$pkg ($inst)";
			}
		}
	}
	return if $inst eq $cand;
	if ($packages{$pkg} eq "hold") {
		if ($dist and $dist =~ /updates/) {
			push @holdsec, $pkg;
			push @holdsecurity, "$pkg ($inst $cand)";
		} else {
			push @holdup, $pkg;
			push @holdupgrades, "$pkg ($inst $cand)";
		}
		return;
	}
	if ($dist and $dist =~ /updates/) {
		push @sec, $pkg;
		push @security, "$pkg ($inst $cand)";
	} else {
		push @up, $pkg;
		push @upgrades, "$pkg ($inst $cand)";
	}
}

while (<P>) {
	if (/^(\S+):/) {
		my $next_pkg = $1;
		try_pkg () if $pkg;
		$pkg = $next_pkg;
		undef $dist;
		undef $has_repo;
		undef $pin;
		undef $pinprio;
	}
	$inst = $1 if / +Installed: (.+)/;
	$cand = $1 if / +Candidate: (.+)/;
	$pin = $1 if / +Package pin: (.+)/ and $1 eq $inst;
	if (/^[ *]+(\S+) (\d+)$/) {
		$in_dist = ($1 eq $cand);
		$pinprio = $2;
	}
	if ($in_dist and /^ +\d+ \S+ (\S+)/) { # 700 http://localhost lenny/main Packages
		$dist .= "$1 ";
		$has_repo = 1 if m( (https?|ftp)://| file:);
	}
}
try_pkg ();
close P;

waitpid $pid, 0;

sub pkgreport($$\@;\@) {
	my ($title, $color, $longlist, $shortlist) = @_;

	if (@{$longlist}) {
		$bb->print("\n");
		my $number = scalar @{$longlist};
		$bb->color_line($color, "$title ($number):");
		$bb->print(' apt-get install ' . join (' ', @{$shortlist})) if $shortlist;
		$bb->print("\n");
		foreach (sort @{$longlist}) { $bb->print("   $_\n"); }
	}
}

pkgreport('Rejected packages', 'red', @rejected);
pkgreport('Security updates', 'red', @security, @sec);
pkgreport('Other updates', 'yellow', @upgrades, @up);
pkgreport('Security updates on hold', 'green', @holdsecurity, @holdsec);
pkgreport('Other updates on hold', 'green', @holdupgrades, @holdup);
pkgreport('Packages not installed from apt repositories', 'yellow', @no_repo);
pkgreport('Pinned/held packages not installed from apt repositories', 'green', @no_repo_pinned);
pkgreport('Accepted packages not installed from apt repositories', 'green', @no_repo_accepted);

$bb->print("\n");

# apt-get update will also exit with status 0 on some errors, and
# /var/lib/apt/lists/lock will be updated in any case. We suggest to use
# something like the following in /etc/cron.d/:
# 44 */4	* * *	root  apt-get update -qq > /var/lib/apt/update_output 2>&1 && [ ! -s /var/lib/apt/update_output ] && date -u > /var/lib/apt/update_success

# stamp files which should be all checked, take the newest one as
# time-stamp for the last update.
my @stamp_files_check_all = qw(
	/var/lib/apt/update_success
	/var/lib/apt/periodic/update-stamp
);

# stamp files in order of decreasing usefulness, just check the first
# one found and only if none of the files mentioned above were found.
my @stamp_files_check_first = qw(
	/var/lib/apt/lists
	/var/lib/apt/lists/partial
	/var/cache/apt/pkgcache.bin
	/var/lib/apt/lists/lock
);

my $last_update = 'never';
foreach my $stamp_file (@stamp_files_check_all) {
	my $last_update_tmp = -M $stamp_file;
	if ($last_update_tmp and
	    ($last_update eq 'never' or $last_update_tmp < $last_update)) {
		$last_update = $last_update_tmp;
	}
}

if ($last_update eq 'never') {
    foreach my $stamp_file (@stamp_files_check_first) {
	if (-e  $stamp_file) {
	    $last_update = -M _;
	    last
	}
    }
}

my $updatecolor;
if ($last_update eq 'never' or $last_update >= 7) {
	$updatecolor = 'red';
} elsif ($last_update >= 1.5) {
	$updatecolor = 'yellow';
} else {
	$updatecolor = 'green';
}
$bb->color_line($updatecolor, "Last apt update: ".
		($last_update eq 'never' ? 'never' :
		 sprintf("%.1f day(s) ago\n", $last_update)));

# If /var/lib/apt/update_output is present, print its content

my $outputfile = '/var/lib/apt/update_output';
if (-s $outputfile) {
	$bb->print("\n");
	my $mtime = scalar localtime((stat $outputfile)[9]);
	$bb->color_line('red', "Errors from $mtime:\n");
	open F, $outputfile or die "$outputfile: $!";
	while (<F>) {
		$bb->print("   $_");
	}
	close F;
}

$bb->send;
