From: Richard Jones Date: Sat, 28 Aug 2010 11:49:55 +0000 (+0100) Subject: fish: Implement progress bars in guestfish. X-Git-Tag: 1.5.7~11 X-Git-Url: http://git.annexia.org/?p=libguestfs.git;a=commitdiff_plain;h=54837f6d7ba83178625e2f0c3c063457d9f3f79c 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). --- diff --git a/fish/Makefile.am b/fish/Makefile.am index 4060f1f..e1e8833 100644 --- a/fish/Makefile.am +++ b/fish/Makefile.am @@ -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 diff --git a/fish/fish.c b/fish/fish.c index c4ade8c..9696545 100644 --- a/fish/fish.c +++ b/fish/fish.c @@ -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++; diff --git a/fish/fish.h b/fish/fish.h index 8106610..be357f5 100644 --- a/fish/fish.h +++ b/fish/fish.h @@ -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[], diff --git a/fish/guestfish.pod b/fish/guestfish.pod index cf1140a..3b9aa20 100644 --- a/fish/guestfish.pod +++ b/fish/guestfish.pod @@ -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 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. 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: + + > 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 diff --git a/fish/progress.c b/fish/progress.c new file mode 100644 index 0000000..f196786 --- /dev/null +++ b/fish/progress.c @@ -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 + +#include +#include +#include +#include + +#include + +#include "fish.h" +#include "rmsd.h" + +/* Include these last since they redefine symbols such as 'lines' + * which seriously breaks other headers. + */ +#include +#include + +/* 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 */ + estimate /= 60. * 60.; + int hh = floor (estimate); + printf (">%dh", hh); + } else if (estimate >= 100.0 * 60.0 /* >= 100 minutes */) { + /* Display hoursminutes */ + 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); + } +} diff --git a/fish/reopen.c b/fish/reopen.c index 2dfc8db..9e19018 100644 --- a/fish/reopen.c +++ b/fish/reopen.c @@ -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; diff --git a/fish/rmsd.h b/fish/rmsd.h new file mode 100644 index 0000000..d4335bd --- /dev/null +++ b/fish/rmsd.h @@ -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 */ diff --git a/po/POTFILES.in b/po/POTFILES.in index e5fb857..3faa1fb 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -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