#!/usr/bin/perl -w
# timed copy program - copies from stdin to stdout, for given amount of time
# use for capturing video output, such as with the ivtv drivers
# ------
# timing is not exact - could be a second or so off.
# timing depends on precision of alarm() call, and if
# non-default very large buffers are specified, there may also be
# additonal few seconds delay, in pre-allocating buffer at startup
# ------
# Version 1.30: Oct 2006:
# By default, uses alarm(), but there is a bug Time::HiRes, which
# did not handle alarm() if total in microseconds > 2^32. To get around
# this, created the --clock option which does not use alarm, but instead
# keeps looking at wall clock elapsed time. Later this was eventually
# discovered to be a Time::HiRes issue, am now using the working
# alarm() from the default library, and not including Time::HiRes
# Time::HiRes Version 1.86 (tested Oct 2006) issue:
# 71.666 minutes == 4300 seconds becomes 4300*10^6 % 2^32 == 5.035 secs
# So, --clock should never be needed, now that Time::HiRes is not used.
# --clock keeps checking time after each read() call, and will exit when
# amount of time (seconds) is more than specified time.
# ------
# Version 1.35: Oct 2006:
# added code to continue to try to read on EBUSY: "Device or resource busy"
# error, which may occur in back-to-back scheduled commands.
# added printout of start and end times, using Date::Format on -v flag
#----------------------------------------------------------------------
# ivtv page had a simple script, this is a extension to that script:
# http://ivtvdriver.org/index.php/Example_script_to_schedule_recordings
# use the at or crontab entries to drive this, for example, to record a
# program for 30 minutes starting at 1:30AM on Sep 12, using at:
# at -v 1:30AM Sep 12, and enter the command:
# timed-copy -v -m 30 < /dev/video0 > 2006-09-12-0100.mpg
# Can run a script with at, to setup codec, tune to channel, as needed.
#----------------------------------------------------------------------

use warnings;
use strict;

my $version = "1.38";

my $kilo = 1024;
my $mega = $kilo * $kilo;
my $readSize = $kilo*512; # default buffer size for read/write
my $minutes = 30; # default time to run, if no argument provided
my $verbose = 0; # verbosity level - 0 no messages, 1 some, 2 more.
my $busySleep = 1; # number of seconds to sleep and try again if can't read from input device, usually needed on startup of back-to-back runs
my $useClock = 0; # if enabled: don't use alarm(), use wall clock to time script
my $timeTmpl = "%Y-%m-%d %T"; #  Date::Time time2str template for printing

my $usage = "Version $version, Usage:
$0: copies from stdin to stdout, for given amount of time
$0 [-v] [-b <blocksize in bytes>] [-m <number of minutes>] < input > output
  --verbose or -v:  be verbose, use twice to get debug messages also: -v -v 
  --buffer= or -b <blocksize>:  size in bytes of the read/write buffer (default: $readSize)
  --minutes= or -m <number of minutes>: run for this long (default: $minutes) 
  --clock or -c : don't use alarm, but use wall clock to time program (default: $useClock)";

#----------------------------------------------------------------------
use Getopt::Long;
# use Time::HiRes qw ( time alarm ); # Oct 2006: don't use has a problem in
# alarm(), it can't handle the time if it requires over 32-bit arithmetic
# when using microseconds, for example:
# 71.666 minutes == 4300 seconds; alarms in 4300*10^6 % 2^32 == 5.035 secs
# Time::HiRes Version 1.86 (tested Oct 2006) issue.

use Errno qw(EINTR EAGAIN EBUSY);
use Date::Format;

unless ( GetOptions (
		"verbose|v+"			=> \$verbose,
		"minutes|m=f"			=> \$minutes,
		"buffer|b=i"			=> \$readSize,
		"clock|c"			=> \$useClock,
    )) {
    die "*** Invalid arguments\n$usage\n";
}

die "*** Error: Invalid or no time specified ($minutes)\n$usage\n"
    unless $minutes > 0;
die "*** Error: Invalid buffer size ($readSize)\n$usage\n"
    unless $readSize > 0;

