#!/usr/bin/env perl

package PerlACE::ProcessAndroid;

use POSIX "sys_wait_h";
use File::Basename;
use File::Spec;
use FileHandle;
use Cwd;
use Config;

use strict;

###############################################################################

###  Grab signal names

my @signame;

if (defined $Config{sig_name}) {
    my $i = 0;
    foreach my $name (split (' ', $Config{sig_name})) {
        $signame[$i] = $name;
        $i++;
    }
}
else {
    my $i;
    for ($i = 0; $i < 255; ++$i) {
        $signame[$i] = $i;
    }
}

###############################################################################

### Constructor and Destructor

sub new
{
    my $proto = shift;
    my $class = ref ($proto) || $proto;
    my $self = {};

    $self->{TARGET} = shift;
    $self->{RUNNING} = 0;
    $self->{IGNOREEXESUBDIR} = 1;
    $self->{IGNOREHOSTROOT} = 0;
    $self->{PROCESS} = undef;
    $self->{EXECUTABLE} = shift;

    # Only set argument when they are really supplied via the
    # CreateProcess call. If the weren't supplied, an error like
    # Process_Android::HASH (0x...) is generated.
    if (@_ == 1) {
        $self->{ARGUMENTS} = shift;
    }
    else {
        $self->{ARGUMENTS} = "";
    }
    $self->{FSROOT} = $ENV{'ANDROID_FS_ROOT'};

    bless ($self, $class);

    # copy the test executable to the target at forehand
    $self->copy_executable ();

    return $self;
}

sub DESTROY
{
    my $self = shift;

    if ($self->{RUNNING} == 1) {
        print STDERR "ERROR: <", $self->{EXECUTABLE},
                     "> still running upon object destruction\n";
        $self->Kill ();
    }

    if (defined $self->{SCRIPTFILE}) {
        unlink $self->{SCRIPTFILE};
    }
    if (defined $self->{UNLINKLIST}) {
        foreach my $ul (@{$self->{UNLINKLIST}}) {
            unlink $ul;
        }
    }
}

###############################################################################

sub Arguments
{
    my $self = shift;

    if (@_ != 0) {
        $self->{ARGUMENTS} = shift;
        if (defined $self->{SCRIPTFILE}) {
          if (!defined $self->{UNLINKLIST}) {
              $self->{UNLINKLIST} = ();
          }
          push(@{$self->{UNLINKLIST}}, $self->{SCRIPTFILE});
          $self->{SCRIPTFILE} = undef;
        }
    }

    return $self->{ARGUMENTS};
}

sub Executable
{
    my $self = shift;

    if (@_ != 0) {
        $self->{EXECUTABLE} = shift;
        if (defined $self->{SCRIPTFILE}) {
            if (!defined $self->{UNLINKLIST}) {
                $self->{UNLINKLIST} = ();
            }
            push(@{$self->{UNLINKLIST}}, $self->{SCRIPTFILE});
            $self->{SCRIPTFILE} = undef;
        }
        # copy the (new) test executable to the target
        # previously scanned .vxtest files and detected libraries
        # will not be scanned/copied twice
        $self->copy_executable ();
    }

    my $executable = $self->{EXECUTABLE};

    # If the target's config has a different ACE_ROOT, rebase the executable
    # from $ACE_ROOT to the target's root.
    if (defined $self->{TARGET} &&
          $self->{TARGET}->ACE_ROOT() ne $ENV{'ACE_ROOT'}) {
        $executable = PerlACE::rebase_path ($executable,
                                            $ENV{'ACE_ROOT'},
                                            $self->{TARGET}->ACE_ROOT());
    }

    if ($self->{IGNOREEXESUBDIR}) {
        return $executable;
    }

    my $basename = basename ($executable);
    my $dirname = dirname ($executable).'/';

    my $subdir = $PerlACE::Process::ExeSubDir;
    if (defined $self->{TARGET} && defined $self->{TARGET}->{EXE_SUBDIR}) {
        $subdir = $self->{TARGET}->{EXE_SUBDIR};
    }

    $executable = $dirname . $subdir . $basename;

    return $executable;
}

sub Wait ($)
{
    my $self = shift;
    my $timeout = shift;
    if (!defined $timeout || $timeout < 0) {
        waitpid ($self->{PROCESS}, 0);
    } else {
        return $self->TimedWait($timeout);
    }
}

