#!/usr/bin/perl -w
use strict;
my @args;

=head1 NAME

cpumax - Run as many commands as there are CPUs

=cut

use POSIX;
use Getopt::Long;
use Pod::Usage;
use Inline C => <<'END_C';

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

long get_nprocs() {
  long nprocs;
  if ((nprocs = sysconf(_SC_NPROCESSORS_ONLN))<1) {
    nprocs=1;
  }
  return nprocs;
}

END_C

=head1 SYNOPSIS

cpumax [OPTIONS]

cpumax reads command lines from STDIN, and runs as many in parallel
as there are available CPUs.

=head1 DESCRIPTION

maxcpu reads command lines from STDIN, and executes as many in
parallel as there are detected CPUs.

To portably detect how many CPUs are available, the POSIX system call
"sysconf" is available.  Unfortunately, Perl's POSIX module doesn't include
the constant required for CPU detection (_SC_NPROCESSORS_ONLN).  As a result,
maxcpu uses Inline::C to execute the detection function.

=head1 OPTIONS

=over 8

=cut

### cpus
our $opt_cpus = undef;
push(@args,"cpus|c=i");

=item B<-c, --cpus=NUM>

Override the number of detected CPUs.

=cut

### debug
our $opt_debug = 0;
push(@args,"debug|d");

=item B<-d, --debug>

Turns on process debugging.

=cut

### verbose
our $opt_verbose = 0;
push(@args,"verbose|v");

=item B<-v, --verbose>

Reports commands as they are executed.

=cut

### help
our $opt_help = 0;
push(@args,"help|h");

=item B<-h, --help>

Shows help documentation.

=cut

### version
use vars qw($NAME $VERSION);
$NAME="maxcpu";
$VERSION="1.02";
our $LICENSE="
All rights reserved. You may distribute this code under the terms 
of the GNU General Public License.
";
our $opt_version = 0;
push(@args,"version|V");

=item B<-V, --version>

Reports version

=cut

=back

=cut

Getopt::Long::Configure("bundling","no_ignore_case");
GetOptions(@args) || pod2usage(2);
pod2usage(-verbose => 1, -exitstatus => 0) if ($opt_help);
if ($opt_version) {
	print "$NAME v$VERSION\n";
	print "$LICENSE\n";
	exit(0);
}

select STDERR; $|=1;
select STDOUT; $|=1;

my %child;
my $running=0;
my @queue;
my $debug=0;

$debug=1 if (defined($ARGV[0]) && $ARGV[0] eq '-d');

warn "I am PID $$\n" if ($debug);

if (!defined($opt_cpus)) {
  # One Linux-only method is to check /proc/cpuinfo
  #open(CPUS,"</proc/cpuinfo") || die("/proc/cpuinfo: $!\n");
  #$opt_cpus=grep(/^$/,<CPUS>);
  #close(CPUS);

  # This should work, but Perl's POSIX module isn't up to date yet
  #my $opt_cpus=POSIX::sysconf(POSIX::_SC_NPROCESSORS_ONLN);

  $opt_cpus=get_nprocs();

  warn "Detected CPUs: $opt_cpus\n" if ($debug);

  $opt_cpus=1 if ($opt_cpus<1); # sanity check
}
warn "Using CPUs: $opt_cpus\n" if ($debug);

if (-t STDIN) {
	warn "Reading from terminal for commands.  Use --help for details.\n";
}

while (my $line=<STDIN>) {
	chomp($line);
	AddChild($line);
	Children(POSIX::WNOHANG);
}
while (Children(0)) {};

sub AddChild {
	my ($cmd)=@_;

	push(@queue,$cmd);
	warn "Command queue increased ($#queue: '$cmd')\n" if ($debug);

	CheckQueue();
}

sub CheckQueue {
    return unless (@queue);

    if ($running<$opt_cpus) {
    	my $cmd=shift(@queue);
	my $pid=fork();
	if ($pid<0) {
		die "fork: $!\n";
	}
	elsif ($pid==0) {
		exec $cmd;
		die "$cmd: $!\n";
	}
	else {
		print $cmd,"\n" if ($opt_verbose);
		warn "Spawned child $pid '$cmd' (remaining: ".($#queue+1).")\n" if ($debug);

		$child{$pid}=1;
		$running++;
	}
    }
}

sub Children {
	my ($flags)=@_;

	warn "Checking for dead children\n" if ($debug);
	my $done = POSIX::waitpid(-1, $flags);
	my $status = $?;
	warn "\tdone: $done\n" if ($debug);

	if ($done > 0) {
		warn "Reaped PID $done: $status\n" if ($debug);
		if (defined($child{$done})) {
			delete $child{$done};
			$running--;
		}
		else {
			warn "Unknown child PID $done caught!?\n" if ($debug);
		}
		CheckQueue();
	}
	else {
		# Children still running
		if ($debug) {
			warn "Still running:\n";
			foreach my $pid (sort keys %child) {
				warn "\t$pid\n";
			}
		}
	}

	my $ret=$#queue+1;
	$ret=$running if ($ret==0);

	warn "Queued: ".($#queue+1)."\n" if ($debug);
	warn "Running: $running\n" if ($debug);

	return $ret;
}

=head1 EXAMPLE

To execute CPU-intensive tasks in parallel, a list of tasks can be
created by the shell and fed into maxcpu:

	(for i in *.jpg; do
		echo convert -geometry 640x480 -quality 70 $i $i
	done) | maxcpu

=head1 AUTHOR

 Kees Cook, <kees@outflux.net>

=head1 SEE ALSO

perl(1), sysconf(3), Inline::C(1).

=head1 COPYRIGHT

maxcpu is Copyright (c) 2003, by Kees Cook, <kees@outflux.net>. 

All rights reserved. You may distribute this code under the terms 
of the GNU General Public License.

=cut

