#!/usr/bin/env perl
#
# This file is part of StorageDisplay
#
# This software is copyright (c) 2014-2023 by Vincent Danjean.
#
# This is free software; you can redistribute it and/or modify it under
# the same terms as the Perl 5 programming language system itself.
#

# PODNAME: storage2dot
# ABSTRACT: analyse and generate a graphical view of a machine storage


use strict;
use warnings;

our $VERSION = '2.02'; # VERSION

# delay module loading so that local data collect can be done
# without extra modules
# use StorageDisplay;
use StorageDisplay::Collect;

my $cleanup_readmode=0;
sub collect_from_remote {
    my $remote = shift;
    my $content='';
    eval {
	require Net::OpenSSH;
        Net::OpenSSH->import;
        require Term::ReadKey;
        Term::ReadKey->import;
    } or die "Cannot load required modules (Net::OpenSSH and/or Term::ReadKey) for remote data collect: $!\n";
    END {
        if ($cleanup_readmode) {
            # in case of bug, always restore normal mode
            ReadMode('normal');
        }
    }
    my $ssh = Net::OpenSSH->new($remote);
    $ssh->error and
        die "Couldn't establish SSH connection: ". $ssh->error;

    my ($in, $out, $pid) = $ssh->open2(
        #'cat',
        'perl', '--', '-',
        );

    my $fdperlmod;
    open($fdperlmod, '<', $INC{'StorageDisplay/Collect.pm'})
        or die "Cannot open ".INC{'StorageDisplay/Collect.pm'}.": $!\n";
    #use Sys::Syscall;
    #Sys::Syscall::sendfile($in, $fdperlmod);
    {
        while(defined(my $line=<$fdperlmod>)) {
	    last if $line =~ m/^__END__\s*$/;
            print $in $line;
        }
        close $fdperlmod;
    }
    #print $in "StorageDisplay::Collect::dump_collect;\n";
    my @args = (@_, 'LocalBySSH');
    my $cmd = "StorageDisplay::Collect::dump_collect('".join("','", @args)."');\n";
    print STDERR 'Running through SSH: ',$cmd;
    print $in $cmd;
    print $in "__END__\n";
    flush $in;

    use IO::Select;
    use POSIX ":sys_wait_h";
    my $sel = IO::Select->new(\*STDIN, $out);
    my $timeout = 1;
    $cleanup_readmode=1;
    ReadMode('noecho');
    my ($in_closed,$out_closed) = (0,0);
    while(1) {
        $!=0;
        my @ready = $sel->can_read($timeout);
        if ($!) {
            die "Error with select: $!\n";
        }
        if (scalar(@ready)) {
            foreach my $fd (@ready) {
                if ($fd == $out) {
                    my $line=<$out>;
                    if (defined($line)) {
                        $content .= $line;
                    } else {
                        $sel->remove($out);
                        close $out;
                        $out_closed=1;
                    }
                } else {
                    my $line=<STDIN>;
                    if (print $in $line) {
                        flush $in;
                    } else {
                        $sel->remove(\*STDIN);
                        close $in;
                        $in_closed=1;
                    }
                }
            }
        } else {
            my $res = waitpid($pid, WNOHANG);
            if ($res==-1) {
                die "Some error occurred ".($? >> 8).": $!\n";
            }
            if ($res) {
                if (!$in_closed) {
                    $sel->remove(\*STDIN);
                    close $in;
                }
                ReadMode('normal');
                last;
            }
            #print STDERR "timeout for $pid\n";
        }
    }
    if (!$out_closed) {
        while (defined(my $line=<$out>)) {
            $content .= $out;
        }
        $sel->remove($out);
        close $out;
    }
    ReadMode('normal');
    $cleanup_readmode=0;
    return $content;
}

use Getopt::Long;
use Pod::Usage;
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Purity = 1;

#use Carp::Always;

my $remote;
my $data;
my $output;

my $collect;
my $recordfile;
my $replayfile;

my $verbose;
my $help;
my $man;

GetOptions ("d|data=s"       => \$data,      # string
            "r|remote=s"     => \$remote,  # string
            "o|output=s"     => \$output,    # string
            "c|collect-only" => \$collect,    # flag
            "record-file=s"       => \$recordfile,  # string
            "replay-file=s"       => \$replayfile,  # string
            "verbose"        => \$verbose,     # flag
            "h|help"         => \$help,     # flag
            "man"            => \$man,     # flag
    ) or pod2usage(2);