# really only for internal use
sub check_return_value ($)
{
    my $self = shift;
    my $rc = shift;

    if ($rc < 0x80) {
        return $rc;
    }

    # Remember Core dump flag
    my $dump = 0;

    if ($rc & 0x80) {
        $rc &= ~0x80;
        $dump = 1;
    }

    # check for ABRT, KILL or TERM
    if ($rc == 6 || $rc == 9 || $rc == 15) {
        return 0;
    }

    print STDERR "ERROR: <", $self->{EXECUTABLE},
                 "> exited with ";

    print STDERR "coredump from " if ($dump == 1);

    print STDERR "signal $rc : ", $signame[$rc], "\n";

    return 255;
}

sub TimedWait ($)
{
    my $self = shift;
    my $timeout = shift;

    if ($PerlACE::Process::WAIT_DELAY_FACTOR > 0) {
        $timeout *= $PerlACE::Process::WAIT_DELAY_FACTOR;
    }

    if (defined $ENV{'ACE_TEST_VERBOSE'} && $self->{PROCESS} > 0) {
        print STDERR "Wait $timeout to finish executable $self->{PROCESS}. ";
        print STDERR "RUNNING: $self->{RUNNING}\n";
    }

    # Multiply with 10 because we wait a tenth of a second each time
    $timeout *= 10;

    while ($timeout-- != 0 && $self->{PROCESS} > 0 && $self->{RUNNING} == 1) {
        my $pid = waitpid ($self->{PROCESS}, WNOHANG);
        if ($pid != 0 && $? != -1) {
            if (defined $ENV{'ACE_TEST_VERBOSE'} && $self->{PROCESS} > 0) {
                print STDERR "Executable $self->{PROCESS} finished ($pid)\n";
            }
            last;
        }
        select(undef, undef, undef, 0.1);
    }

    # attempt to retrieve exitstatus from remote .rc file
    my $shell = $ENV{'ANDROID_SDK_ROOT'} . '/platform-tools/adb shell';
    my $rcfile = $self->{RCFILE};
    ## wait max 5 * $PerlACE::Process::WAIT_DELAY_FACTOR sec for RC file to appear
    my $start_tm = time ();
    my $max_wait = 5;
    if ($PerlACE::Process::WAIT_DELAY_FACTOR > 0) {
        $max_wait *= $PerlACE::Process::WAIT_DELAY_FACTOR;
    }
    my $rc = -255;
    while ((time() - $start_tm) < $max_wait) {
        select(undef, undef, undef, 0.2);
        $rc = int(`$shell 'if [ -e $rcfile -a -s $rcfile ] ; then cat $rcfile; rm -f $rcfile >/dev/null 2>&1; else echo -255; fi'`);
        if ($rc != -255) {
            return $self->check_return_value ($rc);
        }
    }

    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        print STDERR "Exit TimedWait with Process: $self->{PROCESS}, Running: $self->{RUNNING}\n";
    }

    return -1;
}

sub Kill ()
{
    my $self = shift;

    if ($self->{RUNNING} == 1) {
        if (defined $ENV{'ACE_TEST_VERBOSE'}) {
            print STDERR "Killing process <$self->{PROCESS}>\n";
        }
        # killing the adb process, not the actual test executable.
        kill (1, $self->{PROCESS});

        my $pid = waitpid ($self->{PROCESS}, WNOHANG);
        if ($pid == -1) {
            if (defined $ENV{'ACE_TEST_VERBOSE'}) {
                print STDERR "Process <$self->{PROCESS}> already ended\n";
            }
        }
        elsif ($pid == $self->{PROCESS}) {
            if (defined $ENV{'ACE_TEST_VERBOSE'}) {
                print STDERR "Process <$self->{PROCESS}> ended\n";
            }
        }
    }

    $self->{RUNNING} = 0;
}

sub WaitKill ($)
{
    my $self = shift;
    my $timeout = shift;

    if ($self->{RUNNING} == 0) {
        return 0;
    }

    my $status = $self->TimedWait ($timeout);

    if ($status == -1) {
        print STDERR "ERROR: $self->{EXECUTABLE} timedout\n";
        $self->Kill ();
    }

    $self->{RUNNING} = 0;

    return $status;
}


# Do a Spawn and immediately WaitKill
#
sub SpawnWaitKill ($)
{
    my $self = shift;
    my $timeout = shift;

    if ($self->Spawn () == -1) {
        return -1;
    }
    my $result = 0;

    if ($self->{RUNNING} == 1) {
        $result = $self->WaitKill ($timeout);
    }

    return $result;
}

