fish: Implement progress bars in guestfish.

The progress bar is updated 3 times per second, and is not displayed
at all for operations which take less than two seconds.

You can disable progress bars by using the flag --no-progress-bars,
and you can enable progress bars in non-interactive sessions with
the flag --progress-bars.

A good way to test this is to use the following command:

guestfish --progress-bars \
          -N disk:10G \
          zero-device /dev/sda

(adjust "10G" to get different lengths of time).
This commit is contained in:
Richard Jones
2010-08-28 12:49:55 +01:00
parent 3003df6bbc
commit 54837f6d7b
8 changed files with 375 additions and 1 deletions

View File

@@ -49,6 +49,7 @@ guestfish_SOURCES = \
man.c \
more.c \
prep.c \
progress.c \
rc.c \
reopen.c \
supported.c \
@@ -72,7 +73,7 @@ guestfish_CFLAGS = \
guestfish_LDADD = \
$(LIBVIRT_LIBS) $(LIBXML2_LIBS) \
$(top_builddir)/src/libguestfs.la $(LIBREADLINE)
$(top_builddir)/src/libguestfs.la $(LIBREADLINE) -lm
# Make libguestfs use the convenience library.
noinst_LTLIBRARIES = librc_protocol.la

View File

@@ -85,6 +85,8 @@ static void cleanup_readline (void);
static void add_history_line (const char *);
#endif
static int override_progress_bars = -1;
/* Currently open libguestfs handle. */
guestfs_h *g;
@@ -100,6 +102,7 @@ const char *libvirt_uri = NULL;
int inspector = 0;
int utf8_mode = 0;
int have_terminfo = 0;
int progress_bars = 0;
static void __attribute__((noreturn))
usage (int status)
@@ -137,6 +140,8 @@ usage (int status)
" -m|--mount dev[:mnt] Mount dev on mnt (if omitted, /)\n"
" -n|--no-sync Don't autosync\n"
" -N|--new type Create prepared disk (test1.img, ...)\n"
" --progress-bars Enable progress bars even when not interactive\n"
" --no-progress-bars Disable progress bars\n"
" --remote[=pid] Send commands to remote %s\n"
" -r|--ro Mount read-only\n"
" --selinux Enable SELinux support\n"
@@ -182,6 +187,8 @@ main (int argc, char *argv[])
{ "new", 1, 0, 'N' },
{ "no-dest-paths", 0, 0, 'D' },
{ "no-sync", 0, 0, 'n' },
{ "progress-bars", 0, 0, 0 },
{ "no-progress-bars", 0, 0, 0 },
{ "remote", 2, 0, 0 },
{ "ro", 0, 0, 'r' },
{ "selinux", 0, 0, 0 },
@@ -267,6 +274,10 @@ main (int argc, char *argv[])
guestfs_set_selinux (g, 1);
} else if (STREQ (long_options[option_index].name, "keys-from-stdin")) {
keys_from_stdin = 1;
} else if (STREQ (long_options[option_index].name, "progress-bars")) {
override_progress_bars = 1;
} else if (STREQ (long_options[option_index].name, "no-progress-bars")) {
override_progress_bars = 0;
} else {
fprintf (stderr, _("%s: unknown long option: %s (%d)\n"),
program_name, long_options[option_index].name, option_index);
@@ -500,6 +511,15 @@ main (int argc, char *argv[])
}
}
/* Decide if we display progress bars. */
progress_bars =
override_progress_bars >= 0
? override_progress_bars
: (optind >= argc && isatty (0));
if (progress_bars)
guestfs_set_progress_callback (g, progress_callback, NULL);
/* Interactive, shell script, or command(s) on the command line? */
if (optind >= argc) {
if (isatty (0))
@@ -963,6 +983,8 @@ issue_command (const char *cmd, char *argv[], const char *pipecmd)
int pid = 0;
int i, r;
reset_progress_bar ();
/* This counts the commands issued, starting at 1. */
command_num++;

View File

@@ -55,6 +55,7 @@ extern int verbose;
extern int command_num;
extern int utf8_mode;
extern int have_terminfo;
extern int progress_bars;
extern const char *libvirt_uri;
extern int issue_command (const char *cmd, char *argv[], const char *pipe);
extern void pod2text (const char *name, const char *shortdesc, const char *body);
@@ -122,6 +123,10 @@ extern prep_data *create_prepared_file (const char *type_string,
extern void prepare_drive (const char *filename, prep_data *data,
const char *device);
/* in progress.c */
extern void reset_progress_bar (void);
extern void progress_callback (guestfs_h *g, void *data, int proc_nr, int serial, uint64_t position, uint64_t total);
/* in rc.c (remote control) */
extern void rc_listen (void) __attribute__((noreturn));
extern int rc_remote (int pid, const char *cmd, int argc, char *argv[],

View File

@@ -232,6 +232,17 @@ alternative to the I<-a> option: whereas I<-a> adds an existing disk,
I<-N> creates a preformatted disk with a filesystem and adds it.
See L</PREPARED DISK IMAGES> below.
=item B<--progress-bars>
Enable progress bars, even when guestfish is used non-interactively.
Progress bars are enabled by default when guestfish is used as an
interactive shell.
=item B<--no-progress-bars>
Disable progress bars.
=item B<--remote[=pid]>
Send remote commands to C<$GUESTFISH_PID> or C<pid>. See section
@@ -729,6 +740,31 @@ Create a blank 200MB disk:
guestfish -N disk:200M
=head1 PROGRESS BARS
Some (not all) long-running commands send progress notification
messages as they are running. Guestfish turns these messages into
progress bars.
When a command that supports progress bars takes longer than two
seconds to run, and if progress bars are enabled, then you will see
one appearing below the command:
><fs> copy-size /large-file /another-file 2048M
/ 10% [#####-----------------------------------------] 00:30
The spinner on the left hand side moves round once for every progress
notification received from the backend. This is a (reasonably) golden
assurance that the command is "doing something" even if the progress
bar is not moving, because the command is able to send the progress
notifications. When the bar reaches 100% and the command finishes,
the spinner disappears.
Progress bars are enabled by default when guestfish is used
interactively. You can enable them even for non-interactive modes
using I<--progress-bars>, and you can disable them completely using
I<--no-progress-bars>.
=head1 GUESTFISH COMMANDS
The commands in this section are guestfish convenience commands, in

239
fish/progress.c Normal file
View File

@@ -0,0 +1,239 @@
/* guestfish - the filesystem interactive shell
* Copyright (C) 2010 Red Hat Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <math.h>
#include <guestfs.h>
#include "fish.h"
#include "rmsd.h"
/* Include these last since they redefine symbols such as 'lines'
* which seriously breaks other headers.
*/
#include <term.h>
#include <curses.h>
/* Provided by termcap or terminfo emulation, but not defined
* in any header file.
*/
extern const char *UP;
static const char *
spinner (int count)
{
/* Choice of unicode spinners.
*
* For basic dingbats, see:
* http://www.fileformat.info/info/unicode/block/geometric_shapes/utf8test.htm
* http://www.fileformat.info/info/unicode/block/dingbats/utf8test.htm
*
* Arrows are a mess in unicode. This page helps a lot:
* http://xahlee.org/comp/unicode_arrows.html
*
* I prefer something which doesn't point, just spins.
*/
/* Black pointing triangle. */
//static const char *us[] = { "\u25b2", "\u25b6", "\u25bc", "\u25c0" };
/* White pointing triangle. */
//static const char *us[] = { "\u25b3", "\u25b7", "\u25bd", "\u25c1" };
/* Circle with half black. */
static const char *us[] = { "\u25d0", "\u25d3", "\u25d1", "\u25d2" };
/* White square white quadrant. */
//static const char *us[] = { "\u25f0", "\u25f3", "\u25f2", "\u25f1" };
/* White circle white quadrant. */
//static const char *us[] = { "\u25f4", "\u25f7", "\u25f6", "\u25f5" };
/* Black triangle. */
//static const char *us[] = { "\u25e2", "\u25e3", "\u25e4", "\u25e5" };
/* Spinning arrow in 8 directions. */
//static const char *us[] = { "\u2190", "\u2196", "\u2191", "\u2197",
// "\u2192", "\u2198", "\u2193", "\u2199" };
/* ASCII spinner. */
static const char *as[] = { "/", "-", "\\", "|" };
const char **s;
size_t n;
if (utf8_mode) {
s = us;
n = sizeof us / sizeof us[0];
}
else {
s = as;
n = sizeof as / sizeof as[0];
}
return s[count % n];
}
static double start; /* start time of command */
static int count; /* number of progress notifications per cmd */
static struct rmsd rmsd; /* running mean and standard deviation */
/* This function is called just before we issue any command. */
void
reset_progress_bar (void)
{
/* The time at which this command was issued. */
struct timeval start_t;
gettimeofday (&start_t, NULL);
start = start_t.tv_sec + start_t.tv_usec / 1000000.;
count = 0;
rmsd_init (&rmsd);
}
/* Return remaining time estimate (in seconds) for current call.
*
* This returns the running mean estimate of remaining time, but if
* the latest estimate of total time is greater than two s.d.'s from
* the running mean then we don't print anything because we're not
* confident that the estimate is meaningful. (Returned value is <0.0
* when nothing should be printed).
*/
static double
estimate_remaining_time (double ratio)
{
if (ratio <= 0.)
return -1.0;
struct timeval now_t;
gettimeofday (&now_t, NULL);
double now = now_t.tv_sec + now_t.tv_usec / 1000000.;
/* We've done 'ratio' of the work in 'now - start' seconds. */
double time_passed = now - start;
double total_time = time_passed / ratio;
/* Add total_time to running mean and s.d. and then see if our
* estimate of total time is meaningful.
*/
rmsd_add_sample (&rmsd, total_time);
double mean = rmsd_get_mean (&rmsd);
double sd = rmsd_get_standard_deviation (&rmsd);
if (fabs (total_time - mean) >= 2.0*sd)
return -1.0;
/* Don't return early estimates. */
if (time_passed < 3.0)
return -1.0;
return total_time - time_passed;
}
/* The overhead is how much we subtract before we get to the progress
* bar itself.
*
* / 100% [########---------------] xx:xx
* | | | | |
* | | | | time (5 cols)
* | | | |
* | | open paren + close paren + space (3 cols)
* | |
* | percentage and space (5 cols)
* |
* spinner and space (2 cols)
*
* Total = 2 + 5 + 3 + 5 = 15
*/
#define COLS_OVERHEAD 15
/* Callback which displays a progress bar. */
void
progress_callback (guestfs_h *g, void *data,
int proc_nr, int serial,
uint64_t position, uint64_t total)
{
if (have_terminfo == 0) {
dumb:
printf ("%" PRIu64 "/%" PRIu64 "\n", position, total);
} else {
int cols = tgetnum ((char *) "co");
if (cols < 32) goto dumb;
/* Update an existing progress bar just printed? */
if (count > 0)
tputs (UP, 2, putchar);
count++;
double ratio = (double) position / total;
if (ratio < 0) ratio = 0; else if (ratio > 1) ratio = 1;
if (ratio < 1) {
int percent = 100.0 * ratio;
printf ("%s%3d%% ", spinner (count), percent);
} else {
fputs (" 100% ", stdout);
}
int dots = ratio * (double) (cols - COLS_OVERHEAD);
const char *s_open, *s_dot, *s_dash, *s_close;
if (utf8_mode) {
s_open = "\u27e6"; s_dot = "\u2589"; s_dash = "\u2550"; s_close = "\u27e7";
} else {
s_open = "["; s_dot = "#"; s_dash = "-"; s_close = "]";
}
fputs (s_open, stdout);
int i;
for (i = 0; i < dots; ++i)
fputs (s_dot, stdout);
for (i = dots; i < cols - COLS_OVERHEAD; ++i)
fputs (s_dash, stdout);
fputs (s_close, stdout);
fputc (' ', stdout);
/* Time estimate. */
double estimate = estimate_remaining_time (ratio);
if (estimate >= 100.0 * 60.0 * 60.0 /* >= 100 hours */) {
/* Display hours<h> */
estimate /= 60. * 60.;
int hh = floor (estimate);
printf (">%dh", hh);
} else if (estimate >= 100.0 * 60.0 /* >= 100 minutes */) {
/* Display hours<h>minutes */
estimate /= 60. * 60.;
int hh = floor (estimate);
double ignore;
int mm = floor (modf (estimate, &ignore) * 60.);
printf ("%02dh%02d", hh, mm);
} else if (estimate >= 0.0) {
/* Display minutes:seconds */
estimate /= 60.;
int mm = floor (estimate);
double ignore;
int ss = floor (modf (estimate, &ignore) * 60.);
printf ("%02d:%02d", mm, ss);
}
else /* < 0 means estimate was not meaningful */
fputs ("--:--", stdout);
fputc ('\n', stdout);
}
}

View File

@@ -66,6 +66,9 @@ do_reopen (const char *cmd, int argc, char *argv[])
if (p)
guestfs_set_path (g2, p);
if (progress_bars)
guestfs_set_progress_callback (g2, progress_callback, NULL);
/* Close the original handle. */
guestfs_close (g);
g = g2;

67
fish/rmsd.h Normal file
View File

@@ -0,0 +1,67 @@
/* libguestfs - guestfish shell
* Copyright (C) 2010 Red Hat Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef FISH_RMSD_H
#define FISH_RMSD_H
/* Compute the running mean and standard deviation from the
* series of estimated values.
*
* Method:
* http://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
* Checked in a test program against answers given by Wolfram Alpha.
*/
struct rmsd {
double a; /* mean */
double i; /* number of samples */
double q;
};
static void
rmsd_init (struct rmsd *r)
{
r->a = 0;
r->i = 1;
r->q = 0;
}
static void
rmsd_add_sample (struct rmsd *r, double x)
{
double a_next, q_next;
a_next = r->a + (x - r->a) / r->i;
q_next = r->q + (x - r->a) * (x - a_next);
r->a = a_next;
r->q = q_next;
r->i += 1.0;
}
static double
rmsd_get_mean (const struct rmsd *r)
{
return r->a;
}
static double
rmsd_get_standard_deviation (const struct rmsd *r)
{
return sqrt (r->q / (r->i - 1.0));
}
#endif /* FISH_RMSD_H */

View File

@@ -81,6 +81,7 @@ fish/lcd.c
fish/man.c
fish/more.c
fish/prep.c
fish/progress.c
fish/rc.c
fish/reopen.c
fish/supported.c