Files
handbrake-cluster/handbrake-cluster-worker.pl
Ben Roberts 9c163e57c4 Simple bugfixes
Fixed typo preventing log entries in the email report from being
separated onto new lines.
Fixed mistake in error messages when HandBrake sockets are closed
2010-02-20 03:56:20 +00:00

281 lines
8.4 KiB
Perl
Executable File

#!/usr/bin/perl
use warnings;
use strict;
use Data::Dumper;
use Gearman::Worker;
use Getopt::Long;
use IO::Select;
use IO::Socket;
use IPC::Open3;
use Log::Handler;
use Pod::Usage;
use String::Random qw/random_string/;
use Storable qw/freeze thaw/;
use Switch;
use Symbol qw/gensym/;
# Globals
our %options = (
verbose => 0,
quiet => 0,
log_file => '/tmp/handbrake-cluster-worker.log',
silent => 0,
help => 0,
pretend => 0,
job_servers => ['build0.sihnon.net', 'build1.sihnon.net', 'build2.sihnon.net'],
handbrake => '/usr/bin/HandBrakeCLI',
);
Getopt::Long::Configure( qw(bundling no_getopt_compat) );
GetOptions(
'help|h' => \$options{help},
'verbose|v' => \$options{verbose},
'debug|d' => \$options{debug},
'quiet|q' => \$options{quiet},
'silent|s' => \$options{silent},
'log|l=s' => \$options{log_file},
'pretend|n' => \$options{pretend},
'job-servers|j=s@' => \$options{job_servers},
'handbrake' => \$options{handbrake},
) or pod2usage(-verbose => 0);
pod2usage(-verbose => 1) if ($options{help});
# Setup logging
my $log = Log::Handler->new();
my $maxLogLevel = ($options{debug} ? 'debug' : ($options{quiet} ? 3 : $options{verbose} + 4)); # default to logging warning+, unless quiet in which case log error+, or debug in which case log everything
my $maxFileLogLevel = ($options{debug} ? 'debug' : 'info');
if ( ! $options{silent}) {
$log->add(
screen => {
log_to => 'STDOUT',
minlevel => 'emergency',
maxlevel => $maxLogLevel,
},
);
}
if ( $options{log_file}) {
$log->add(
file => {
filename => $options{log_file},
minlevel => 'emergency',
maxlevel => $maxFileLogLevel,
},
);
}
# Setup the worker, and listen for jobs
my $worker = Gearman::Worker->new(job_servers => $options{job_servers});
$worker->register_function(handbrake_rip => \&handbrake_rip);
$worker->work while 1;
sub handbrake_rip {
my $job = shift;
my $rip_options = thaw($job->arg);
my $response = {};
$log->notice("Beginning new rip to $rip_options->{output_filename}");
# Generate a unique filename based on the output filename to prevent clashes from previous runs
my $uuid = random_string('cccccc');
$rip_options->{real_output_filename} = $rip_options->{output_filename};
$rip_options->{real_output_filename} =~ s/\.([^\.]+)$/\.$uuid\.$1/;
$response->{real_output_filename} = $rip_options->{real_output_filename};
# Generate the command line for handbrake
my @handbrake_cmd = (
# Reduce the priority of the ripping process, since it is very processor intensive
'nice', '-n', $rip_options->{nice},
# Construct the handbrake command line
$options{handbrake},
get_options($rip_options, 'input_filename', '-i'),
get_options($rip_options, 'real_output_filename', '-o'),
get_options($rip_options, 'title'),
get_options($rip_options, 'format', '-f'),
get_options($rip_options, 'video_codec', '-e'),
get_options($rip_options, 'quantizer', '-q'),
get_options($rip_options, 'video_width', '-w'),
get_options($rip_options, 'video_height', '-l'),
get_options($rip_options, 'deinterlace'),
get_options($rip_options, 'audio_tracks', '-a'),
get_options($rip_options, 'audio_codec', '-E'),
get_options($rip_options, 'audio_names', '-A'),
get_options($rip_options, 'subtitle_tracks', '-s'),
);
# Return a copy of the command used to rip the title
$response->{handbrake_cmd} = @handbrake_cmd;
# flag the start of the job
$job->set_status(0, 100);
# Execute the ripping process
$log->debug("Beginning ripping process with command:\n" . join(' ', @handbrake_cmd));
my ($child_in, $child_out, $child_err) = map gensym, 1..3;
my $child_pid = open3($child_in, $child_out, $child_err, @handbrake_cmd);
# No need to write to the child process
close($child_in);
# Log the output from handbrake, and return it back to the client
$response->{log} = ();
my $line;
my $select = IO::Select->new();
$select->add($child_out);
$select->add($child_err);
my $child_out_buffer;
my $child_err_buffer;
while (my @ready = $select->can_read()) {
foreach my $handle (@ready) {
my $bytes_read = sysread($handle, my $buf = '', 1024);
if ($bytes_read == -1) {
$log->error("Error reading from HandBrake socket: $!");
$select->remove($child_out);
next;
} elsif ($bytes_read == 0) {
$log->debug("HandBrake socket closed");
$select->remove($handle);
next;
}
if ($handle == $child_out) {
$child_out_buffer .= $buf;
# Check for complete lines in the output
while ((my @lines = split(/[\r\n]+/, $child_out_buffer, 2)) > 1) {
$line = $lines[0];
$child_out_buffer = $lines[1];
if ($line =~ m/Encoding: task \d+ of \d+, (\d+\.\d+) %/) {
my $numerator = $1;
$job->set_status($numerator, 1);
}
}
} else {
$child_err_buffer .= $buf;
# Check for complete lines in the output
while ((my @lines = split(/[\r\n]+/, $child_err_buffer, 2)) > 1) {
$line = $lines[0];
$child_err_buffer = $lines[1];
push @{ $response->{log} }, $line;
$log->notice($line);
}
}
}
}
close($child_out);
close($child_err);
$job->set_status(100, 100);
# If the rip process failed, report an error status here
waitpid($child_pid, 0);
$response->{status} = $? >> 8;
$response->{success} = $response->{status} == 0;
if ($response->{success}) {
$log->notice("Finished rip to $response->{real_output_filename}");
} else {
$log->warning("Ripping process returned error status: $response->{status}");
}
return freeze($response);
}
sub get_options {
my $rip_options = shift or die;
my $option_name = shift or die;
my $option = shift;
switch ($option_name) {
case 'title' {
return ('-L') if ! defined($rip_options->{$option_name}) || $rip_options->{$option_name} < 0;
return ('-t', $rip_options->{$option_name});
}
case 'deinterlace' {
switch ($rip_options->{$option_name}) {
case 1 { return ('-d') }
case 2 { return ('-5') }
return ();
}
}
return (defined $rip_options->{$option_name} ? ($option, $rip_options->{$option_name}) : ());
}
}
__END__;
=head1 NAME
handbrake-cluster-worker - Processes DVD rips farmed out by gearman
=head1 SYNOPSIS
handbrake-cluster-worker.pl -h|--help
handbrake-cluster-worker.pl [[-v|--verbose] | [-d|--debug] |
[-q|--quiet] | [-s|--silent]]
[-l|--log <log file>]
[-n|--pretend]
[-j|--job-servers <server list>]
[--handbrake <handbrake executable>]
=head1 DESCRIPTION
Processes ripping tasks as requested by a gearman job server. Logging and the
job servers to use are configurable by command line arguments.
=head1 OPTIONS
=over 4
=item B<-h|--help>
Displays this help information and quits.
=item B<-v|--verbose>
Displays verbose logging information.
=item B<-d|--debug>
Displays full debugging information, including all output from HandBrake.
=item B<-q|--quiet>
Displays only errors.
=item B<-s|--silent>
Displays no output whatsoever, useful for scripting. Output will still be
logged to disk if a file is specified.
=item B<-l|--log E<lt>log fileE<gt>>
Specifies the name of a file to write logging information to.
=item B<-n|--pretend>
Only shows what action would be taken, no rips are actually performed.
=item B<-j|--job-servers E<lt>server listE<gt>>
Specifies a comma separated list of alternate servers to use for job
distribution.
=item B<--handbrake E<lt>handbrake executableE<gt>>
Specifies an alternate HandBrake executable to use. Default is
C</usr/bin/HandBrakeCLI>.
=cut