#!/usr/bin/perl

use strict;
use warnings;
use 5.020; # needs at least Debian 8 Jessie

# No modules, must work with just the essential package perl-base

# Check commandline options
my $work_on_backup = 0;
my $usage_text = <<"EOT";
Usage: $0 [-b]

$0 has no parameters and only these commandline flags:

-b          work on backup database "records.old" instead of "records".
-h, --help  show this help.

See https://github.com/rpodgorny/uptimed/issues/2,
https://github.com/rpodgorny/uptimed/pull/22 and
https://bugs.debian.org/654830 for details on the issue this script
has been written for.
EOT

if (@ARGV) {
    if ($ARGV[0] eq '--help' or $ARGV[0] eq '-h') {
        print "$0 -- Fix uptimed records db wrt. duplicate entries due to leap seconds\n\n".
            $usage_text;
        exit 0;
    }

    if ($#ARGV >= 1) {
        die "Error: too many arguments.\n\n".
            $usage_text;
    }

    if ($ARGV[0] ne '-b') {
        die "Error: unknown commandline option: $ARGV[0]\n\n".
            $usage_text;
    }

    if ($ARGV[0] eq '-b') {
        $work_on_backup = 1;
    }
}

# Check if we're running as root
if ($> != 0) {
    die "$0 needs to be run with super-user (root) permissions.\n";
}

# Constants taken from the 0.4.6 upstream release
my $boot_fuzz = 30; # seconds

# Cache / lookup variables
my @records = ();
my %record_by_boot_time = ();
my @ch_cmd;
my $last_boot_time = 0;
my $last_boot_record = '0:0:none';
my @last_boot_record = (0,0,'none');

# Filehandle variables
my $infh = undef;
my $outfh = undef;

# Spool directory and file
my $spooldir = '/var/spool/uptimed';
my $records = "$spooldir/records".($work_on_backup ? '.old' : '');
my $records_backup = "$records.fixup-duplicates-backup";

# Temporary file
my $mktemp_cmd = "mktemp $spooldir/uptime-records-fixup-XXXXXXXXXX";
my $tempfile = `$mktemp_cmd`;

# Check exit code of mktemp command
if    ($? == -1)       { die "\"$mktemp_cmd\" failed to execute: $!"; }
elsif ($? & 127)       { die "\"$mktemp_cmd\" died with signal ".($? & 127); }
elsif (($? >> 8) != 0) { die "\"$mktemp_cmd\" exited with value ".($? >> 8); }
chomp($tempfile);

# Then fixup the permissions of the temporary file.
@ch_cmd = ('chown', "--reference=$records", $tempfile);
system(@ch_cmd) == 0 or die '"'.join(' ', @ch_cmd)."\" failed: $?";

@ch_cmd = ('chmod', "--reference=$records", $tempfile);
system(@ch_cmd) == 0 or die '"'.join(' ', @ch_cmd)."\" failed: $?";

# Telling the use that we're going to fixup that records file
say "$0: Will fixup $records for https://bugs.debian.org/654830".
    "\nrespectively https://github.com/rpodgorny/uptimed/issues/2".
    "\nleftovers.";

# Read the original records file
open($infh, '<', $records) or die "Can't open $records: $!";
while (my $line = <$infh>) {
    # Store original line minus the trailing line break;
    chomp($line);
    push(@records, $line);

    # Extract the boot time and save
    my @line = split(/:/, $line);
    $record_by_boot_time{$line[1]} = $line;
}
close($infh);

# Sort boot times and check if there's any difference less than
# $boot_fuzz, but with an uprecord higher than $boot_fuzz (and the
# same kernel).
foreach my $boot_time (sort { $a <=> $b } keys %record_by_boot_time) {
    my $boot_record = $record_by_boot_time{$boot_time};
    my @boot_record = split(/:/, $boot_record);

    # Check for boot times close behind each other. Can only be
    # consecutive boot times, as we sorted the whole list.
    if ($last_boot_time + $boot_fuzz > $boot_time) {
        # Now cross-check if the uptime is bigger than $boot_fuzz
        if ($boot_record[0] > $boot_fuzz) {
            # Now compare the kernels
            if ($boot_record[2] eq $last_boot_record[2]) {
                say "Dropping near-duplicate uptimed entry \"$last_boot_record\".\n".
                    "                  (more recent entry: \"$boot_record\")";
                @records = grep { $_ ne $last_boot_record } @records;
                # Check for higher uptime record
                if ($last_boot_record[0] > $boot_record[0]) {
                    my $merged_boot_record = $boot_record;
                    # Re-add leap-seconds
                    my $fixed_uptime =
                        $last_boot_record[0] + ($boot_time - $last_boot_time);
                    $merged_boot_record =~
                        s/^$boot_record[0]:/$fixed_uptime:/;
                    $boot_record = $merged_boot_record;
                    @records =
                        map { s/^$boot_record[0]:/$fixed_uptime:/r }
                        @records;
                }
            } else {
                warn "Kernel differs between \"$last_boot_record\" and \"$boot_record\", not removed.\n";
            }
        } else {
            warn "Two close boots, but with very low uptime: $last_boot_record and $boot_record\", not removed.\n";
        }
    }
    $last_boot_time = $boot_time;
    $last_boot_record = $boot_record;
    @last_boot_record = @boot_record;
}

# Save the result as temporary file
#say $tempfile;
open($outfh, '>', $tempfile) or die "Can't write to $tempfile: $!";
foreach my $record (@records) {
    say $outfh $record or die "Couldn't write \"$record\" to $tempfile: $!";
}
close($outfh);

# Finally rename the original file and replace it with the new file.
if (! -e $records_backup) {
    rename($records, $records_backup)
        or die "Couldn't rename $records to $records_backup: $!";
}

rename($tempfile, $records)
    or die "Couldn't rename $tempfile to $records: $!";

say "$records backed up at $records_backup and replaced with fixed version.\n";
