]> mj.ucw.cz Git - bex.git/blob - lib/bin/bex-prun
685c8be0b06d4e065c168fa623e785f2deeb44cf
[bex.git] / lib / bin / bex-prun
1 #!/usr/bin/perl
2 # Batch EXecutor 3.0 -- Parallel Execution Using Screen
3 # (c) 2011-2012 Martin Mares <mj@ucw.cz>
4
5 use strict;
6 use warnings;
7 use feature 'switch';
8 use Getopt::Long;
9 use POSIX;
10 use BEX;
11
12 my $queue_name;
13 my $text_mode;
14 my $debug_children;
15
16 sub usage() {
17         print <<AMEN ;
18 Usage: bex prun [<options>] [[!]<machine-or-class> ...]
19
20 Options:
21     --debug-children    Log stdout and stderr to ./debug.log
22 -p, --parallel=<n>      Set limit on the number of jobs run in parallel
23 -q, --queue=<name>      Run jobs in the given queue
24     --text              Use plain-text user interface instead of curses
25 AMEN
26         exit 0;
27 }
28
29 GetOptions(
30         "q|queue=s" => \$queue_name,
31         "text!" => \$text_mode,
32         "debug-children!" => \$debug_children,
33         "p|parallel=i" => \$BEX::Config::max_parallel_jobs,
34         "help" => \&usage,
35 ) or die "Try `bex prun --help' for more information.\n";
36
37 system 'tmux', 'has-session';
38 !$? or die "You need to start tmux first.\n";
39
40 my $queue = BEX::Queue->new($queue_name);
41 my $fifo_name = $queue->{'Path'} . '/status-fifo';
42 unlink $fifo_name;
43 mkfifo $fifo_name, 0700 or die "Cannot create $fifo_name: $!";
44 open FIFO, '+<', $fifo_name or die "Cannot open $fifo_name: $!";
45
46 my $ui = ($text_mode ? BEX::bprun::text->new : BEX::bprun::curses->new);
47
48 my @machines = ();
49 for my $mach (BEX::Config::parse_machine_list(@ARGV ? @ARGV : '*')) {
50         my @jobs = $queue->scan($mach);
51         @jobs or next;
52         push @machines, $mach;
53         for (@jobs) { $ui->update($mach, $_, 'READY'); }
54 }
55
56 my %running = ();
57 my $max = $BEX::Config::max_parallel_jobs;
58
59 while (keys %running || @machines) {
60         if (@machines && keys %running < $max) {
61                 my $mach = shift @machines;
62                 $ui->update($mach, undef, 'START');
63                 my @tm = ('tmux', 'new-window', '-n', $mach, '-d');
64                 my $P5LIB = $ENV{"PERL5LIB"} // "";
65                 my @cmd = (
66                         "BEX_HOME='$BEX::Config::home'",
67                         "BEX_LIB='$BEX::Config::lib'",
68                         "PERL5LIB='$P5LIB'",
69                         "$BEX::Config::lib/bin/bex-run",
70                         "--status-fifo=$fifo_name",
71                         "--queue=" . $queue->{'Name'},
72                         $mach,
73                         );
74                 push @cmd, ">debug.log", "2>&1" if $debug_children;
75                 push @tm, join(" ", @cmd);
76                 system @tm;
77                 !$? or $ui->update($mach, undef, 'INTERR');
78                 $running{$mach} = 'START';
79                 next;
80         }
81         $_ = <FIFO>;
82         chomp;
83         my ($mach, $jid, $stat) = /^! (\S+) (\S+) (\S+)$/;
84         if (!defined $stat) {
85                 $ui->err("Received invalid status message <$_>");
86                 next;
87         }
88         if (!defined $running{$mach}) {
89                 $ui->err("Received status message <$_> for a machine which does not run");
90                 next;
91         }
92         $running{$mach} = $stat;
93         $ui->update($mach, ($jid eq '-' ? undef : $jid), $stat);
94         if ($stat eq 'DONE') {
95                 delete $running{$mach};
96         }
97 }
98
99 close FIFO;
100 unlink $fifo_name;
101 $ui->done;
102
103 package BEX::bprun::text;
104
105 sub new($) {
106         return bless {};
107 }
108
109 sub done($) {
110 }
111
112 sub update($$$$) {
113         my ($ui, $mach, $jid, $stat) = @_;
114         print +($mach // '-'), (defined($jid) ? ":$jid" : ""), " $stat\n";
115 }
116
117 sub err($$) {
118         my ($ui, $msg) = @_;
119         print STDERR "ERROR: $msg\n";
120 }
121
122 package BEX::bprun::curses;
123
124 use Curses;
125
126 my $C;
127
128 my $nrows;
129 my @by_row = ();
130 my %by_host = ();
131
132 my %host_state;
133 my %host_cnt;
134
135 my %job_state;
136 my %job_cnt;
137
138 my %host_last_fail_job;
139 my %host_last_fail_stat;
140
141 sub new($) {
142         $C = new Curses;
143         start_color;
144         has_colors && COLORS >= 8 && COLOR_PAIRS >= 8 or die "Your terminal is too dumb for me\n";
145         cbreak; noecho;
146         $C->intrflush(0);
147         $C->keypad(1);
148         $C->meta(1);
149         $C->clear;
150         init_pair(1, COLOR_YELLOW, COLOR_BLUE);
151         init_pair(2, COLOR_YELLOW, COLOR_RED);
152         init_pair(3, COLOR_YELLOW, COLOR_BLACK);
153         init_pair(4, COLOR_RED, COLOR_BLACK);
154         init_pair(5, COLOR_BLUE, COLOR_BLACK);
155
156         $nrows = $C->getmaxy - 2;
157         if ($BEX::Config::max_parallel_jobs > $nrows) {
158                 $BEX::Config::max_parallel_jobs = $nrows;
159         }
160
161         %host_state = %host_cnt = ();
162         %job_state = %job_cnt = ();
163         for my $s ('unknown', 'ready', 'running', 'done', 'failed') {
164                 $host_cnt{$s} = 0;
165                 $job_cnt{'*'}{$s} = 0;
166         }
167
168         my $ui = bless {};
169         $ui->refresh_status;
170         return $ui;
171 }
172
173 sub done($)
174 {
175         $C->bkgdset(COLOR_PAIR(1) | A_BOLD);
176         $C->addstr($C->getmaxy-1, 0, "Press any key to quit...");
177         $C->clrtoeol;
178         $C->getch;
179         endwin;
180 }
181
182 sub err($$) {
183         my ($ui, $msg) = @_;
184         $C->bkgdset(COLOR_PAIR(2) | A_BOLD);
185         $C->addnstr($C->getmaxy-1, 0, "ERROR: $msg", $C->getmaxx);
186         $C->clrtoeol;
187         $C->refresh;
188 }
189
190 sub set_host_status($$$) {
191         my ($ui, $mach, $stat) = @_;
192         my $prev_stat = $host_state{$mach};
193         if (defined $prev_stat) {
194                 $host_cnt{$prev_stat}--;
195         } else {
196                 for my $s ('unknown', 'ready', 'running', 'done', 'failed') { $job_cnt{$mach}{$s} = 0; }
197         }
198         $host_state{$mach} = $stat;
199         $host_cnt{$stat}++;
200 }
201
202 sub set_job_status($$$$) {
203         my ($ui, $mach, $jid, $stat) = @_;
204         my $prev_stat = $job_state{$mach}{$jid} // 'unknown';
205         $job_cnt{$mach}{$prev_stat}--;
206         $job_cnt{'*'}{$prev_stat}--;
207         $job_state{$mach}{$jid} = $stat;
208         $job_cnt{$mach}{$stat}++;
209         $job_cnt{'*'}{$stat}++;
210 }
211
212 sub refresh_status($) {
213         $C->bkgdset(COLOR_PAIR(1) | A_BOLD);
214         $C->addnstr(0, 0,
215                 sprintf("BEX  Hosts: %dR %dD %dE %dW  Jobs: %dR %dD %dE %dW",
216                         $host_cnt{'running'},
217                         $host_cnt{'done'},
218                         $host_cnt{'failed'},
219                         $host_cnt{'ready'},
220                         $job_cnt{'*'}{'running'},
221                         $job_cnt{'*'}{'done'},
222                         $job_cnt{'*'}{'failed'},
223                         $job_cnt{'*'}{'ready'},
224                 ), $C->getmaxx);
225         $C->clrtoeol;
226         $C->refresh;
227 }
228
229 sub get_slot($) {
230         my ($mach) = @_;
231         my $s;
232         if (defined ($s = $by_host{$mach})) {
233                 delete $s->{'Gone'};
234         } else {
235                 my ($best, $besti);
236                 for my $i (0..$nrows-1) {
237                         my $r = $by_row[$i];
238                         if (!defined $r) {
239                                 $besti = $i;
240                                 $best = undef;
241                                 last;
242                         } elsif ($r->{'Gone'} && (!$best || $best->{'Gone'} > $r->{'Gone'})) {
243                                 $besti = $i;
244                                 $best = $r;
245                         }
246                 }
247                 if ($best) {
248                         delete $by_host{$best->{'Host'}};
249                 }
250                 $s->{'Host'} = $mach;
251                 $s->{'Row'} = $besti;
252                 $by_host{$mach} = $s;
253                 $by_row[$besti] = $s;
254         }
255         return $s;
256 }
257
258 my $gone_counter = 1;
259 sub delete_slot($) {
260         my ($s) = @_;
261         $s->{'Gone'} = $gone_counter++;
262 }
263
264 sub redraw_slot($) {
265         my ($s) = @_;
266         my $mach = $s->{'Host'};
267         my $stat = $s->{'Status'} // "?";
268         my $jid = $s->{'Job'} // "";
269         my $jname = ($jid eq "" ? "" : $queue->job_name($jid));
270         my $jcnt = $job_cnt{$mach};
271         if ($jcnt->{'running'}) {
272                 if ($jcnt->{'failed'}) {
273                         $C->bkgdset(COLOR_PAIR(4) | A_BOLD);
274                 } else {
275                         $C->bkgdset(COLOR_PAIR(3) | A_BOLD);
276                 }
277         } else {
278                 if ($jcnt->{'failed'}) {
279                         $C->bkgdset(COLOR_PAIR(4));
280                 } else {
281                         $C->bkgdset(0);
282                 }
283         }
284         my $r = $s->{'Row'} + 1;
285         $C->addstr($r, 0, sprintf("%-20.20s", $mach));
286         if ($jcnt->{'failed'}) {
287                 $C->bkgdset(COLOR_PAIR(4));
288                 $C->addstr(sprintf("%3dE ", $jcnt->{'failed'}));
289         } else {
290                 $C->bkgdset(0);
291                 $C->addstr("     ");
292         }
293         $C->bkgdset(0);
294         $C->addstr(sprintf("%3dD %3dW", $jcnt->{'done'}, $jcnt->{'ready'}));
295         if ($stat eq 'DONE') {
296                 if (defined $host_last_fail_stat{$mach}) {
297                         $C->bkgdset(($host_last_fail_stat{$mach} eq 'NOPING') ? COLOR_PAIR(5) : COLOR_PAIR(4));
298                         $C->addstr(sprintf("  %-8s %s", $host_last_fail_stat{$mach}, $queue->job_name($host_last_fail_job{$mach})));
299                 }
300         } else {
301                 my $text = sprintf("  %-8s %s", $stat, $jname);
302                 $C->addstr($text);
303         }
304         $C->clrtoeol;
305         $C->refresh;
306 }
307
308 sub update($$$$) {
309         my ($ui, $mach, $jid, $stat) = @_;
310         my $s = get_slot($mach);
311         given ($stat) {
312                 when ('READY') {
313                         # Pseudo-state generated internally
314                         $ui->set_host_status($mach, 'ready');
315                         $ui->set_job_status($mach, $jid, 'ready');
316                 }
317                 when ('OK') {
318                         $ui->set_job_status($mach, $jid, 'done');
319                 }
320                 when (['FAILED', 'INTERR', 'NOPING', 'PREPFAIL', 'NOXFER']) {
321                         $ui->set_job_status($mach, $jid, 'failed');
322                         $host_last_fail_job{$mach} = $jid;
323                         $host_last_fail_stat{$mach} = $stat;
324                 }
325                 when ('DONE') {
326                         if ($job_cnt{$mach}{'failed'}) {
327                                 $ui->set_host_status($mach, 'failed');
328                         } else {
329                                 $ui->set_host_status($mach, 'done');
330                         }
331                 }
332                 when ('INIT') {
333                         $ui->set_host_status($mach, 'running');
334                         $ui->set_job_status($mach, $jid, 'running') if defined $jid;
335                 }
336                 when ('LOCKED') {
337                         if (defined $jid) {
338                                 $ui->set_job_status($mach, $jid, 'failed');
339                         } else {
340                                 for my $j (keys %{$job_state{$mach}}) {
341                                         $ui->set_job_status($mach, $jid, 'failed');
342                                 }
343                                 $ui->set_host_status($mach, 'failed');
344                                 $host_last_fail_job{$mach} = $jid;
345                                 $host_last_fail_stat{$mach} = $stat;
346                         }
347                 }
348                 when (['START', 'PING', 'SEND', 'RUN']) {
349                 }
350                 default {
351                         $ui->err("Received unknown job status $stat");
352                 }
353         }
354         $s->{'Job'} = $jid;
355         $s->{'Status'} = $stat;
356         redraw_slot($s);
357         if ($stat eq 'DONE') { delete_slot($s); }
358         $ui->refresh_status;
359 }