/[debconf]/trunk/src/debconf/debconf-apt-progress
ViewVC logotype

Contents of /trunk/src/debconf/debconf-apt-progress

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2349 - (show annotations) (download)
Fri Jul 3 13:09:34 2009 UTC (3 years, 10 months ago) by cjwatson
File size: 18117 byte(s)
debconf-apt-progress: Handle cancellation right at the end. We don't
have a process to kill at this point, but we should at least return the
correct exit code.
1 #!/usr/bin/perl -w
2
3 =head1 NAME
4
5 debconf-apt-progress - install packages using debconf to display a progress bar
6
7 =head1 SYNOPSIS
8
9 debconf-apt-progress [--] command [args ...]
10 debconf-apt-progress --config
11 debconf-apt-progress --start
12 debconf-apt-progress --from waypoint --to waypoint [--] command [args ...]
13 debconf-apt-progress --stop
14
15 =head1 DESCRIPTION
16
17 B<debconf-apt-progress> installs packages using debconf to display a
18 progress bar. The given I<command> should be any command-line apt frontend;
19 specifically, it must send progress information to the file descriptor
20 selected by the C<APT::Status-Fd> configuration option, and must keep the
21 file descriptors nominated by the C<APT::Keep-Fds> configuration option open
22 when invoking debconf (directly or indirectly), as those file descriptors
23 will be used for the debconf passthrough protocol.
24
25 The arguments to the command you supply should generally include B<-y> (for
26 B<apt-get> or B<aptitude>) or similar to avoid the apt frontend prompting
27 for input. B<debconf-apt-progress> cannot do this itself because the
28 appropriate argument may differ between apt frontends.
29
30 The B<--start>, B<--stop>, B<--from>, and B<--to> options may be used to
31 create a progress bar with multiple segments for different stages of
32 installation, provided that the caller is a debconf confmodule. The caller
33 may also interact with the progress bar itself using the debconf protocol if
34 it so desires.
35
36 debconf locks its config database when it starts up, which makes it
37 unfortunately inconvenient to have one instance of debconf displaying the
38 progress bar and another passing through questions from packages being
39 installed. If you're using a multiple-segment progress bar, you'll need to
40 eval the output of the B<--config> option before starting the debconf
41 frontend to work around this. See L<the EXAMPLES section/EXAMPLES> below.
42
43 =head1 OPTIONS
44
45 =over 4
46
47 =item B<--config>
48
49 Print environment variables necessary to start up a progress bar frontend.
50
51 =item B<--start>
52
53 Start up a progress bar, running from 0 to 100 by default. Use B<--from> and
54 B<--to> to use other endpoints.
55
56 =item B<--from> I<waypoint>
57
58 If used with B<--start>, make the progress bar begin at I<waypoint> rather
59 than 0.
60
61 Otherwise, install packages with their progress bar beginning at this
62 "waypoint". Must be used with B<--to>.
63
64 =item B<--to> I<waypoint>
65
66 If used with B<--start>, make the progress bar end at I<waypoint> rather
67 than 100.
68
69 Otherwise, install packages with their progress bar ending at this
70 "waypoint". Must be used with B<--from>.
71
72 =item B<--stop>
73
74 Stop a running progress bar.
75
76 =item B<--no-progress>
77
78 Avoid starting, stopping, or stepping the progress bar. Progress
79 messages from apt, media change events, and debconf questions will still
80 be passed through to debconf.
81
82 =item B<--dlwaypoint> I<percentage>
83
84 Specify what percent of the progress bar to use for downloading packages.
85 The remainder will be used for installing packages. The default is to use
86 15% for downloading and the remaining 85% for installing.
87
88 =item B<--logfile> I<file>
89
90 Send the normal output from apt to the given file.
91
92 =item B<--logstderr>
93
94 Send the normal output from apt to stderr. If you supply neither
95 B<--logfile> nor B<--logstderr>, the normal output from apt will be
96 discarded.
97
98 =item B<-->
99
100 Terminate options. Since you will normally need to give at least the B<-y>
101 argument to the command being run, you will usually need to use B<--> to
102 prevent that being interpreted as an option to B<debconf-apt-progress>
103 itself.
104
105 =back
106
107 =head1 EXAMPLES
108
109 Install the GNOME desktop and an X window system development environment
110 within a progress bar:
111
112 debconf-apt-progress -- aptitude -y install gnome x-window-system-dev
113
114 Install the GNOME, KDE, and XFCE desktops within a single progress bar,
115 allocating 45% of the progress bar for each of GNOME and KDE and the
116 remaining 10% for XFCE:
117
118 #! /bin/sh
119 set -e
120 case $1 in
121 '')
122 eval "$(debconf-apt-progress --config)"
123 "$0" debconf
124 ;;
125 debconf)
126 . /usr/share/debconf/confmodule
127 debconf-apt-progress --start
128 debconf-apt-progress --from 0 --to 45 -- apt-get -y install gnome
129 debconf-apt-progress --from 45 --to 90 -- apt-get -y install kde
130 debconf-apt-progress --from 90 --to 100 -- apt-get -y install xfce4
131 debconf-apt-progress --stop
132 ;;
133 esac
134
135 =head1 RETURN CODE
136
137 The exit code of the specified command is returned, unless the user hit the
138 cancel button on the progress bar. If the cancel button was hit, a value of
139 30 is returned. To avoid ambiguity, if the command returned 30, a value of
140 3 will be returned.
141
142 =cut
143
144 use strict;
145 use POSIX;
146 use Fcntl;
147 use Getopt::Long;
148 # Avoid starting the debconf frontend just yet.
149 use Debconf::Client::ConfModule ();
150
151 my ($config, $start, $from, $to, $stop);
152 my $progress=1;
153 my $dlwaypoint=15;
154 my ($logfile, $logstderr);
155 my $had_frontend;
156
157 sub checkopen (@) {
158 my $file = $_[0];
159 my $fd = POSIX::open($file, &POSIX::O_RDONLY);
160 defined $fd or die "$0: can't open $_[0]: $!\n";
161 return $fd;
162 }
163
164 sub checkclose ($) {
165 my $fd = $_[0];
166 unless (POSIX::close($fd)) {
167 return if $! == &POSIX::EBADF;
168 die "$0: can't close fd $fd: $!\n";
169 }
170 }
171
172 sub checkdup2 ($$) {
173 my ($oldfd, $newfd) = @_;
174 checkclose($newfd);
175 POSIX::dup2($oldfd, $newfd)
176 or die "$0: can't dup fd $oldfd to $newfd: $!\n";
177 }
178
179 sub nocloexec (*) {
180 my $fh = shift;
181 my $flags = fcntl($fh, F_GETFD, 0);
182 fcntl($fh, F_SETFD, $flags & ~FD_CLOEXEC);
183 }
184
185 sub nonblock (*) {
186 my $fh = shift;
187 my $flags = fcntl($fh, F_GETFL, 0);
188 fcntl($fh, F_SETFL, $flags | O_NONBLOCK);
189 }
190
191 # Open the given file descriptors to make sure they won't accidentally be
192 # used by Perl, leading to confusion.
193 sub reservefds (@) {
194 my $null = checkopen('/dev/null');
195 my $close = 1;
196 for my $fd (@_) {
197 if ($null == $fd) {
198 $close = 0;
199 } else {
200 checkclose($fd);
201 checkdup2($null, $fd);
202 }
203 }
204 if ($close) {
205 checkclose($null);
206 }
207 }
208
209 # Does this environment variable exist, and is it non-empty?
210 sub envnonempty ($) {
211 my $name = shift;
212 return (exists $ENV{$name} and $ENV{$name} ne '');
213 }
214
215 sub start_debconf (@) {
216 if (! $ENV{DEBIAN_HAS_FRONTEND}) {
217 # Save existing environment variables.
218 if (envnonempty('DEBCONF_DB_REPLACE')) {
219 $ENV{DEBCONF_APT_PROGRESS_DB_REPLACE} =
220 $ENV{DEBCONF_DB_REPLACE};
221 }
222 if (envnonempty('DEBCONF_DB_OVERRIDE')) {
223 $ENV{DEBCONF_APT_PROGRESS_DB_OVERRIDE} =
224 $ENV{DEBCONF_DB_OVERRIDE};
225 }
226
227 # Make sure the main configdb is opened read-only ...
228 $ENV{DEBCONF_DB_REPLACE} = 'configdb';
229 # ... and stack a writable db on top of it, since the
230 # passthrough instance is going to be sending us db updates.
231 $ENV{DEBCONF_DB_OVERRIDE} = 'Pipe{infd:none outfd:none}';
232
233 # Leave a note for ourselves. We need to do it this way
234 # round since DEBIAN_HAS_FRONTEND will be set the second
235 # time round even if it isn't set initially.
236 $ENV{DEBCONF_APT_PROGRESS_NO_FRONTEND} = 1;
237
238 # Restore @ARGV so that
239 # Debconf::Client::ConfModule::import() can use it.
240 @ARGV = @_;
241 }
242
243 import Debconf::Client::ConfModule;
244 }
245
246 sub passthrough (@) {
247 my $priority = Debconf::Client::ConfModule::get('debconf/priority');
248
249 defined(my $pid = fork) or die "$0: can't fork: $!\n";
250 if (!$pid) {
251 close STATUS_READ;
252 close COMMAND_WRITE;
253 close DEBCONF_COMMAND_READ;
254 close DEBCONF_REPLY_WRITE;
255 $^F = 6; # avoid close-on-exec
256 if (fileno(COMMAND_READ) != 0) {
257 checkdup2(fileno(COMMAND_READ), 0);
258 close COMMAND_READ;
259 }
260 if (fileno(APT_LOG) != 1) {
261 checkclose(1);
262 checkdup2(fileno(APT_LOG), 1);
263 }
264 if (fileno(APT_LOG) != 2) {
265 checkclose(2);
266 checkdup2(fileno(APT_LOG), 2);
267 }
268 close APT_LOG;
269 delete $ENV{DEBIAN_HAS_FRONTEND};
270 delete $ENV{DEBCONF_REDIR};
271 delete $ENV{DEBCONF_SYSTEMRC};
272 delete $ENV{DEBCONF_PIPE}; # just in case ...
273 $ENV{DEBIAN_FRONTEND} = 'passthrough';
274 $ENV{DEBIAN_PRIORITY} = $priority;
275 $ENV{DEBCONF_READFD} = 5;
276 $ENV{DEBCONF_WRITEFD} = 6;
277 $ENV{APT_LISTCHANGES_FRONTEND} = 'none';
278 # If we already had a debconf frontend when we started, then
279 # the passthrough child needs to use the same pipe-database
280 # trick as we do. See start_debconf.
281 if ($had_frontend) {
282 $ENV{DEBCONF_DB_REPLACE} = 'configdb';
283 $ENV{DEBCONF_DB_OVERRIDE} = 'Pipe{infd:none outfd:none}';
284 }
285 exec @_;
286 }
287
288 close STATUS_WRITE;
289 close COMMAND_READ;
290 close DEBCONF_COMMAND_WRITE;
291 close DEBCONF_REPLY_READ;
292 return $pid;
293 }
294
295 sub handle_status ($$$) {
296 my ($from, $to, $line) = @_;
297 my ($status, $pkg, $percent, $description) = split ':', $line, 4;
298
299 my ($min, $len);
300 if ($status eq 'dlstatus') {
301 $min = 0;
302 $len = $dlwaypoint;
303 }
304 elsif ($status eq 'pmstatus') {
305 $min = $dlwaypoint;
306 $len = 100 - $dlwaypoint;
307 }
308 elsif ($status eq 'media-change') {
309 Debconf::Client::ConfModule::subst(
310 'debconf-apt-progress/media-change', 'MESSAGE',
311 $description);
312 my @ret = Debconf::Client::ConfModule::input(
313 'critical', 'debconf-apt-progress/media-change');
314 $ret[0] == 0 or die "Can't display media change request!\n";
315 Debconf::Client::ConfModule::go();
316 print COMMAND_WRITE "\n" || die "can't talk to command fd: $!";
317 return;
318 }
319 else {
320 return;
321 }
322
323 $percent = ($percent * $len / 100 + $min);
324 $percent = ($percent * ($to - $from) / 100 + $from);
325 $percent =~ s/\..*//;
326 if ($progress) {
327 my @ret=Debconf::Client::ConfModule::progress('SET', $percent);
328 if ($ret[0] eq '30') {
329 cancel();
330 }
331 }
332 Debconf::Client::ConfModule::subst(
333 'debconf-apt-progress/info', 'DESCRIPTION', $description);
334 my @ret=Debconf::Client::ConfModule::progress(
335 'INFO', 'debconf-apt-progress/info');
336 if ($ret[0] eq '30') {
337 cancel();
338 }
339 }
340
341 sub handle_debconf_command ($) {
342 my $line = shift;
343
344 # Debconf::Client::ConfModule has already dealt with checking
345 # DEBCONF_REDIR.
346 print "$line\n" || die "can't write to stdout: $!";
347 my $ret = <STDIN>;
348 chomp $ret;
349 print DEBCONF_REPLY_WRITE "$ret\n" ||
350 die "can't write to DEBCONF_REPLY_WRITE: $!";
351 }
352
353 my $pid;
354 sub run_progress ($$@) {
355 my $from = shift;
356 my $to = shift;
357 my $command = shift;
358 local (*STATUS_READ, *STATUS_WRITE);
359 local (*COMMAND_READ, *COMMAND_WRITE);
360 local (*DEBCONF_COMMAND_READ, *DEBCONF_COMMAND_WRITE);
361 local (*DEBCONF_REPLY_READ, *DEBCONF_REPLY_WRITE);
362 local *APT_LOG;
363 use IO::Handle;
364
365 if ($progress) {
366 my @ret=Debconf::Client::ConfModule::progress(
367 'INFO', 'debconf-apt-progress/preparing');
368 if ($ret[0] eq '30') {
369 cancel();
370 return 30;
371 }
372 }
373
374 reservefds(4, 5, 6);
375
376 pipe STATUS_READ, STATUS_WRITE
377 or die "$0: can't create status pipe: $!";
378 nonblock(\*STATUS_READ);
379 checkdup2(fileno(STATUS_WRITE), 4);
380 open STATUS_WRITE, '>&=4'
381 or die "$0: can't reopen STATUS_WRITE as fd 4: $!";
382 nocloexec(\*STATUS_WRITE);
383
384 pipe COMMAND_READ, COMMAND_WRITE
385 or die "$0: can't create command pipe: $!";
386 nocloexec(\*COMMAND_READ);
387 COMMAND_WRITE->autoflush(1);
388
389 pipe DEBCONF_COMMAND_READ, DEBCONF_COMMAND_WRITE
390 or die "$0: can't create debconf command pipe: $!";
391 nonblock(\*DEBCONF_COMMAND_READ);
392 checkdup2(fileno(DEBCONF_COMMAND_WRITE), 6);
393 open DEBCONF_COMMAND_WRITE, '>&=6'
394 or die "$0: can't reopen DEBCONF_COMMAND_WRITE as fd 6: $!";
395 nocloexec(\*DEBCONF_COMMAND_WRITE);
396
397 pipe DEBCONF_REPLY_READ, DEBCONF_REPLY_WRITE
398 or die "$0: can't create debconf reply pipe: $!";
399 checkdup2(fileno(DEBCONF_REPLY_READ), 5);
400 open DEBCONF_REPLY_READ, '<&=5'
401 or die "$0: can't reopen DEBCONF_REPLY_READ as fd 5: $!";
402 nocloexec(\*DEBCONF_REPLY_READ);
403 DEBCONF_REPLY_WRITE->autoflush(1);
404
405 if (defined $logfile) {
406 open APT_LOG, '>>', $logfile
407 or die "$0: can't open $logfile: $!";
408 } elsif ($logstderr) {
409 open APT_LOG, '>&STDERR'
410 or die "$0: can't duplicate stderr: $!";
411 } else {
412 open APT_LOG, '>', '/dev/null'
413 or die "$0: can't open /dev/null: $!";
414 }
415 nocloexec(\*APT_LOG);
416
417 $pid = passthrough $command,
418 '-o', 'APT::Status-Fd=4',
419 '-o', 'APT::Keep-Fds::=5',
420 '-o', 'APT::Keep-Fds::=6',
421 @_;
422
423 my $status_eof = 0;
424 my $debconf_command_eof = 0;
425 my $status_buf = '';
426 my $debconf_command_buf = '';
427
428 # STATUS_READ should be the last fd to close. DEBCONF_COMMAND_WRITE
429 # may end up captured by buggy daemons, so terminate the loop even
430 # if we haven't hit $debconf_command_eof.
431 while (not $status_eof) {
432 my $rin = '';
433 my $rout;
434 vec($rin, fileno(STATUS_READ), 1) = 1;
435 vec($rin, fileno(DEBCONF_COMMAND_READ), 1) = 1
436 unless $debconf_command_eof;
437 my $sel = select($rout = $rin, undef, undef, undef);
438 if ($sel < 0) {
439 next if $! == &POSIX::EINTR;
440 die "$0: select failed: $!";
441 }
442
443 if (vec($rout, fileno(STATUS_READ), 1) == 1) {
444 # Status message from apt. Transform into debconf
445 # messages.
446 while (1) {
447 my $r = sysread(STATUS_READ, $status_buf, 4096,
448 length $status_buf);
449 if (not defined $r) {
450 next if $! == &POSIX::EINTR;
451 last if $! == &POSIX::EAGAIN or
452 $! == &POSIX::EWOULDBLOCK;
453 die "$0: read STATUS_READ failed: $!";
454 }
455 elsif ($r == 0) {
456 if ($status_buf ne '' and
457 $status_buf !~ /\n$/) {
458 $status_buf .= "\n";
459 }
460 $status_eof = 1;
461 last;
462 }
463 last if $status_buf =~ /\n/;
464 }
465
466 while ($status_buf =~ /\n/) {
467 my $status_line;
468 ($status_line, $status_buf) =
469 split /\n/, $status_buf, 2;
470 handle_status $from, $to, $status_line;
471 }
472 }
473
474 if (vec($rout, fileno(DEBCONF_COMMAND_READ), 1) == 1) {
475 # Debconf command. Pass straight through.
476 while (1) {
477 my $r = sysread(DEBCONF_COMMAND_READ,
478 $debconf_command_buf, 4096,
479 length $debconf_command_buf);
480 if (not defined $r) {
481 next if $! == &POSIX::EINTR;
482 last if $! == &POSIX::EAGAIN or
483 $! == &POSIX::EWOULDBLOCK;
484 die "$0: read DEBCONF_COMMAND_READ " .
485 "failed: $!";
486 }
487 elsif ($r == 0) {
488 if ($debconf_command_buf ne '' and
489 $debconf_command_buf !~ /\n$/) {
490 $debconf_command_buf .= "\n";
491 }
492 $debconf_command_eof = 1;
493 last;
494 }
495 last if $debconf_command_buf =~ /\n/;
496 }
497
498 while ($debconf_command_buf =~ /\n/) {
499 my $debconf_command_line;
500 ($debconf_command_line, $debconf_command_buf) =
501 split /\n/, $debconf_command_buf, 2;
502 handle_debconf_command $debconf_command_line;
503 }
504 }
505 }
506
507 waitpid $pid, 0;
508 undef $pid;
509 my $status = $?;
510
511 # make sure that the progress bar always gets to the end
512 if ($progress) {
513 my @ret=Debconf::Client::ConfModule::progress('SET', $to);
514 if ($ret[0] eq '30') {
515 cancel();
516 }
517 }
518
519 if ($status & 127) {
520 return 127;
521 }
522
523 return ($status >> 8);
524 }
525
526 # Called if the progress bar is cancelled. Starts with a SIGINT but
527 # if called repeatedly, falls back to SIGKILL.
528 my $cancelled=0;
529 my $cancel_sent_signal=0;
530 sub cancel () {
531 $cancelled++;
532 if (defined $pid) {
533 $cancel_sent_signal++;
534 if ($cancel_sent_signal == 1) {
535 kill INT => $pid;
536 }
537 else {
538 kill KILL => $pid;
539 }
540 }
541 }
542
543 sub start_bar ($$) {
544 my ($from, $to) = @_;
545 if ($progress) {
546 Debconf::Client::ConfModule::progress(
547 'START', $from, $to, 'debconf-apt-progress/title');
548 my @ret=Debconf::Client::ConfModule::progress(
549 'INFO', 'debconf-apt-progress/preparing');
550 if ($ret[0] eq '30') {
551 cancel();
552 }
553 }
554 }
555
556 sub stop_bar () {
557 Debconf::Client::ConfModule::progress('STOP') if $progress;
558 # If we don't stop, we leave a zombie in case some daemon fails to
559 # disconnect from fd 3. Don't do this if debconf was already
560 # running, though, since in that case we're running as part of a
561 # larger application which will need to take its own care to stop
562 # when it's finished.
563 Debconf::Client::ConfModule::stop() unless $had_frontend;
564 }
565
566 # Restore saved environment variables.
567 if (envnonempty('DEBCONF_APT_PROGRESS_DB_REPLACE')) {
568 $ENV{DEBCONF_DB_REPLACE} = $ENV{DEBCONF_APT_PROGRESS_DB_REPLACE};
569 } else {
570 delete $ENV{DEBCONF_DB_REPLACE};
571 }
572 if (envnonempty('DEBCONF_APT_PROGRESS_DB_OVERRIDE')) {
573 $ENV{DEBCONF_DB_OVERRIDE} = $ENV{DEBCONF_APT_PROGRESS_DB_OVERRIDE};
574 } else {
575 delete $ENV{DEBCONF_DB_OVERRIDE};
576 }
577 $had_frontend = 1 unless $ENV{DEBCONF_APT_PROGRESS_NO_FRONTEND};
578 delete $ENV{DEBCONF_APT_PROGRESS_NO_FRONTEND}; # avoid inheritance
579
580 my @saved_argv = @ARGV;
581
582 my $result = GetOptions('config' => \$config,
583 'start' => \$start,
584 'from=i' => \$from,
585 'to=i' => \$to,
586 'stop' => \$stop,
587 'logfile=s' => \$logfile,
588 'logstderr' => \$logstderr,
589 'progress!' => \$progress,
590 'dlwaypoint=i' => \$dlwaypoint,
591 );
592
593 if (! $progress && ($start || $from || $to || $stop)) {
594 die "--no-progress cannot be used with --start, --from, --to, or --stop\n";
595 }
596
597 unless ($start) {
598 if (defined $from and not defined $to) {
599 die "$0: --from requires --to\n";
600 } elsif (defined $to and not defined $from) {
601 die "$0: --to requires --from\n";
602 }
603 }
604
605 my $mutex = 0;
606 ++$mutex if $config;
607 ++$mutex if $start;
608 ++$mutex if $stop;
609 if ($mutex > 1) {
610 die "$0: must use only one of --config, --start, or --stop\n";
611 }
612
613 if (($config or $stop) and (defined $from or defined $to)) {
614 die "$0: cannot use --from or --to with --config or --stop\n";
615 }
616
617 start_debconf(@saved_argv) unless $config;
618
619 my $status = 0;
620
621 if ($config) {
622 print <<'EOF';
623 DEBCONF_APT_PROGRESS_DB_REPLACE="$DEBCONF_DB_REPLACE"
624 DEBCONF_APT_PROGRESS_DB_OVERRIDE="$DEBCONF_DB_OVERRIDE"
625 export DEBCONF_APT_PROGRESS_DB_REPLACE DEBCONF_APT_PROGRESS_DB_OVERRIDE
626 DEBCONF_DB_REPLACE=configdb
627 DEBCONF_DB_OVERRIDE='Pipe{infd:none outfd:none}'
628 export DEBCONF_DB_REPLACE DEBCONF_DB_OVERRIDE
629 EOF
630 } elsif ($start) {
631 $from = 0 unless defined $from;
632 $to = 100 unless defined $to;
633 start_bar($from, $to);
634 } elsif (defined $from) {
635 $status = run_progress($from, $to, @ARGV);
636 } elsif ($stop) {
637 stop_bar();
638 } else {
639 start_bar(0, 100);
640 if (! $cancelled) {
641 $status = run_progress(0, 100, @ARGV);
642 stop_bar();
643 }
644 }
645
646 if ($cancelled) {
647 # This is pure paranoia. What if the child was in the
648 # middle of writing a debconf command out, only to be
649 # interrupted with a truncated write? Let's send a no-op
650 # command to finish it out just in case.
651 Debconf::Client::ConfModule::get("debconf/priority");
652
653 exit 30;
654 }
655 elsif ($status == 30) {
656 exit 3;
657 }
658 else {
659 exit $status;
660 }
661
662 =head1 AUTHORS
663
664 Colin Watson <cjwatson@debian.org>
665
666 Joey Hess <joeyh@debian.org>
667
668 =cut

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.5