#!/usr/bin/perl

use strict;
use warnings;
use 5.010;

# VERSION
# PODNAME: systray-mdstat
# ABSTRACT: system tray icon which shows the state of local MD RAIDs

=head1 SYNOPSIS

B<systray-mdstat> [ B<-h> | B<--help> | B<--usage> ]

=head1 DESCRIPTION

This program allows one to display an icon indicating the state of
local Linux Software (MD) RAIDs in any freedesktop.org-compliant
status area.

=head1 SEE ALSO

L<smart-notifier(1)>

=head1 NOTES ABOUT THE ORIGIN OF THE CODE

Parts of the code are very loosely based on the outer framework of
fdpowermon by Wouter Verhelst under Poul-Henning Kamp's "Beer-ware"
license, and the mdstat check from Debian's hobbit-plugins package
written by Christoph Berg under the MIT license. (Both stated that the
amount of code I copied is too small to make their copyright apply,
hence I'm not bound to the licenses they used for their code.)

=cut

use List::Util qw(max);
use File::ShareDir ':ALL';
use Try::Tiny;

my $proc_mdstat = '/proc/mdstat';
my @icon_dirs = qw(
    share
    );
my @states = qw( spare ok warn fail );
my @mdstat_checks = (
    # regular expression  => state => string
    [ qr/\[U+\]/                => 1     => 'OK'       ],
    [ qr/(resync|verify|check)/ => 2     => ' (%s)'    ],
    [ qr/\(F\)/                 => 3     => '<b>FAILED</b>'   ],
    [ qr/\[U*_U*\]/             => 3     => '<b>DEGRADED</b>' ],
    );

our $use_notify = 1;
my $old_state = 0;
my $icon;

main() unless caller(0);

sub main {
    use Getopt::Long qw( :config no_auto_abbrev );
    use Gtk3 -init;
    use Glib qw(TRUE FALSE);
    use Glib::Object::Introspection;
    use Pod::Usage;

    my $help;
    my $usage;
    GetOptions(
        'help|h'        => \$help,
        'usage'         => \$usage,
        'read-from=s'   => \$proc_mdstat,
        ) or exit 1;

    pod2usage(0) if $help;
    pod2usage(
            -verbose => 2,
            -exitval => 0,
        ) if $usage;

    if (@ARGV) {
        pod2usage(
            -message => "E: $0 takes no arguments\n",
            -exitval => 1,
            );
    }

    $icon = Gtk3::StatusIcon->new();
    $icon->set_visible(1);

    try {
        Glib::Object::Introspection->setup(
            basename => 'Notify',
            version => '0.7',
            package => "Systray::Mdstat::Notify",
            );
        Systray::Mdstat::Notify->init();
    } catch {
        say "no notify because setup failed: $@";
        $use_notify = 0;
    };

    populate_icon_dirs();

    check_mdstat();
    Glib::Timeout->add_seconds(3, \&check_mdstat);
    Gtk3->main();
}

sub populate_icon_dirs {
    # Unfortunately File::ShareDir functions die instead of returning
    # undef if they can't find an according directory.
    try {
        push(@icon_dirs, dist_dir('systray-mdstat'));
    };
}


sub read_and_parse_mdstat {
    # 0 = unknown
    # 1 = ok
    # 2 = warning
    # 3 = alarm
    my $state = 0;
    my $text = '';
    my $path = shift;

    my $last_md = '';
    if (-e $path) {
        open(my $MDSTAT, '<', $path)
            or die "Can't read from $path despite it exists: $!";
        while (my $line = <$MDSTAT>) {
            if ($line =~ /^(md\d+) *: /) {
                $last_md = $1;
                $text .= "; " unless $text eq '';
            } else {
                foreach my $check (@mdstat_checks) {
                    my $use_format = $check->[2] =~ /\%/;
                    if ($line =~ $check->[0]) {
                        $state = max($state, $check->[1]);
                        if ($use_format) {
                            $text .= sprintf($check->[2], $1);
                        } else {
                            $text .= "$last_md: ".$check->[2];
                        }
                    }
                }
            }
        }
        close $MDSTAT;
        $text = "$path exists but doesn't contain any RAID."
            unless $text;
    } else {
        $text = "No $path found";
    }

    return($text, $state);
}

sub check_mdstat {
    my ($text, $state) = read_and_parse_mdstat($proc_mdstat);
    &update_icon($icon, $text, $state);

    my $message;
    if ($old_state == 0 and $state != 0) {
        $message = "Current $proc_mdstat state: ".$text;
    } else {
        $message = "$proc_mdstat state changed: ".$text;
    }
    if ($state != $old_state) {
        &send_notification($icon, "Software RAID State", $message);
        $old_state = $state;
    }

    # Return explicitly true so that Gtk3 continues to run this
    # function.
    return TRUE;
}

sub update_icon {
    my ($icon, $text, $state) = @_;
    my $path = find_icon_path("harddrive".$states[$state]);

    if ($icon) {
        $icon->set_tooltip_text($text);
        $icon->set_from_file($path);
    } else {
        return "UPDATE ICON: text=$text path=$path";
    }
}

sub send_notification {
    my ($icon, $title, $text) = @_;

    # Check if we're running under a GUI by looking at $icon
    if ($icon) {
        if ($use_notify) {
            my $notif = Systray::Mdstat::Notify::Notification->new(
                $title, $text);
            try {
                $notif->show;
            } catch {
                # Something went wrong trying to show a notification
                # message. Fall back to using a dialog box instead.
                $use_notify = 0;
            };
        }
        if (!$use_notify) {
            my $dialog = Gtk3::MessageDialog->new(
                undef, 'destroy-with-parent', 'warning', 'ok', $text);
            $dialog->run;
            $dialog->destroy;
        }
    } else {
        return "NOTIFY: title=$title text=$text";
    }
}

sub find_icon_path {
    my $basename = shift;
    my $filename = "$basename.png";
    my $fullpath;
    foreach my $path (@icon_dirs) {
        if (-r "$path/$filename") {
            $fullpath = "$path/$filename";
            last;
        }
    }
    die "Icon $filename not found inside ".join(', ', @icon_dirs)
        unless $fullpath;
    return $fullpath;
}