sub TerminateWaitKill ($)
{
    my $self = shift;
    my $timeout = shift;

    if ($self->{RUNNING}) {
        print STDERR "INFO: $self->{EXECUTABLE} being killed.\n";

        my $killcmd = $ENV{'ANDROID_SDK_ROOT'} . '/platform-tools/adb shell "kill -s TERM ' . $self->{REMOTE_PID} . '"';
        system ($killcmd);
    }

    return $self->WaitKill ($timeout);
}

sub IgnoreExeSubDir
{
    my $self = shift;

    if (@_ != 0) {
        $self->{IGNOREEXESUBDIR} = shift;
    }

    return $self->{IGNOREEXESUBDIR};
}

sub IgnoreHostRoot
{
    my $self = shift;

    if (@_ != 0) {
        $self->{IGNOREHOSTROOT} = shift;
    }

    return $self->{IGNOREHOSTROOT};
}

sub Spawn ()
{
    my $self = shift;

    if ($self->{RUNNING} == 1) {
        print STDERR "ERROR: Cannot Spawn: <", $self->Executable (),
                     "> already running\n";
        return -1;
    }

    if (!defined $self->{EXECUTABLE}) {
        print STDERR "ERROR: Cannot Spawn: No executable specified\n";
        return -1;
    }

    if ($self->{IGNOREEXESUBDIR} == 0) {
        if (!-f $self->Executable ()) {
            print STDERR "ERROR: Cannot Spawn: <", $self->Executable (),
                         "> not found\n";
            return -1;
        }
    }
    my $status = 0;

    my $fsroot_target = $ENV{'ANDROID_FS_ROOT'};
    my $exe = $self->Executable ();
    my $program = "$fsroot_target/".basename($exe);

    my($test, $dir, $suffix) = fileparse($program);

    my $local_xdir = cwd ();

    if (!defined $self->{PIDFILE}) {
        $self->{PIDFILE} = "$fsroot_target/ace-".rand(time).".pid";
    }
    if (!defined $self->{RCFILE}) {
        $self->{RCFILE} = "$fsroot_target/ace-".rand(time).".rc";
    }
    if (!defined $self->{SCRIPTFILE}) {
        $self->{SCRIPTFILE} = "$local_xdir/run-".rand(time).".sh";
    }
    ## create scriptfile
    my $run_script =
        # "if [ ! -e /tmp/.acerun ]; then mkdir /tmp/.acerun; fi\n".
        "cd $fsroot_target\n".
        "export LD_LIBRARY_PATH=$fsroot_target/lib:.:\$LD_LIBRARY_PATH\n".
        "export PATH=\$PATH:$fsroot_target/lib:.\n".
        "export ACE_ROOT=$fsroot_target\n";

    if (defined $self->{TARGET} && defined $self->{TARGET}->{EXTRA_ENV}) {
      my $x_env_ref  = $self->{TARGET}->{EXTRA_ENV};
      while ( my ($env_key, $env_value) = each(%$x_env_ref) ) {
          $run_script .=
          "export $env_key=\"$env_value\"\n";
      }
    }
    $run_script .=
      "./$test $self->{ARGUMENTS} &\n";
    $run_script .=
      "MY_PID=\$!\n".
      "echo \$MY_PID > ".$self->{PIDFILE}."\n";
    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        $run_script .=
          "echo INFO: Process started remote with pid [\$MY_PID]\n";
    }
    $run_script .=
      "wait \$MY_PID\n";
    $run_script .=
      "MY_RC=\$?\n".
      "echo \$MY_RC > ".$self->{RCFILE}."\n";
    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        $run_script .=
          "echo INFO: Process [\$MY_PID] returned exit code [\$MY_RC]\n";
    }
    unless (open (RUN_SCRIPT, ">".$self->{SCRIPTFILE})) {
        print STDERR "ERROR: Cannot Spawn: <", $self->Executable (),
                      "> failed to create ",$self->{SCRIPTFILE},"\n";
        return -1;
    }
    print RUN_SCRIPT $run_script;
    close RUN_SCRIPT;

    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        print STDERR "INFO: created run script [",$self->{SCRIPTFILE},"]\n", $run_script;
    }
    $self->PutFile ($self->{SCRIPTFILE}, "$fsroot_target/".basename($self->{SCRIPTFILE}));

    my $adb_process = $ENV{'ANDROID_SDK_ROOT'} . "/platform-tools/adb";
    my $cmd = $adb_process . ' shell "cd ' . $fsroot_target . ' && source ./' . basename($self->{SCRIPTFILE}) . '"';

    FORK: {
        if ($self->{PROCESS} = fork) {
            bless $self;
        }
        elsif (defined $self->{PROCESS}) {
            $self->{RUNNING} = 1;

            if (defined $ENV{'ACE_TEST_VERBOSE'}) {
                print STDERR "Start to execute: $cmd\n";
            }
            exec ( $cmd );
            exit;
        }
        elsif ($! =~ /No more process/) {
            sleep 5;
            redo FORK;
        }
        else {
            print STDERR "ERROR: Can't fork <" . $cmd . ">: $!\n";
       }
    }

    my $shell = $adb_process . ' shell';
    my $pidfile = $self->{PIDFILE};
    ## wait max 10 * $PerlACE::Process::WAIT_DELAY_FACTOR sec for pid file to appear
    my $start_tm = time ();
    my $max_wait = 10;
    if ($PerlACE::Process::WAIT_DELAY_FACTOR > 0) {
        $max_wait *= $PerlACE::Process::WAIT_DELAY_FACTOR;
    }
    my $rc = 1;
    while ((time() - $start_tm) < $max_wait) {
        select(undef, undef, undef, 0.2);
        $rc = int(`$shell 'if [ -e $pidfile -a -s $pidfile ] ; then cat $pidfile; rm -f $pidfile >/dev/null 2>&1; else echo 0; fi'`);
        if ($rc != 0) {
            $self->{REMOTE_PID} = $rc;
            last;
        }
    }
    if (!defined $self->{REMOTE_PID}) {
        print STDERR "ERROR: Remote command failed <" . $test . ' ' . $self->{ARGUMENTS} . ">: $! No PID found.\n";
        return -1;
    }

    $self->{RUNNING} = 1;

    return 0;
}