sub main() {
    pod2usage(-exitval => 0, -verbose => 1) if $help;
    pod2usage(-exitval => 0, -verbose => 2) if $man;

    if (defined($data) && (
            defined($remote)
            || defined($recordfile)
            || defined($replayfile)
        )) {
        die "E: --data cannot be used with --remote, --record, nor --replay\n";
    }

    if (defined($replayfile) && (
            defined($remote)
            || defined($recordfile)
        )) {
        die "E: --replay cannot be used with --remote, nor --record\n";
    }

    if (!$collect) {
	require StorageDisplay or
	    die "Cannot load the StorageDisplay module to handle collected data: $!\n";
    }

    my $infos;

    if ($replayfile) {
	require StorageDisplay::Collect::CMD::Replay or
	    die "Replay requested, but unable to load the StorageDisplay::Collect::CMD::Replay module: $!\n";
        my $dh;
        open($dh, "<", $replayfile)
            or die "Cannot open '$replayfile': $!" ;
        my $replay=join('', <$dh>);
        my $replaydata;
        close($dh);
        {
            my $VAR1;
            eval($replay); ## no critic (ProhibitStringyEval)
            #print STDERR "c: $content\n";
            $replaydata = $VAR1;
        }
        $infos = StorageDisplay::Collect->new(
            'Replay', 'replay-data' => $replaydata)->collect();
    }

    my $contents;
    my @recorder;
    if (defined($recordfile)) {
        @recorder = ('Proxy::Recorder', 'recorder-reader');
    }
    if (defined($data)) {
        my $dh;
        open($dh, "<", $data)
            or die "Cannot open '$data': $!" ;
        $contents=join('', <$dh>);
        close($dh);
    } elsif (defined($remote)) {
        $contents = collect_from_remote($remote, @recorder);
    } elsif (not defined($infos)) {
        $infos = StorageDisplay::Collect->new(@recorder, 'Local')->collect();
    }

    # data are in $contents (if got through Data::Dumper) or directly in $infos
    if (defined($contents)) {
        # moving data from $contents to $infos
        {
            my $VAR1;
            eval($contents); ## no critic (ProhibitStringyEval)
            #print STDERR "c: $content\n";
            $infos = $VAR1;
        }
    }

    if (defined($recordfile)) {
        if (! exists($infos->{'recorder'})) {
            print STDERR "W: skpping recording: no records!\n";
        } else {
            my $dh;
            open($dh, ">", $recordfile)
                or die "Cannot open '$data': $!";
            print $dh Dumper($infos->{'recorder'});
            close($dh);
        }
    }
    delete($infos->{'recorder'});

    my $oldout;
    if (defined($output)) {
	# dzil do not want Two-argument "open"
	# so, commented-out as we do not use it
	# if this change, a way to write this would have to be found
        # open(my $oldout, ">&STDOUT")     or die "Can't dup STDOUT: $!";
        open(STDOUT, '>', $output) or die "Can't redirect STDOUT to $output: $!";
    }

    if ($collect) {
        print Dumper($infos);
        return;
    }
    my $st=StorageDisplay->new('infos' => $infos);

    $st->createElems();
    $st->display;
}

main

__END__

=pod

=encoding UTF-8

=head1 NAME

storage2dot - analyse and generate a graphical view of a machine storage

=head1 VERSION

version 2.02

=head1 SYNOPSIS

B<storage2dot [OPTIONS]>

  Options:
    --remote|-r MACHINE   collect data on MACHINE (through SSH)
    --collect-only|-c     generate plain data instead of dot file
    --data|-d FILE        use FILE as data source
    --output|-o FILE      write output into FILE
    --record-file FILE    record shell commandsinto FILE [for tests]
    --replay-file FILE    collect data from FILE [for tests]
    --help|-h             brief documentation
    --man                 full documentation

This program can be used to collect data about the storage state from
local or remote machines (through SSH) and use them to generate a DOT
graphic representing them.

=head1 OPTIONS

=over 8

=item B<--remote MACHINE>

Collect storage data on MACHINE (through SSH). By default, local
storage data are collected (without SSH).

=item B<--collect-only>

By default, a DOT file is generated from the storage data. With this
option, the program do not create the DOT file but only output the
raw collected data for later analyze.

=item B<--data FILE>

In order to generate the DOT file, use the provided data. B<FILE> must
have been created with the help of the previous option. No new data
are collected when this option is used.

=item B<--output FILE>

Write generated data (DOT by default) into B<FILE> instead of the
standard output.

=item B<--record-file FILE>

Write shell commands (and their output) that are used to collect data
into B<FILE>. This is mainly used for reproducibility during tests.

=item B<--replay-file FILE>

Use information from B<FILE> instead of running real shell commands in
order to collect data. B<FILE> must be This is mainly used for
reproducibility during tests.

=item B<--help>

Print a brief help message and exits.

=item B<--man>

Prints the manual page and exits.

=back

=head1 EXAMPLES

=over 8

=item B<storage2dot -o state.dot>

Generate a DOT file representing the state of the storage on the local machine.

=item B<storage2dot -r host -o state.dot>

Generate a DOT file representing the state of the storage on the
remote B<host> machine. Only perl (and its standard modules) are
required on the remote machine. Of course, a SSH account is also
required.

=item B<storage2dot -c -o state.data>

Just collect data on current machine without generating a DOT file.
Only perl (and its standard modules) are required on the current
machine.

=item B<storage2dot --data state.data -o state.dot>

Generate a DOT file representing the state of the storage recorded in
the state.data file. Extra perl modules are required for this command.

=item B<dot -Tpdf state.dot >>B< state.pdf>

Generate a PDF from the DOT file using dot(1).

=back

=head1 AUTHOR

Vincent Danjean <Vincent.Danjean@ens-lyon.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2014-2023 by Vincent Danjean.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut