File Coverage

blib/lib/Makefile/Parallel.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Makefile::Parallel;
2              
3 1     1   31716 use Makefile::Parallel::Grammar;
  0            
  0            
4             use Log::Log4perl;
5             use Proc::Simple;
6             use Clone qw(clone);
7             use Time::HiRes qw(gettimeofday tv_interval);
8             use Time::Interval;
9             use Time::Piece::ISO;
10             use GraphViz;
11             use Digest::MD5;
12             use Data::Dumper;
13              
14             use warnings;
15             use strict;
16             our $VERSION = '0.08';
17              
18             =head1 NAME
19              
20             =encoding utf8
21              
22             Makefile::Parallel - A distributed parallel makefile
23              
24             =head1 SYNOPSIS
25              
26             This module should not be called directly. Please see the perldoc of
27             the pmake program on the /examples directory of this distribution.
28              
29             =cut
30              
31             # Module Stuff
32             our @ISA = qw(Exporter);
33             our %EXPORT_TAGS = ( 'all' => [ qw() ] );
34             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
35              
36             our @EXPORT = qw( process_makefile );
37              
38             my $logger;
39              
40             my $queue;
41             my $running = {}; # Holds the running ID's (ID -> info)
42             my $finnished = {}; # Holds the finnished ID's (ID -> info)
43             my $scheduler; # Holds the scheduler engine
44             my $counter = 0; # Holds the order of the executed processes
45             my $filename; # Holds the filename of the makefile
46             my $debug; # TRUE if we got debug enabled
47              
48             # This stuff deals with the interruption (Ctrl + C)
49             $SIG{INT} = \&process_interrupt;
50             my $interrupted = 0;
51              
52             =head1 process_makefile
53              
54             Main function. Accepts a file to parse and a
55             hash reference with options.
56              
57             TODO: Document options
58              
59             =cut
60              
61             sub process_makefile {
62             my ($file, $options) = @_;
63              
64             # Set sensible defaults
65             $options ||= {};
66             $options->{scheduler} ||= 'LOCAL';
67             $options->{local} ||= '1'; # Default CPU's on local mode
68             $options->{dump} ||= 0;
69             $options->{clean} ||= 0;
70             $options->{clock} ||= 10;
71             $options->{debug} ||= 0;
72             $options->{continue} ||= 0;
73              
74             # TODO: Give more flexibility
75             if($options->{scheduler} eq 'PBS') {
76             use Makefile::Parallel::Scheduler::PBS;
77             $scheduler = Makefile::Parallel::Scheduler::PBS->new();
78             $scheduler->{mail} = $options->{mail} if $options->{mail};
79             } else {
80             use Makefile::Parallel::Scheduler::Local;
81             $scheduler = Makefile::Parallel::Scheduler::Local->new({ max => $options->{local} });
82             }
83              
84             # Debug settings
85             if($options->{debug}) {
86             # Clean logs... ## FIXME - do not rely on OS.
87             `rm -rf log/`; mkdir "log";
88              
89             my $conf = q(
90             log4perl.category.PMake = DEBUG, Logfile, Screen
91              
92             log4perl.appender.Logfile = Log::Log4perl::Appender::File
93             log4perl.appender.Logfile.filename = log/makefile.log
94             log4perl.appender.Logfile.layout = Log::Log4perl::Layout::PatternLayout
95             log4perl.appender.Logfile.layout.ConversionPattern = [%d] [%p] %F(%L) %m%n
96              
97             log4perl.appender.Screen = Log::Log4perl::Appender::Screen
98             log4perl.appender.Screen.stderr = 0
99             log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
100             log4perl.appender.Screen.layout.ConversionPattern = [%d] %m%n
101             );
102             Log::Log4perl::init(\$conf);
103             $debug = 1;
104             }
105             else {
106             my $conf = q(
107             log4perl.category.PMake = INFO, Screen
108              
109             log4perl.appender.Screen = Log::Log4perl::Appender::Screen
110             log4perl.appender.Screen.stderr = 0
111             log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
112             log4perl.appender.Screen.layout.ConversionPattern = [%d] %m%n
113             );
114             Log::Log4perl::init(\$conf);
115             $debug = 0;
116             }
117             $logger = Log::Log4perl::get_logger("PMake");
118              
119             # Parse the file
120             $logger->info("Trying to parse \"$file\"");
121             $queue = Makefile::Parallel::Grammar->parseFile($file);
122              
123             if($queue) { $logger->info("Parse ok.. proceeding to plan the scheduling"); }
124             else { $logger->error("Parse failed, aborting..."); return }
125             $filename = $file;
126              
127             # Copy perl routines to perl actions
128             if(defined $queue->[-1]{perl}) {
129             for my $job (@{$queue}) {
130             if(defined $job->{action}[0]{perl}) {
131             $job->{perl} = $queue->[-1]{perl};
132             }
133             }
134             delete $queue->[-1];
135             }
136              
137             # Dump if the user want it
138             die Dumper $queue if($options->{dump});
139              
140             # Clean the temporary files if we are PBS
141             clean() if($options->{clean});
142              
143             # Recover the journal if the user wants to continue
144             journal_recover() if ($options->{continue});
145              
146             # Enter the loop
147             while(1) {
148             # $logger->debug("New loop starting");
149             loop();
150             # $logger->debug("Loop processed, sleeping");
151             sleep $options->{clock};
152             }
153             }
154              
155             =head1 journal_recover
156              
157             Tries to recover the journal of the last makefile run.
158              
159             =cut
160              
161             sub journal_recover {
162             my $journal = do "$filename.journal" or die "Can't open $filename.journal: $!";
163              
164             my $md5 = calc_makefile_md5();
165             if($journal->{md5} ne $md5) {
166             $logger->warn("MD5 Check Failed... The original Makefile was changed!! CONTINUE AT YOUR OWN RISK!");
167             }
168              
169             # Restore the finnished list
170             $finnished = $journal->{finnished};
171             $counter = $journal->{counter};
172              
173             # Ignore jobs already concluded
174             # 1a passagem - cálculo das variáveis
175             for my $job (@{$queue}) {
176             next unless $job;
177             if(is_finnished($job->{rule}{id})) {
178             # If we got asShell to run, run it!
179             find_and_run_asShell($job->{rule}{id});
180             # If we got asPerl to run, run it!
181             find_and_run_asPerl($job->{rule}{id});
182             }
183             }
184              
185             # 2a passagem - remoção dos já executados
186             my $new_queue = [];
187             for my $job (@{$queue}) {
188             next unless $job;
189             push @{$new_queue}, $job unless is_finnished($job->{rule}{id});
190             }
191             $queue = $new_queue;
192              
193             $logger->warn("Journal recovered.. Cross your fingers now...");
194             }
195              
196             =head1 clean
197              
198             This function is responsible to clean all the temporary files
199             created by the PBS system. It should be used only on the PBS scheduler
200             method.
201              
202             =cut
203              
204             sub clean {
205             $scheduler->clean($queue);
206              
207             $logger->info("Temporary files cleaned");
208             exit(0);
209             }
210              
211             =head1 loop
212              
213             Loop it baby :D
214              
215             =cut
216              
217             sub loop {
218             reap_dead_bodies();
219             dispatch();
220             write_journal();
221             }
222              
223             =head1 reap_dead_bodies
224              
225             This function is responsible of reaping the jobs that are
226             finnished. If the job needs to run something at the end
227             (example, find i <- grep | awk...) it is executed and the job
228             queue is expanded.
229              
230             =cut
231              
232             sub reap_dead_bodies {
233             # Search all running procs for someone who died
234             for my $runid (keys %{$running}) {
235             if($scheduler->poll($running->{$runid}, $logger)) {
236             # Still running
237             } else {
238             # No running anymore, remove from running and save
239              
240             # Save time stats
241             my $t1 = [gettimeofday];
242             my $elapsed = tv_interval($running->{$runid}->{starttime}, $t1);
243              
244             $running->{$runid}->{stoptime} = $t1;
245              
246             $elapsed = parseInterval(seconds => int($elapsed), Small => 1);
247             $running->{$runid}->{elapsed} = $elapsed;
248              
249             # Give user some feedback
250             $logger->info("Process " . $scheduler->get_id($running->{$runid})
251             . " (" . $running->{$runid}->{rule}->{id}
252             . ") has terminated [$elapsed]");
253             $finnished->{$runid} = $running->{$runid};
254             delete $running->{$runid};
255              
256             # Don't do nothing more if it was interrupted
257             next if($finnished->{$runid}{interrupted});
258              
259             # Verify the exit status
260             $scheduler->get_dead_job_info($finnished->{$runid});
261             if($finnished->{$runid}{exitstatus} && !$finnished->{$runid}{interrupted}) {
262             # Pumm!! Cancelar tudo!
263             $logger->fatal("Process " . $scheduler->get_id($finnished->{$runid}) . " exited
264             with exit status " . $finnished->{$runid}{exitstatus} . "! Aborting
265             all queue...");
266             process_interrupt(1); # Forced;
267             $finnished->{$runid}{fatal} = 1; # To graphviz later...
268             }
269              
270             # If we got asShell to run, run it!
271             find_and_run_asShell($runid);
272             # If we got asPerl to run, run it!
273             find_and_run_asPerl($runid);
274             }
275             }
276             }
277              
278             =head1 find_and_run_asShell
279              
280             This function goes through the finnished job
281             and tries to find asShell commands to run, doing
282             all the expands necessary
283              
284             =cut
285              
286             sub find_and_run_asShell {
287             my ($runid) = @_;
288              
289             for my $action (@{$finnished->{$runid}->{action}}) {
290             if($action->{asShell} && !(defined $finnished->{__var__}->{$action->{def}})) {
291             $logger->info("Running shell action $action->{asShell}");
292             $finnished->{__var__}->{$action->{def}} = [];
293              
294             open P, "$action->{asShell} |";
295             while(

) {

296             chomp;
297             $logger->warn("Return value from the shell action is not a integer") unless /^\d+$/;
298             push @{$finnished->{__var__}->{$action->{def}}}, $_;
299             }
300             close P;
301              
302             # Now expand the queue
303             expand_forks($action->{def});
304             }
305             }
306             }
307              
308             =head1 find_and_run_asPerl
309              
310             This function goes through the finnished job
311             and tries to find asPerl commands to run, doing
312             all the expands necessary
313              
314             =cut
315              
316             sub find_and_run_asPerl {
317             my ($runid) = @_;
318              
319             for my $action (@{$finnished->{$runid}->{action}}) {
320             if($action->{asPerl} && !(defined $finnished->{__var__}->{$action->{def}})) {
321             $logger->info("Running perl action $action->{asPerl}");
322             $finnished->{__var__}->{$action->{def}} = [];
323             $finnished->{__var__}{$action->{def}} = paction_list($action->{asPerl});
324              
325             # Now expand the queue
326             expand_forks($action->{def});
327             }
328             }
329             }
330              
331             =head1 paction_list
332              
333             this function evaluates a perl action and retruns a list of strings.
334             the action can:
335              
336             .return a ARRAY reference,
337             .print a list of lines to STDOUT (to be splited end chomped)
338             .or return a string (to be splited and chomped)
339              
340             =cut
341              
342             sub paction_list{
343             my $act=shift;
344             my $var="";
345             my $final=[];
346             open(A,'>', \$var);
347             my $old= select A;
348             my $res = eval( "package main; no strict; " . $act );
349             die $@ if $@;
350             close A;
351             select $old;
352              
353             if (ref($res) eq "ARRAY"){
354             $final = $res; }
355             elsif($var =~ /\S/) {
356             for(split("\n",$var)){ push (@$final, $_) if /\S/; } }
357             else{
358             for(split("\n",$res)){ push (@$final, $_) if /\S/; } }
359              
360             $final;
361             }
362              
363             =head1 expand_forks
364              
365             This function is responsible of expanding all the jobs
366             when a variable is evaluated. It expands both forks and
367             joins.
368              
369             =cut
370              
371             sub expand_forks {
372             my ($var) = @_;
373             my $values = $finnished->{__var__}->{$var};
374              
375             # For all queue items that has a $var, expand
376             my $index = -1;
377             for my $job (@{$queue}) {
378             $index++;
379             next unless $job;
380              
381             if($job->{rule}{vars} && (grep { $_ eq "\$$var" } @{$job->{rule}{vars}} )) {
382             $logger->info("Found a fork on $job->{rule}->{id}. Expanding...");
383              
384             # Expand, expand, expand
385             $job->{rule}{vars} = [ grep { $_ ne "\$$var" } @{$job->{rule}{vars} }];
386             delete $job->{rule}{vars} unless scalar @{$job->{rule}{vars}};
387            
388             delete $queue->[$index];
389              
390             my $count = 0;
391             my @added_jobs = ();
392             for my $index (@{$values}) {
393             my $newjob = clone($job);
394             $count++;
395              
396             # Actualiazr o id
397             $newjob->{rule}{id} .= $index;
398              
399             # Actualizar a linha a executar
400             for my $act (@{$newjob->{action}}) {
401             if($act->{shell}){
402             $act->{shell} =~ s/\$$var\b/$index/g;
403             }
404             elsif($act->{perl}){
405             $act->{perl} =~ s/\$$var\b/$index/g;
406             }
407             }
408              
409             # Expand pipelines
410             for my $dep (@{$newjob->{depend_on}}) {
411             if ($dep->{vars} && (grep { $_ eq "\$$var"} @{$dep->{vars}} )) {
412             # Expand the dependencie
413             $dep->{vars} = [ grep { $_ ne "\$$var" } @{$dep->{vars}} ];
414             delete $dep->{vars} unless scalar @{$dep->{vars}};
415             $dep->{id} .= $index;
416             }
417             }
418             push @{$queue}, $newjob;
419             push @added_jobs, $newjob->{rule}{id};
420             }
421             $logger->info("Expanded.. Created new $count jobs: @added_jobs");
422             }
423              
424             # Find joiners
425             my $pos = 0;
426             for my $dep (@{$job->{depend_on}}) {
427             if ($dep->{vars} && (grep { $_ eq "\$$var" } @{$dep->{vars}} )) {
428            
429             $dep->{vars} = [ grep { $_ ne "\$$var" } @{$dep->{vars}}];
430            
431             # Expand the dependencies
432             delete $job->{depend_on}->[$pos];
433             for my $index (@{$values}) {
434             my @vars = (scalar @{$dep->{vars}})?(vars => $dep->{vars}):();
435             push @{$job->{depend_on}}, { @vars,
436             id => $dep->{id} . $index };
437             }
438             }
439             $pos++;
440             }
441             }
442              
443             # Now find constructors like @var
444             for my $job (@{$queue}) {
445             next unless $job;
446              
447             # Search on actions
448             for my $action (@{$job->{action}}) {
449             if($action->{shell} && $action->{shell} =~ /\@$var\b/) {
450             my $string = '';
451             map { $string .= "$_ " } @{$values};
452             $action->{shell} =~ s/\@$var\b/$string/g;
453             $logger->info("The job $job->{rule}->{id} has been action expanded with $string");
454             }
455             elsif($action->{perl} && $action->{perl} =~ /\@$var\b/) {
456             my $string = join(",", map { "q{$_}" } @{$values});
457             $action->{perl} =~ s/\@$var\b/($string)/g;
458             $logger->info("The job $job->{rule}->{id} has been action expanded with ($string)");
459             }
460             }
461              
462             # Search on asShell
463             for my $action (@{$job->{action}}) {
464             if($action->{asShell} && $action->{asShell} =~ /\@$var\b/) {
465             my $string = '';
466             map { $string .= "$_ " } @{$values};
467             $action->{asShell} =~ s/\@$var\b/$string/g;
468             $logger->info("The job $job->{rule}->{id} has been shell expanded with $string");
469             }
470             }
471              
472             # Search on asPerl
473             for my $action (@{$job->{action}}) {
474             if($action->{asPerl} && $action->{asPerl} =~ /\@$var\b/) {
475             my $string = 'qw/';
476             map { $string .= "$_ " } @{$values};
477             $string .= "/";
478              
479             $action->{asPerl} =~ s/\@$var\b/$string/g;
480             $logger->info("The job $job->{rule}->{id} has been Perl expanded with $string");
481             }
482             }
483             }
484             }
485              
486             =head1 report
487              
488             Print a pretty report bla bla bla
489              
490             =cut
491              
492             sub report {
493              
494             $logger->info("Creating HTML report");
495             open REPORT, ">$filename.html" or die "Can't create $filename.html";
496              
497             print REPORT "\n"; \n"; \n";
498             print REPORT "
IDStart TimeEnd TimeElapsed
499              
500             my ($id,$start,$stop,$interval);
501             for my $job (sort sortcallback keys %{$finnished}) {
502             next unless $finnished->{$job}{rule};
503              
504             $id = $finnished->{$job}{rule}{id};
505             $start = (localtime($finnished->{$job}{starttime}[0]))->iso;
506             $stop = (localtime($finnished->{$job}{stoptime}[0]))->iso;
507             $interval = $finnished->{$job}{realtime} || $finnished->{$job}{elapsed};
508             print REPORT "
$id$start$stop$interval
509             }
510              
511             print REPORT "
\n";
512             close REPORT;
513             }
514              
515             sub sortcallback {
516             my $foo = $finnished->{$a};
517             my $bar = $finnished->{$b};
518              
519             return 0 if(!$foo->{order} && !$bar->{order});
520             return -1 unless $foo->{order};
521             return 1 unless $bar->{order};
522              
523             return $foo->{order} <=> $bar->{order};
524             }
525              
526             =head1 dispatch
527              
528             This function is responsible for dispatching the jobs that can run.
529              
530             =cut
531              
532             sub dispatch {
533             my $new_queue = [];
534              
535             # If we aren't running nothing and ($interrupted || $queue empty) exit
536             if((scalar keys %{$running}) == 0 && ($interrupted || (scalar @{$queue} == 0))) {
537             $logger->info("Terminating the pipeline");
538             at_exit();
539             }
540              
541             # We don't wanna dispatch NOTHING if we have interrupted
542             return if $interrupted;
543              
544             for my $job (@{$queue}) {
545             next unless $job;
546              
547             # Find if the job dependencies are finnished
548             if(can_run_job($job->{rule}->{id}, $job->{depend_on})) {
549              
550             $logger->info(Dumper($job)) unless $job->{rule}{id};
551              
552             $logger->info("The job \"" . $job->{rule}->{id} . "\" is ready to run. Launching");
553             launch($job);
554             $job->{starttime} = [gettimeofday];
555              
556             # Jump to the next job in queue
557             next;
558             }
559              
560             # This job can't run yet.. add it to the new queue
561             push @{$new_queue}, $job;
562             }
563              
564             # Return the new queue, the jobs that can't be dispatched yet
565             $queue = $new_queue;
566             }
567              
568             =head1 is_finnished
569              
570             This function checks if the specified job is already done in
571             the finnished list.
572              
573             =cut
574              
575             sub is_finnished {
576             my ($jobid) = @_;
577             for my $job (keys %{$finnished}) {
578             next unless $finnished->{$job}{rule};
579             return 1 if($finnished->{$job}{rule}{id} eq $jobid);
580             }
581             return 0;
582             }
583              
584             =head1 at_exit
585              
586             This sub is called at the program exit
587              
588             =cut
589              
590             sub at_exit {
591             graphviz();
592             report();
593             write_journal();
594              
595             exit(0);
596             }
597              
598             =head1 write_journal
599              
600             Saves the scheduler state to disk.
601              
602             =cut
603              
604             sub write_journal {
605             my $journal = {};
606             $journal->{md5} = calc_makefile_md5();
607              
608             # Pass all interrupted and failled processes back to queue
609             my $acabados = clone($finnished);
610             for my $job (keys %{$acabados}) {
611             next unless $acabados->{$job}{rule};
612              
613             if($acabados->{$job}{fatal} || $acabados->{$job}{interrupted}) {
614             delete $acabados->{$job};
615             }
616             }
617             delete $acabados->{__var__};
618              
619             $journal->{finnished} = $acabados;
620             $journal->{counter} = $counter;
621              
622             open F, ">$filename.journal";
623             print F (Dumper $journal);
624             close F;
625             }
626              
627             =head1 calc_makefile_md5
628              
629             Calculates the MD5 of the current makefile
630              
631             =cut
632              
633             sub calc_makefile_md5 {
634             open F, "<$filename";
635             my $ctx = Digest::MD5->new;
636             $ctx->addfile(*F);
637             close F;
638              
639             return $ctx->b64digest;
640             }
641              
642             =head1 can_run_jub
643              
644             This one finds out if a job can run (all the dependencies are met).
645              
646             =cut
647              
648             sub can_run_job {
649             my ($id, $deps) = @_;
650              
651             return 0 unless $scheduler->can_run();
652              
653             for my $dep (@{$deps}) {
654             next unless $dep;
655             next unless $dep->{id};
656             return 0 unless defined $finnished->{$dep->{id}}
657             }
658              
659             return 1;
660             }
661              
662             =head1 launch
663              
664             Launch a process (really??)
665              
666             =cut
667              
668             sub launch {
669             my ($job) = @_;
670              
671             # Launch the process
672             $scheduler->launch($job, $debug);
673             $job->{order} = $counter++;
674              
675             # Save in the running list
676             $running->{$job->{rule}->{id}} = $job;
677             $logger->info("Launched \"" . $job->{rule}->{id} . "\" (" . $scheduler->get_id($job) . ")");
678             }
679              
680             =head1 graphviz
681              
682             Builds a preety graphviz file after the execution of the makefile
683              
684             =cut
685              
686             sub graphviz {
687             my $time_for = {}; # Holds the walltime for the job id
688              
689             my $g = GraphViz->new(rankdir => 1);
690              
691             $logger->info("Creating GraphViz nodes");
692             # Create all nodes
693             for my $job (keys %{$finnished}) {
694             next unless $finnished->{$job}{rule};
695              
696             my $id = $finnished->{$job}{rule}{id};
697             $time_for->{$id} = $finnished->{$job}{realtime} || $finnished->{$job}{elapsed};
698              
699             my $color = 'black';
700             $color = 'red' if $finnished->{$job}{fatal};
701             $color = 'yellow' if $finnished->{$job}{interrupted};
702              
703             $g->add_node($id, label => "$id\n$time_for->{$id}"
704             , shape => 'box', color => $color);
705             }
706              
707             $logger->info("Creating GraphViz edges");
708             # Create edges
709             for my $job (keys %{$finnished}) {
710             next unless $finnished->{$job}{rule};
711              
712             for my $dep (@{$finnished->{$job}{depend_on}}) {
713             next unless $dep;
714              
715             $g->add_edge($dep->{id}, $finnished->{$job}{rule}{id});
716             }
717             }
718              
719             open F, ">$filename.ps";
720             print F $g->as_ps;
721             close F;
722              
723             open F, ">$filename.dot";
724             print F $g->as_text;
725             close F;
726              
727             $logger->info("GraphViz file created on $filename.ps");
728             }
729              
730             =head1 process_interrupt
731              
732             This function is called everytime the user send a SIGINT to this process.
733             The objective is to kill all the running processes and wait for them to die.
734              
735             =cut
736              
737             sub process_interrupt {
738             my $forced = shift;
739             $forced = 0 if $forced eq "INT"; # Hack O:-)
740              
741             if(!$interrupted || $forced) {
742             if(!$forced) {
743             $logger->warn("Interrupt pressed, enter QUIT to quit, other thing to continue");
744             my $linha = ;
745             chomp($linha);
746              
747             if($linha ne 'QUIT') {
748             $logger->info("Interrupt canceled... Keeping the loop");
749             return;
750             }
751             }
752              
753             $interrupted = 1;
754             $logger->info("Interrupt pressed, cleaning all the running processes");
755              
756             for my $runid (keys %{$running}) {
757             $logger->info("Terminating job " . $scheduler->get_id($running->{$runid}));
758             $running->{$runid}{interrupted} = 1;
759             $scheduler->interrupt($running->{$runid});
760             }
761             } else {
762             $logger->warn("Interrupt already called, please wait while cleaning");
763             }
764             }
765              
766              
767             =head1 AUTHOR
768              
769             Ruben Fonseca, C<< >>
770              
771             Alberto Simões C<< >>
772              
773             José João Almeida C<< >>
774              
775             =head1 BUGS
776              
777             Please report any bugs or feature requests to
778             C, or through the web interface at
779             L.
780             I will be notified, and then you'll automatically be notified of progress on
781             your bug as I make changes.
782              
783             =head1 COPYRIGHT & LICENSE
784              
785             Copyright 2006-2011 Ruben Fonseca, et al, all rights reserved.
786              
787             This program is free software; you can redistribute it and/or modify it
788             under the same terms as Perl itself.
789              
790             =cut
791              
792             1; # End of Makefile::Parallel