sub copy_executable ($)
{
    my $self = shift;

    my $fsroot_target = $ENV{'ANDROID_FS_ROOT'};
    my $program = $self->Executable ();
    # never copy program subdirectory if specified (like '../Generic_Servant/server')
    my $exe = "$fsroot_target/".basename($program);

    $self->{SHLIBS} = ();
    $self->{VXTESTS} = ();
    $self->{XLIBPATH} = ();

    push (@{$self->{XLIBPATH}}, '.');
    if (defined $self->{TARGET} && defined $self->{TARGET}->{LIBPATH}) {
        foreach my $libpath (split(/:|;/, $self->{TARGET}->{LIBPATH})) {
          push(@{$self->{XLIBPATH}}, $libpath);
        }
    }

    $self->PutFile ("$program", $exe);

    if ($PerlACE::Static == 0) {
        # collect libraries from .vxtest file
        $self->process_vxtest ($program.'.vxtest');
        # collect libraries from extra lib paths (might be dynamically loaded)
        $self->collect_extra_libs ();
        # collect any runtime lib dependencies specified
        $self->collect_runtime_libs ();
        # copy all collected libraries
        foreach my $lib (@{$self->{SHLIBS}}) {
            $self->PutFile ($lib, "$self->{FSROOT}/lib/".basename($lib));
        }
        # handle defined system libraries
        if (defined $self->{TARGET} && defined $self->{TARGET}->{SYSTEM_LIBS}) {
            $self->copy_system_libs ($self->{TARGET}->{SYSTEM_LIBS});
        }
    }
}

sub process_vxtest ($)
{
    my $self = shift;
    my $newvxtest = shift;

    foreach my $vxtest (@{$self->{VXTESTS}}) {
        if ($vxtest eq $newvxtest) {
            return;
        }
    }
    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        print STDERR "Processing vxtest file $newvxtest\n";
    }
    $self->collect_vxtest_libs ($newvxtest);
}

sub add_unique_lib ($)
{
    my $self = shift;
    my $newlib = shift;

    foreach my $lib (@{$self->{SHLIBS}}) {
        if ($lib eq $newlib) {
            return 0;
        }
    }
    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        print STDERR "Collecting library $newlib\n";
    }
    push(@{$self->{SHLIBS}}, $newlib);
    return 1;
}

sub collect_vxtest_libs ()
{
    my $self = shift;
    my $vxtestfile = shift;
    my $fh = new FileHandle;

    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        print STDERR "Analyzing vxtest file <$vxtestfile>\n";
    }

    if (open ($fh, $vxtestfile)) {
        my $line1 = <$fh>;
        chomp $line1;
        if (defined $ENV{'ACE_TEST_VERBOSE'}) {
            print STDERR "Analyzing vxtest file: Found line $line1\n";
        }
        while(<$fh>) {
            $line1 = $_;
            chomp $line1;
            if (defined $ENV{'ACE_TEST_VERBOSE'}) {
                print STDERR "Analyzing vxtest file: Found line $line1\n";
            }

            $self->collect_vxtest_lib ($line1);
        }
    }
    close $fh;
}

