#!/usr/bin/perl -w =head1 NAME debconf-apt-progress - install packages using debconf to display a progress bar =head1 SYNOPSIS debconf-apt-progress [--] command [args ...] debconf-apt-progress --config debconf-apt-progress --start debconf-apt-progress --from waypoint --to waypoint [--] command [args ...] debconf-apt-progress --stop =head1 DESCRIPTION B installs packages using debconf to display a progress bar. The given I should be any command-line apt frontend; specifically, it must send progress information to the file descriptor selected by the C configuration option, and must keep the file descriptors nominated by the C configuration option open when invoking debconf (directly or indirectly), as those file descriptors will be used for the debconf passthrough protocol. The arguments to the command you supply should generally include B<-y> (for B or B) or similar to avoid the apt frontend prompting for input. B cannot do this itself because the appropriate argument may differ between apt frontends. The B<--start>, B<--stop>, B<--from>, and B<--to> options may be used to create a progress bar with multiple segments for different stages of installation, provided that the caller is a debconf confmodule. Use B<--start> to start up the progress bar (running from 0 to 100 by default), B<--from> and B<--to> to install packages with their progress bar bounded by the given beginning and ending "waypoints", and B<--stop> to stop the progress bar when installation is complete. The caller may also interact with the progress bar itself using the debconf protocol if it so desires. debconf locks its config database when it starts up, which makes it unfortunately inconvenient to have one instance of debconf displaying the progress bar and another passing through questions from packages being installed. To work around this, you'll need to use the B<--config> option, which sets up separate databases and outputs the path to a configuration file which you can put in the C environment variable. See L below. Use the B<--logfile> option to send the normal output from apt to a file. =head1 EXAMPLES Install the GNOME desktop and an X window system development environment within a progress bar: debconf-apt-progress -- aptitude -y install gnome x-window-system-dev Install the GNOME, KDE, and XFCE desktops within a single progress bar, allocating 45% of the progress bar for each of GNOME and KDE and the remaining 10% for XFCE: #! /bin/sh set -e case $1 in '') export DEBCONF_SYSTEMRC="$(debconf-apt-progress --config)" trap 'rm -rf "$(dirname "$DEBCONF_SYSTEMRC")"' EXIT HUP INT QUIT TERM "$0" debconf ;; debconf) . /usr/share/debconf/confmodule debconf-apt-progress --start debconf-apt-progress --from 0 --to 45 -- apt-get -y install gnome debconf-apt-progress --from 45 --to 90 -- apt-get -y install kde debconf-apt-progress --from 90 --to 100 -- apt-get -y install xfce4 debconf-apt-progress --stop ;; esac =cut use strict; use POSIX; use Fcntl; use Getopt::Long; use Debconf::Client::ConfModule (); my ($config, $start, $from, $to, $stop); my $logfile; my $tempdir; my $cleanup_tempdir = 0; if (defined $ENV{DEBCONF_APT_PROGRESS_TEMPDIR}) { $tempdir = $ENV{DEBCONF_APT_PROGRESS_TEMPDIR}; $cleanup_tempdir = 1; } END { if ($cleanup_tempdir and defined $tempdir) { # File::Path is in perl-modules, so we can't use it. Boo. system('rm', '-rf', $tempdir); } } sub checkopen (@) { my $file = $_[0]; my $fd = POSIX::open($file, &POSIX::O_RDONLY); defined $fd or die "$0: can't open $_[0]: $!\n"; return $fd; } sub checkclose ($) { my $fd = $_[0]; unless (POSIX::close($fd)) { return if $! == &POSIX::EBADF; die "$0: can't close fd $fd: $!\n"; } } sub checkdup2 ($$) { my ($oldfd, $newfd) = @_; checkclose($newfd); POSIX::dup2($oldfd, $newfd) or die "$0: can't dup fd $oldfd to $newfd: $!\n"; } sub nocloexec (*) { my $fh = shift; my $flags = fcntl($fh, F_GETFD, 0); fcntl($fh, F_SETFD, $flags & ~FD_CLOEXEC); } # Open the given file descriptors to make sure they won't accidentally be # used by Perl, leading to confusion. sub reservefds (@) { my $null = checkopen('/dev/null'); my $close = 1; for my $fd (@_) { if ($null == $fd) { $close = 0; } else { checkclose($fd); checkdup2($null, $fd); } } if ($close) { checkclose($null); } } sub passthrough (@) { my $priority = Debconf::Client::ConfModule::get('debconf/priority'); defined(my $pid = fork) or die "$0: can't fork: $!\n"; if (!$pid) { close STATUS_READ; $^F = 6; # avoid close-on-exec my $null = checkopen('/dev/null'); checkdup2(0, 5); # If the shell confmodule was previously loaded, we need to # use fd 3 rather than stdout. if (exists $ENV{DEBCONF_REDIR} and $ENV{DEBCONF_REDIR}) { checkdup2(3, 6); checkclose(3); } else { checkdup2(1, 6); } if ($null != 0) { checkdup2($null, 0); checkclose($null); } if (fileno(APT_LOG) != 1) { checkclose(1); checkdup2(fileno(APT_LOG), 1); } delete $ENV{DEBIAN_HAS_FRONTEND}; delete $ENV{DEBCONF_REDIR}; delete $ENV{DEBCONF_SYSTEMRC}; $ENV{DEBIAN_FRONTEND} = 'passthrough'; $ENV{DEBIAN_PRIORITY} = $priority; $ENV{DEBCONF_READFD} = 5; $ENV{DEBCONF_WRITEFD} = 6; $ENV{APT_LISTCHANGES_FRONTEND} = 'none'; system @_; if ($? != 0) { print STATUS_WRITE "_error:$?"; } exit 0; } close STATUS_WRITE; return $pid; } sub run_progress ($$@) { my $from = shift; my $to = shift; my $command = shift; local (*STATUS_READ, *STATUS_WRITE); local *APT_LOG; reservefds(4, 5, 6); pipe STATUS_READ, STATUS_WRITE or die "$0: can't create pipe: $!"; checkdup2(fileno(STATUS_WRITE), 4); open STATUS_WRITE, '>&=4' or die "$0: can't reopen STATUS_WRITE as fd 4: $!"; nocloexec(\*STATUS_WRITE); if (defined $logfile) { open APT_LOG, '>>', $logfile or die "$0: can't open $logfile: $!"; } else { open APT_LOG, '>&STDERR' or die "$0: can't duplicate stderr: $!"; } nocloexec(\*APT_LOG); my $pid = passthrough $command, '-o', 'APT::Status-Fd=4', '-o', 'APT::Keep-Fds::=5', '-o', 'APT::Keep-Fds::=6', @_; while () { chomp; my ($status, $pkg, $percent, $description) = split ':', $_, 4; # Crude waypointing. 15% was chosen to match base-installer, # but could benefit from timing tests under various # bandwidth conditions. my ($min, $len); if ($status eq 'dlstatus') { $min = 0; $len = 15; } elsif ($status eq 'pmstatus') { $min = 15; $len = 85; } elsif ($status eq '_error') { waitpid $pid, 0; return $pkg; } else { next; } $percent = ($percent * $len / 100 + $min); $percent = ($percent * ($to - $from) / 100 + $from); $percent =~ s/\..*//; Debconf::Client::ConfModule::progress('SET', $percent); Debconf::Client::ConfModule::subst( 'debconf-apt-progress/info', 'DESCRIPTION', $description); Debconf::Client::ConfModule::progress( 'INFO', 'debconf-apt-progress/info'); } waitpid $pid, 0; return 0; } sub create_tempdir ($) { $cleanup_tempdir = shift; # File::Temp is in perl-modules, so we can't use it. Boo. $tempdir = `mktemp -d -t debconf-apt-progress.XXXXXX`; unless (defined $tempdir) { die "$0: can't create temporary directory: $!"; } chomp $tempdir; } sub create_config () { system('debconf-copydb', 'configdb', 'progressconfigdb', '-c', 'Name:progressconfigdb', '-c', 'Driver:File', '-c', "Filename:$tempdir/progressconfig.dat"); $? == 0 or die "$0: failed to create progressconfig.dat: $?"; # ignore errors for now; see bug #321290 my $pid = fork; if ($pid) { waitpid $pid, 0; die "$0: child process failed: $?" if $?; } else { open STDERR, '>', '/dev/null' or die "$0: can't reopen stderr on /dev/null: $!"; exec('debconf-copydb', 'templatedb', 'progresstemplatedb', '-c', 'Name:progresstemplatedb', '-c', 'Driver:File', '-c', "Filename:$tempdir/progresstemplates.dat") or die "$0: failed to create progresstemplates.dat: $!"; } local *CONFIG; open CONFIG, '>', "$tempdir/progress.conf" or die "$0: can't write to $tempdir/progress.conf: $!"; print CONFIG < \$config, 'start' => \$start, 'from=i' => \$from, 'to=i' => \$to, 'stop' => \$stop, 'logfile=s' => \$logfile); if (defined $from and not defined $to) { die "$0: --from requires --to\n"; } elsif (defined $to and not defined $from) { die "$0: --to requires --from\n"; } my $mutex = 0; ++$mutex if $config; ++$mutex if $start; ++$mutex if $stop; ++$mutex if defined $from; if ($mutex > 1) { die "$0: must use only one of --config, --start, --from/--to, or --stop\n"; } if ($config) { create_tempdir(0); print create_config(), "\n"; } elsif ($start) { import Debconf::Client::ConfModule; start_bar(); } elsif (defined $from) { import Debconf::Client::ConfModule; run_progress($from, $to, @ARGV); } elsif ($stop) { import Debconf::Client::ConfModule; stop_bar(); } else { unless ($ENV{DEBIAN_HAS_FRONTEND}) { create_tempdir(1); # hack to remember tempdir over exec $ENV{DEBCONF_APT_PROGRESS_TEMPDIR} = $tempdir; $ENV{DEBCONF_SYSTEMRC} = create_config(); @ARGV = @saved_argv; import Debconf::Client::ConfModule; } print STDERR join "\n", %ENV; start_bar(); run_progress(0, 100, @ARGV); stop_bar(); } =head1 AUTHORS Colin Watson Joey Hess =cut