
| Current Path : /var/www/web-klick.de/dsh/AMTC-RMS-Batch/1.2/bin/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/web-klick.de/dsh/AMTC-RMS-Batch/1.2/bin/dexec.pl |
#!/sw/common-os/oss/perl/5.20.3-4.0.0/bin/perl
# $URL: https://svn.photomask.com/DR_FE/RMS-Batch/branches/AMTC-RMS-Batch-1.2/bin/dexec.pl $
# $Id: dexec.pl 56 2021-06-01 14:09:11Z heydero $
# Purpose: Execute one or more commands on the LSF cluster.
# POD and change history are at the end of this script.
package dexec;
use strict;
use warnings;
our $VERSION; # is set in bin/inc.pl
BEGIN
{
use FindBin;
my $dir = $ENV{UNITTEST} ? './bin' : $FindBin::RealBin;
my $file = "$dir/inc.pl";
do $file || die "Couldn't run $file : $@ : $!";
}
use Carp;
use Data::Dumper;
use File::Temp;
use Getopt::Long qw( :config no_ignore_case gnu_getopt pass_through );
use Env qw( LSB_JOBID LSB_JOBINDEX );
use Pod::Usage;
use AMTC::FileUtils;
use AMTC::LSF;
# pseudo constants
my $JOB_NAME_PREFIX = __PACKAGE__;
my $EXEC_ERROR = "$JOB_NAME_PREFIX: command execution failed:";
my $BEGIN_OF_OUTPUT = "$JOB_NAME_PREFIX:<BEGIN_OF_OUTPUT>";
my $END_OF_OUTPUT = "$JOB_NAME_PREFIX:<END_OF_OUTPUT>";
my ( $Lsf,
$Opt_help,
$Opt_man,
$Debug,
$Input_File,
$Job_Name,
$Job_Count,
$Job_Id,
$Terminated_Jobs,
$Priority,
$Queue,
$Res_Req,
@Cmd_Args );
# This block makes the script loadable as a module (aka modulino)
if ( ! caller )
{
$LSB_JOBID && $LSB_JOBINDEX ? _do_job() : main();
exit 0;
}
#-----------------------------------------------------------------------------
sub main
{
my @cmdl = ( $0, @ARGV );
# TODO: Replace GetOptions by AppConfig for AMTC consistency
GetOptions(
'help|h|?' => \$Opt_help,
'man' => \$Opt_man,
'debug' => \$Debug,
'jobname=s' => \$Job_Name,
'input-file=s' => \$Input_File,
'priority=i' => \$Priority,
'queue=s' => \$Queue,
'res-req=s' => \$Res_Req,
);
pod2usage( -exitstatus => 0, -msg => "Version: $VERSION\n" )
if $Opt_help;
if ( $Opt_man )
{
$ENV{PAGER} = '/usr/bin/less -R -f';
pod2usage( -exitstatus => 0, -verbose => 2 );
}
if ( $Debug )
{
print "cmdl=[@cmdl]\n";
print "ARGV=[@ARGV]\n"
}
@Cmd_Args = @ARGV;
create_job_input_file();
determine_job_count();
$Lsf = AMTC::LSF->new();
submit_job();
wait_for_termination();
print_joblogs_and_cleanup();
if ( $Debug )
{
my $msg = $Job_Count;
$msg .= $Job_Count > 1 ? " jobs" : " job";
$msg .= " successfully executed\n";
print $msg;
}
}
# Create a job input file and assign its path to $Input_File.
# In case the program option --input-file was used, then this file is used
# as is.
sub create_job_input_file
{
my $tmp = File::Temp->new(
TEMPLATE => $JOB_NAME_PREFIX . '.input.XXXXX',
DIR => '.',
UNLINK => 0,
);
my @cmds;
if ( defined $Input_File )
{
@cmds = AMTC::FileUtils::read_file( $Input_File );
}
else
{
@cmds = @Cmd_Args ? @Cmd_Args : <STDIN>;
}
@cmds = grep { ! /^\s*#/ } @cmds; # remove comments
@cmds = grep { ! /^\s*$/ } @cmds; # remove empty lines
# With double quotes a space is printed between the list items.
# The new line chars are also printed. This gives us a line per command.
print $tmp "@cmds";
$tmp->close || die "Couldn't close $Input_File: $!";
$Input_File = $tmp->filename;
if ( $Debug )
{
print "job_input_file=$Input_File\n";
print "--- begin of file ---\n";
system 'cat', $Input_File;
print "--- end of file ---\n";
}
}
sub determine_job_count
{
my @lines = AMTC::FileUtils::read_file( $Input_File );
$Job_Count = @lines;
print "job_count=$Job_Count\n" if $Debug;
}
sub submit_job
{
my $job_name = $Job_Name || $JOB_NAME_PREFIX;
$job_name .= "[1-$Job_Count]";
my $job_log = $JOB_NAME_PREFIX . '.%J.%I.log';
my $priority;
{
my $p = $Priority || 1000;
$p = 1 if $p < 1;
$p = 2000 if $p > 2000;
$priority = $p;
}
my %options = (
-J => $job_name,
-i => $Input_File,
-command => $0,
-oo => $job_log,
-sp => $priority,
);
$options{'-q'} = $Queue if defined $Queue;
$options{'-R'} = $Res_Req if defined $Res_Req;
print "submit_options=", Dumper( \%options ) if $Debug;
eval
{
$Job_Id = $Lsf->submit_job( %options );
};
die "LSF job submission failed: $@" if $@;
print "job_id=$Job_Id\n" if $Debug;
}
sub wait_for_termination
{
print "wait for terminated jobs...\n" if $Debug;
my @array_indices;
for ( my $i = 1; $i <= $Job_Count; $i++ )
{
push @array_indices, $i;
}
$Terminated_Jobs =
$Lsf->wait_array_terminated( $Job_Id, \@array_indices );
}
sub print_joblogs_and_cleanup
{
my $error;
my @error_log_files;
foreach my $index ( keys %$Terminated_Jobs )
{
my $job = $Terminated_Jobs->{$index};
my $job_status = $job->runtime_info->job_status;
my $exit_status = $job->runtime_info->exit_status;
die "$JOB_NAME_PREFIX: internal error: job has not terminated yet"
unless $job->is_terminated;
my $job_log_file = job_log_file( $job );
if ( $job->final_status & &FAILED )
{
push @error_log_files, $job_log_file;
}
else
{
print_joblog( $job_log_file );
unlink $job_log_file;
}
}
unlink $Input_File unless $Debug;
return unless @error_log_files;
my $msg = "$JOB_NAME_PREFIX: one or more command executions failed"
." [@error_log_files]\n"
." Check the job specific log files of the format"
." '$JOB_NAME_PREFIX.%J.%I.log'";
die $msg;
}
sub print_joblog
{
my ( $job_log_file ) = @_;
my @content = AMTC::FileUtils::read_file( $job_log_file );
my $in_output; # in the program's output
foreach ( @content )
{
if ( $Debug )
{
print;
next;
}
elsif ( /^$BEGIN_OF_OUTPUT/ )
{
$in_output = 1;
next;
}
elsif ( /^$END_OF_OUTPUT/ )
{
last;
}
else
{
print if $in_output;
}
}
}
# Return the path of the log file of the given AMTC::LSF::Job object.
sub job_log_file
{
my ( $job ) = @_;
my $path = $job->submit_info->output_file; # e.g. "dzip.%J.%I.log"
my $job_id = $job->runtime_info->job_id;
my $array_index = $job->runtime_info->array_index;
$path =~ s/%J/$job_id/;
$path =~ s/%I/$array_index/;
return $path;
}
sub _do_job
{
my $cmdline = _get_cmdline_from_stdin();
my $script = "set -o pipefail; " . $cmdline;
print "$JOB_NAME_PREFIX: $script\n";
print "$BEGIN_OF_OUTPUT\n";
my $failure = system $script;
print "$END_OF_OUTPUT\n";
die "$EXEC_ERROR $!" if $failure;
}
sub _get_cmdline_from_stdin
{
my @lines = <STDIN>;
chomp @lines;
unshift @lines, "index-0-dummy"; # because LSB_JOBINDEX is 1 or higher
$lines[$LSB_JOBINDEX];
}
__END__
=pod
=head1 NAME
dexec.pl - Execute one or more commands on the LSF cluster
=head1 USAGE
dexec.pl [--help] [--man] [--debug]
[--jobname name]
[--input-file path]
[--priority number]
[--queue string]
[--res-req string]
[arguments]
=head1 DESCRIPTION
This program submits the given system commands to LSF, waits for their
termination and evaluates the exit codes of the commands. The exit code of
this program is 0 if and only if all submitted commands returned the exit
code 0.
All commands will be executed using the Bash shell.
Everything what the jobs printed to STDOUT while running on the cluster
is printed as is to STDOUT after all jobs terminated. The order of output
is the same as the order of input.
Depending on the usage mode the commands are run sequentially, or in
parallel, or in a combination of that.
All arguments which are left after parsing the command line are submitted as
is to LSF as a single job, e.g.
$ dexec.pl cp foo bar
would run a simple copy job. It is possible to sumbit shell scripts. In that
case the script needs to be enclosed by single or double quotes, e.g.
$ dexec.pl "cd $HOME && ls -ld * > content-`date '+%Y%m%dT%H%M%S'`.list"
If the option I<--input-file> is set, then the arguments are read from the
given file and any arguments of the commandline are ignored. Such an
I<input file> can have many lines. Each line is interpreted as a command to
be run separately using LSF. By means of this technique an arbitrary number
of commands or scripts can be run in parallel.
Lines which begin with the # character are regarded as comments and will be
ignored. Also empty lines are ignored, i.e. the user can make the input file
more human-readable by using comments and space for structuring.
If neither command line arguments or the I<--input-file> option are given,
then the arguments are read from STDIN as if they were passed via the
I<--input-file> option.
=head1 REQUIRED ARGUMENTS
There are no required arguments. If no arguments are given, then they are read
from STDIN unless the option I<--input-file> is set.
=head1 OPTIONS
=head2 --input-file path
Read the commands to be executed from the given file. Empty lines, lines
having only space and lines beginning with the # character are ignored.
=head2 --jobname name
A name to be used when submitting the jobs. Default is "dexec". Note that
the full LSF jobname has an index range, e.g. if the job array consists of
3 jobs, then the full job name is "dexec[1-3]" if no explicit job name was
given.
=head2 --priority number
Set the priority of the LSF jobs. Expected is a value in the range 1 to 2000
while 1 is the lowest priority and 2000 the maximum. If the given value is
less than 1, then it is set to 1. If the given value is greater than 2000,
then it is set to 2000. The default value is 1000.
=head2 --queue string
Set the name of the LSF queue to which the jobs shall be submitted to.
If not given, then the default queue as configured in LSF is used.
=head2 --res-req string
Set an LSF I<resources requirement> string which is used when the job
array is submitted to LSF and affects the requirements of a single job.
This option is experimental. Handle with care!
=head2 --help
Print a short help text with version, usage and options.
=head2 --man
Print the man page using the I<less> pager.
=head2 --debug
Turn on the debug mode. None of the intermediate files (logs and input file)
are deleted in the debug mode. Verbosive output is written to STDOUT.
=head1 DIAGNOSTICS
The program produces a log file in the working directory for each job
that has been started. If a job was successful, then the corresponding log
file is deleted (unless in debug mode).
The format of the log file names is I<"dzip.%J.%I.log"> while I<%J> stands
for the job number and I<%I> for the job array index.
The program also produces a temporary input file in the working directory
for all jobs to be executed. That file is deleted before the program
terminates (unless in debug mode). The format of the input file name is
I<"dexec.input.XXXXX"> while the string of I<X's> is a random alphanumeric
string.
The exit code of the program is 0 if and only if all given commands have been
executed successfully. The program hangs waiting for input via STDIN if
no argument is given at all.
=head1 DEPENDENCIES
=over 4
=item perl 5.20.2 or higher
=item LSF 9.1
=item AMTC::LSF 1.1
=back
=head1 AUTHOR
If you found a bug, please talk to MDP first. It might be a feature. If not,
then please report it using L<https://helpdesk.photomask.com/>.
Olaf Heyder E<lt>heydero@drux25E<gt> or E<lt>olaf.heyder@amtc-dresden.comE<gt>
=head1 COPYRIGHT
Copyright (C) 2017 by Advanced Mask Technology Center GmbH & Co. KG
This script is for AMTC business use only. All information is confidential.
=cut