#!/usr/bin/perl -w
# Copyright 2009-2013 Bernhard M. Wiedemann
# Copyright 2012-2020 SUSE LLC
# SPDX-License-Identifier: GPL-2.0-or-later
#

=head1 SYNOPSIS

isotovideo [OPTIONS] [TEST PARAMETER]

Parses command line parameters, vars.json and tests the given assets/ISOs.

=head1 OPTIONS

=over 4

=item B<-d, --debug>

Enable direct output to STDERR instead of autoinst-log.txt

=item B<--workdir=>

isotovideo will chdir to that directory on startup

=item B<--color=[yes|no]>

Enable or disable color output explicitly. Defaults to "yes". Alternatively
ANSI_COLORS_DISABLED or NO_COLOR can be set to any value to disable colors.

=item B<-v, --version>

Show the current program version and test API version

=item B<-h, -?, --help>

Show this help.

=head1 TEST PARAMETER

All additional command line arguments specified in the C<key=value> format are
parsed as test parameters which take precedence over the settings in the
vars.json file. Lower case key names are transformed into upper case
automatically for convenience.

=cut

use Mojo::Base -strict, -signatures;
use autodie ':all';
no autodie 'kill';

# Avoid "Subroutine JSON::PP::Boolean::(0+ redefined" warnings
# Details: https://progress.opensuse.org/issues/90371
use JSON::PP;

my $installprefix;    # $bmwqemu::scriptdir
my $fatal_error;    # the last error message caught by the die handler

BEGIN {
    # the following line is modified during make install
    $installprefix = '/usr/lib/os-autoinst';

    my ($wd) = $0 =~ m-(.*)/-;
    $wd ||= '.';
    $installprefix ||= $wd;
    unshift @INC, "$installprefix";
}

use log qw(diag);
use needle;
use commands;
use distribution;
use testapi ();
use Getopt::Long;
require IPC::System::Simple;
use POSIX qw(:sys_wait_h _exit);
use Try::Tiny;
use Mojo::File qw(curfile);
use Mojo::UserAgent;
use Mojo::IOLoop::ReadWriteProcess 'process';
use Mojo::IOLoop::ReadWriteProcess::Session 'session';
Getopt::Long::Configure("no_ignore_case");
use OpenQA::Isotovideo::CommandHandler;
use OpenQA::Isotovideo::Interface;
use OpenQA::Isotovideo::Utils qw(git_rev_parse checkout_git_repo_and_branch
  spawn_debuggers
  checkout_wheels
  checkout_git_refspec handle_generated_assets load_test_schedule);

session->enable;
session->enable_subreaper;

my %options;

sub usage ($r) {
    eval { require Pod::Usage; Pod::Usage::pod2usage($r) };
    die "cannot display help, install perl(Pod::Usage)\n" if $@;
}

sub _get_version_string () {
    my $thisversion = '4.6';
    return "Current version is $thisversion [interface v$OpenQA::Isotovideo::Interface::version]";
}

sub version () {
    print _get_version_string() . "\n";
    exit 0;
}

GetOptions(\%options, 'debug|d', 'workdir=s', 'color=s', 'help|h|?', 'version|v') or usage(1);
usage(0) if $options{help};
version() if $options{version};

my $color = $options{color} // 'yes';
# User setting has preference, see https://no-color.org/
delete $ENV{NO_COLOR} if $color eq 'yes';
# Term::ANSIColor honors this variable
$ENV{ANSI_COLORS_DISABLED} = 1 if $color eq 'no';

chdir $options{workdir} if $options{workdir};

# global exit status
my $return_code = 1;

# record the last die message
# note: It might *not* be a fatal error so we don't call bmwqemu::serialize_state here
#       immediately but only in the END block.
$SIG{__DIE__} = sub ($e) { $fatal_error = $e };

diag(_get_version_string());

# enable debug default when started from a tty
$log::direct_output = $options{debug};

select(STDERR);
$| = 1;
select(STDOUT);    # default
$| = 1;

$bmwqemu::scriptdir = $installprefix;
bmwqemu::init();
for my $arg (@ARGV) {
    if ($arg =~ /^([[:alnum:]_\[\]\.]+)=(.+)/) {
        my $key = uc $1;
        $bmwqemu::vars{$key} = $2;
        diag("Setting forced test parameter $key -> $2");
    }
}

my $cmd_srv_process;
my $command_handler;
my $cmd_srv_fd;
my $cmd_srv_port;

# note: The subsequently defined stop_* functions are used to tear down the process tree.
#       However, the worker also ensures that all processes are being terminated (and
#       eventually killed).