#----------------------------------------------------------------------
my $timerExpired = 0;

$SIG{'ALRM'} = sub { $timerExpired = 1; };

my $seconds = int($minutes * 60 + 0.999); # simple ceiling
print STDERR "...will read using $readSize bytes buffer, for $seconds seconds\n" if ($verbose > 1);
#----------------------------------------------------------------------
alarm($seconds) unless ($useClock);

binmode(STDIN, ':raw'); 
binmode(STDOUT, ':raw'); 

my ($readTotal, $readCount, $writeCount, $writeTotal) = (0,0,0,0);
my $buffer = 'a' x $readSize; # pre-allocate buffer
my $t1 = time(); # script start time
my $t2 = 0; # capture start time
my $busySleepTotal = 0;

print STDERR "Script Started At:   ", time2str($timeTmpl, $t1), "\n"
    if ($verbose > 1);

# first loop, wait until input device is ready - not busy
# this may happen in back-to-back runs of this script
while (1) {
    $t2 = time();
    $readCount = sysread(STDIN, $buffer, $readSize);
    if (!defined $readCount) {
        if ($! == EAGAIN || $! == EBUSY) {
            print STDERR " --warning: capture delayed: $!. Will sleep and try again periodically.\n"
                if ($verbose && $busySleepTotal <= 0); # print message once only
            sleep($busySleep);
            $busySleepTotal += $busySleep;
        }
    }
    last if (defined $readCount);

    if ($useClock) {
        my $t3 = time();
        last if (($t3 - $t1) > $seconds);
    } else {
        if ($timerExpired) {
            print STDERR " --debug: busy loop, timer expired.\n"
                if ($verbose > 1);
            last;
        }
    }
}

if ($verbose) {
    print STDERR " --warning: capture delayed by $busySleepTotal seconds, device was busy\n" if ($busySleepTotal > 0);
    print STDERR "Capture Started At:  ", time2str($timeTmpl, $t2), "\n";
}

# main capture loop
while (1) {
    if (!defined $readCount) {
        if ($! == EINTR) {
            print STDERR " --debug: sysread interrupted, exiting: $!\n"
                if ($verbose > 1);
        } else {
            print STDERR " **error: sysread failed: $!\n" 
        }
        last;
    }
    if ($readCount == 0) { # EOF
        print STDERR " --warning: sysread end-of-file reached\n"
            if ($verbose);
        last;
    }
    $readTotal += $readCount;

    $writeCount = syswrite(STDOUT, $buffer, $readCount);
    if (!defined $writeCount) {
        print STDERR " **error: syswrite failed: $!\n" unless ($! == EINTR);
        print STDERR " --debug: syswrite error-ed out, ok if interrupted: $!\n"
            if ($verbose > 1);
        last;
    }
    $writeTotal += $writeCount;
    if ($writeCount != $readCount) {
        print STDERR " --debug: syswrite wrote only $writeCount, needed to write $readCount bytes (last write, timer expired?)\n"
            if ($verbose > 1);
    }

    if ($useClock) {
        my $t3 = time();
        last if (($t3 - $t1) > $seconds);
    } else {
        if ($timerExpired) {
            print STDERR " --debug: timer expired.\n"
                if ($verbose > 1);
            last;
        }
    }

    $readCount = sysread(STDIN, $buffer, $readSize);
}
my $t3 = time();

#----------------------------------------------------------------------
print STDERR "Capture Finished At: ", time2str($timeTmpl, $t3), "\n" if ($verbose);
printf STDERR "...read %.0f, wrote %.0f bytes, ran for %d seconds (using %s)\n", $readTotal/1.0, $writeTotal/1.0, ($t3-$t1), $useClock?"wall clock timer":"alarm timer"
    if ($verbose > 1);

printf STDERR "...read %.2f, wrote %.2f MBytes, ran for %.2f minutes (using %s)\n", $readTotal/$mega, $writeTotal/$mega, ($t3-$t1)/60, $useClock?"wall clock timer":"alarm timer"
    if ($verbose);
#----------------------------------------------------------------------
