2 # Batch EXecutor 2.0 -- Parallel Execution Using Screen
3 # (c) 2011 Martin Mares <mj@ucw.cz>
16 my $screen_session = 'BEX';
20 "q|queue=s" => \$queue_name,
21 "session=s" => \$screen_session,
22 "text!" => \$text_mode,
24 Usage: bprun [<options>] [[!]<machine-or-class> ...]
27 -q, --queue=<name> Run jobs in the given queue
28 --session=<name> Job windows should be opened within the given screen
29 session (default: BEX)
30 --text Use textual user interface instead of curses
33 system 'screen', '-S', $screen_session, '-X', 'select', '.';
34 !$? or die "Screen session $screen_session not found\n";
36 my $queue = BEX::Queue->new($queue_name);
37 my $fifo_name = $queue->{'Name'} . '/status-fifo';
39 mkfifo $fifo_name, 0700 or die "Cannot create $fifo_name: $!";
40 open FIFO, '+<', $fifo_name or die "Cannot open $fifo_name: $!";
42 my $ui = ($text_mode ? BEX::bprun::text->new : BEX::bprun::curses->new);
45 for my $mach (BEX::Config::parse_machine_list(@ARGV ? @ARGV : '*')) {
46 my @jobs = $queue->scan($mach);
48 push @machines, $mach;
49 for (@jobs) { $ui->update($mach, $_, 'READY'); }
53 my $max = $BEX::Config::max_parallel_jobs;
55 while (keys %running || @machines) {
56 if (@machines && keys %running < $max) {
57 my $mach = shift @machines;
58 $ui->update($mach, undef, 'START');
59 my @scr = ('screen', '-t', $mach);
60 push @scr, '-S', $screen_session if defined $screen_session;
61 push @scr, '-X', 'screen', './brun', "--status-fifo=$fifo_name", $mach;
63 !$? or $ui->update($mach, undef, 'INTERR');
64 $running{$mach} = 'START';
69 my ($mach, $jid, $stat) = /^! (\S+) (\S+) (\S+)$/;
71 $ui->err("Received invalid status message <$_>");
74 if (!defined $running{$mach}) {
75 $ui->err("Received status message <$_> for a machine which does not run");
78 $running{$mach} = $stat;
79 $ui->update($mach, ($jid eq '-' ? undef : $jid), $stat);
80 if ($stat eq 'DONE') {
81 delete $running{$mach};
89 package BEX::bprun::text;
99 my ($ui, $mach, $jid, $stat) = @_;
100 print +($mach // '-'), (defined($jid) ? ":$jid" : ""), " $stat\n";
105 print STDERR "ERROR: $msg\n";
108 package BEX::bprun::curses;
124 my %host_last_fail_job;
125 my %host_last_fail_stat;
130 has_colors && COLORS >= 8 && COLOR_PAIRS >= 8 or die "Your terminal is too dumb for me\n";
136 init_pair(1, COLOR_YELLOW, COLOR_BLUE);
137 init_pair(2, COLOR_YELLOW, COLOR_RED);
138 init_pair(3, COLOR_YELLOW, COLOR_BLACK);
139 init_pair(4, COLOR_RED, COLOR_BLACK);
141 $nrows = $C->getmaxy - 2;
142 if ($BEX::Config::max_parallel_jobs > $nrows) {
143 $BEX::Config::max_parallel_jobs = $nrows;
146 %host_state = %host_cnt = ();
147 %job_state = %job_cnt = ();
148 for my $s ('unknown', 'ready', 'running', 'done', 'failed') {
150 $job_cnt{'*'}{$s} = 0;
160 $C->bkgdset(COLOR_PAIR(1) | A_BOLD);
161 $C->addstr($C->getmaxy-1, 0, "Press any key to quit...");
169 $C->bkgdset(COLOR_PAIR(2) | A_BOLD);
170 $C->addnstr($C->getmaxy-1, 0, "ERROR: $msg", $C->getmaxx);
175 sub set_host_status($$$) {
176 my ($ui, $mach, $stat) = @_;
177 my $prev_stat = $host_state{$mach};
178 if (defined $prev_stat) {
179 $host_cnt{$prev_stat}--;
181 for my $s ('unknown', 'ready', 'running', 'done', 'failed') { $job_cnt{$mach}{$s} = 0; }
183 $host_state{$mach} = $stat;
187 sub set_job_status($$$$) {
188 my ($ui, $mach, $jid, $stat) = @_;
189 my $prev_stat = $job_state{$mach}{$jid} // 'unknown';
190 $job_cnt{$mach}{$prev_stat}--;
191 $job_cnt{'*'}{$prev_stat}--;
192 $job_state{$mach}{$jid} = $stat;
193 $job_cnt{$mach}{$stat}++;
194 $job_cnt{'*'}{$stat}++;
197 sub refresh_status($) {
198 $C->bkgdset(COLOR_PAIR(1) | A_BOLD);
200 sprintf("BEX Hosts: %dR %dD %dE %dW Jobs: %dR %dD %dE %dW",
201 $host_cnt{'running'},
205 $job_cnt{'*'}{'running'},
206 $job_cnt{'*'}{'done'},
207 $job_cnt{'*'}{'failed'},
208 $job_cnt{'*'}{'ready'},
217 if (defined ($s = $by_host{$mach})) {
221 for my $i (0..$nrows-1) {
227 } elsif ($r->{'Gone'} && (!$best || $best->{'Gone'} > $r->{'Gone'})) {
233 delete $by_host{$best->{'Host'}};
235 $s->{'Host'} = $mach;
236 $s->{'Row'} = $besti;
237 $by_host{$mach} = $s;
238 $by_row[$besti] = $s;
243 my $gone_counter = 1;
246 $s->{'Gone'} = $gone_counter++;
251 my $mach = $s->{'Host'};
252 my $stat = $s->{'Status'} // "?";
253 my $jid = $s->{'Job'} // "";
254 my $jname = ($jid eq "" ? "" : $queue->job_name($jid));
255 my $jcnt = $job_cnt{$mach};
256 if ($jcnt->{'running'}) {
257 if ($jcnt->{'failed'}) {
258 $C->bkgdset(COLOR_PAIR(4) | A_BOLD);
260 $C->bkgdset(COLOR_PAIR(3) | A_BOLD);
263 if ($jcnt->{'failed'}) {
264 $C->bkgdset(COLOR_PAIR(4));
269 my $r = $s->{'Row'} + 1;
270 $C->addstr($r, 0, sprintf("%-20.20s", $mach));
271 if ($jcnt->{'failed'}) {
272 $C->bkgdset(COLOR_PAIR(4));
273 $C->addstr(sprintf("%3dE ", $jcnt->{'failed'}));
279 $C->addstr(sprintf("%3dD %3dW", $jcnt->{'done'}, $jcnt->{'ready'}));
280 if ($stat eq 'DONE') {
281 if (defined $host_last_fail_stat{$mach}) {
282 $C->bkgdset(COLOR_PAIR(4));
283 $C->addstr(sprintf(" %-8s %s", $host_last_fail_stat{$mach}, $queue->job_name($host_last_fail_job{$mach})));
286 my $text = sprintf(" %-8s %s", $stat, $jname);
294 my ($ui, $mach, $jid, $stat) = @_;
295 my $s = get_slot($mach);
298 # Pseudo-state generated internally
299 $ui->set_host_status($mach, 'ready');
300 $ui->set_job_status($mach, $jid, 'ready');
303 $ui->set_job_status($mach, $jid, 'done');
305 when (['FAILED', 'INTERR', 'NOPING', 'PREPFAIL']) {
306 $ui->set_job_status($mach, $jid, 'failed');
307 $host_last_fail_job{$mach} = $jid;
308 $host_last_fail_stat{$mach} = $stat;
311 if ($job_cnt{$mach}{'failed'}) {
312 $ui->set_host_status($mach, 'failed');
314 $ui->set_host_status($mach, 'done');
318 $ui->set_host_status($mach, 'running');
319 $ui->set_job_status($mach, $jid, 'running') if defined $jid;
323 $ui->set_job_status($mach, $jid, 'failed');
325 for my $j (keys %{$job_state{$mach}}) {
326 $ui->set_job_status($mach, $jid, 'failed');
328 $ui->set_host_status($mach, 'failed');
329 $host_last_fail_job{$mach} = $jid;
330 $host_last_fail_stat{$mach} = $stat;
333 when (['START', 'PING', 'SEND', 'RUN']) {
336 $ui->err("Received unknown job status $stat");
340 $s->{'Status'} = $stat;
342 if ($stat eq 'DONE') { delete_slot($s); }