sub stop_commands ($reason) {
    return unless defined $cmd_srv_process;
    return unless $cmd_srv_process->is_running;

    my $pid = $cmd_srv_process->pid;
    diag("stopping command server $pid because $reason");

    if ($cmd_srv_port && $reason && $reason eq 'test execution ended') {
        my $job_token = $bmwqemu::vars{JOBTOKEN};
        my $url = "http://127.0.0.1:$cmd_srv_port/$job_token/broadcast";
        diag('isotovideo: informing websocket clients before stopping command server: ' . $url);

        # note: If the job is stopped by the worker because it has been
        # aborted, the worker will send this command on its own to the command
        # server and also stop the command server. So this is only done in the
        # case the test execution just ends.

        my $timeout = 15;
        # The command server might have already been stopped by the worker
        # after the user has aborted the job or the job timeout has been
        # exceeded so no checks for failure done.
        Mojo::UserAgent->new(request_timeout => $timeout)->post($url, json => {stopping_test_execution => $reason});
    }

    $cmd_srv_process->stop();
    $cmd_srv_process = undef;
    diag('done with command server');
}

# make sure all commands coming from the backend will not be in the
# developers's locale - but a defined english one. This is SUSE's
# default locale
$ENV{LC_ALL} = 'en_US.UTF-8';
$ENV{LANG} = 'en_US.UTF-8';

checkout_git_repo_and_branch('CASEDIR');

# Try to load the main.pm from one of the following in this order:
#  - product dir
#  - casedir
#
# This allows further structuring the test distribution collections with
# multiple distributions or flavors in one repository.
$bmwqemu::vars{PRODUCTDIR} ||= $bmwqemu::vars{CASEDIR};

# checkout Git repo NEEDLES_DIR refers to (if it is a URL) and re-assign NEEDLES_DIR to contain the checkout path
checkout_git_repo_and_branch('NEEDLES_DIR');

bmwqemu::ensure_valid_vars();

# as we are about to load the test modules checkout the specified git refspec,
# if specified, or simply store the git hash that has been used. If it is not a
# git repo fail silently, i.e. store an empty variable

$bmwqemu::vars{TEST_GIT_HASH} = checkout_git_refspec($bmwqemu::vars{CASEDIR} => 'TEST_GIT_REFSPEC');

# set a default distribution if the tests don't have one
$testapi::distri = distribution->new;

$bmwqemu::vars{WHEELS_DIR} ||= $bmwqemu::vars{CASEDIR};
checkout_wheels($bmwqemu::vars{WHEELS_DIR});
load_test_schedule;

# start the command fork before we get into the backend, the command child
# is not supposed to talk to the backend directly
($cmd_srv_process, $cmd_srv_fd) = commands::start_server($cmd_srv_port = $bmwqemu::vars{QEMUPORT} + 1);

testapi::init();
needle::init();
bmwqemu::save_vars();

spawn_debuggers;

# stop main loop as soon as one of the child processes terminates
my $stop_loop = sub (@) { $command_handler->loop(0) if $command_handler->loop; };
$cmd_srv_process->once(collected => $stop_loop);

$command_handler = OpenQA::Isotovideo::CommandHandler->new(
    cmd_srv_fd => $cmd_srv_fd,
)->init;
$command_handler->on(signal => sub ($event, $sig) {
        $command_handler->backend->stop if defined $command_handler->backend;    # uncoverable statement
        stop_commands("received signal $sig");    # uncoverable statement
        _exit(1);    # uncoverable statement
});
$command_handler->setup_signal_handler;

$return_code = 0;

# enter the main loop: process messages from autotest, command server and backend
$command_handler->run;

# terminate/kill the command server and let it inform its websocket clients before
stop_commands('test execution ended');

if ($command_handler->test_fd) {
    $return_code = 1;    # unusual shutdown
    CORE::close($command_handler->test_fd);    # uncoverable statement
    $command_handler->stop_autotest;    # uncoverable statement
}

diag 'isotovideo ' . ($return_code ? 'failed' : 'done');

my $clean_shutdown;
if (!$return_code) {
    eval {
        $clean_shutdown = $bmwqemu::backend->_send_json({cmd => 'is_shutdown'});
        diag('backend shutdown state: ' . ($clean_shutdown // '?'));
    };

    # don't rely on the backend in a sane state if we failed - just stop it later
    eval { bmwqemu::stop_vm(); };
    if ($@) {
        bmwqemu::serialize_state(component => 'backend', msg => "unable to stop VM: $@", error => 1);
        $return_code = 1;
    }
}

# read calculated variables from backend and tests
bmwqemu::load_vars();

$return_code = handle_generated_assets($command_handler, $clean_shutdown) unless $return_code;

# clear any previously recorded die message; it was not fatal after all if the execution came this far
$fatal_error = undef;

END {
    $command_handler->backend->stop if $command_handler and $command_handler->backend;
    stop_commands('test execution ended through exception');
    $command_handler->stop_autotest if defined $command_handler;

    # in case of early exit, e.g. help display
    $return_code //= 0;

    bmwqemu::serialize_state(component => 'isotovideo', msg => $fatal_error) if $fatal_error;
    print "$$: EXIT $return_code\n";
    $? = $return_code;
}