sub collect_vxtest_lib ($)
{
    my $self = shift;
    my $name = shift;
    my $query;
    my @libpaths = ("$ENV{'ACE_ROOT'}/lib");

    push (@libpaths, @{$self->{XLIBPATH}});

    foreach my $libpath (@libpaths) {
      $query = "$libpath/lib$name.so";
      if (-e $query) {
          if (defined $ENV{'ACE_TEST_VERBOSE'}) {
              print STDERR "Found $name in library directory $libpath\n";
          }
          # look for versioned and non-versioned filenames
          my @files = glob ($query . '*');
          foreach my $file (@files) {
              $self->add_unique_lib ($file);
          }
      }
    }
}

sub collect_extra_libs ()
{
    my $self = shift;
    # treat current dir as extra libpath
    my @libpaths = (@{$self->{XLIBPATH}});
    my $query;
    my $vxtest;

    if (defined $ENV{'ACE_TEST_VERBOSE'}) {
        print STDERR "Inspecting libpaths (@libpaths) for extra libraries\n";
    }

    foreach my $libpath (@libpaths) {
      # look for versioned and non-versioned libraries
      $query = "$libpath/lib*.so*";
      my @files = glob ($query);
      foreach my $file (@files) {
          if ($self->add_unique_lib ($file) == 1) {
              # check for possible .vxtest for new lib dependency
              $vxtest = basename ($file);
              $vxtest =~ s/^lib(.*)[\.]so([\.].*)?$/\1/;
              if (-e "$libpath/$vxtest.vxtest") {
                  # process .vxtest file if not yet processed before
                  $self->process_vxtest ("$libpath/$vxtest.vxtest");
              }
          }
      }
    }
}

sub collect_runtime_libs ()
{
    my $self = shift;
    # only need to test ACE_ROOT/lib since all libs from '.' and extra libpaths are copied already
    my @libpaths = ("$ENV{'ACE_ROOT'}/lib");
    my $query;

    if (defined $self->{TARGET} && defined $self->{TARGET}->{RUNTIME_LIBDEP}) {

        foreach my $runtimelib (@{$self->{TARGET}->{RUNTIME_LIBDEP}}) {

            if (-e $runtimelib) {
                $self->add_unique_lib ($runtimelib);
            } else {

                if (defined $ENV{'ACE_TEST_VERBOSE'}) {
                    print STDERR "Inspecting libpaths (@libpaths) for runtime libe $runtimelib\n";
                }

                foreach my $libpath (@libpaths) {
                    # look for versioned and non-versioned libraries
                    $query = "$libpath/lib$runtimelib.so*";
                    my @files = glob ($query);
                    foreach my $file (@files) {
                        $self->add_unique_lib ($file);
                    }
                }
            }
        }
    }
}

sub copy_system_libs ()
{
    my $self = shift;
    my $syslibs = shift;
    my @liblist = split (',', $syslibs);
    foreach my $lib (@liblist) {
        if (-e $lib) {
            if (defined $ENV{'ACE_TEST_VERBOSE'}) {
                print STDERR "Found system library $lib\n";
            }
            $self->PutFile ($lib, "$self->{FSROOT}/lib/" . basename ($lib));
        } else {
            print STDERR "Cannot find system library $lib!\n";
        }
    }
}

sub PutFile ($)
{
    my $self = shift;
    my $src = shift;
    my $dest = shift;

    if (defined $self->{TARGET}) {
        return $self->{TARGET}->PutLib($src, $dest);
    } else {
        my $silent;

        if (!defined $ENV{'ACE_TEST_VERBOSE'}) {
          $silent = "2> /dev/null"
        }

        my $adb_process = $ENV{'ANDROID_SDK_ROOT'} . "/platform-tools/adb";

        my $cmd = "$adb_process" . ' push '. "\"$src\" \"$dest\" $silent";

        if (defined $ENV{'ACE_TEST_VERBOSE'}) {
          print STDERR "PutFile cmd: $cmd\n";
        }

        system ( $cmd );
        if ($? != 0) {
            return -1;
        }
        return 0;
    }
}

1;
