Kaynağa Gözat

qvm-ls: run a spinner while waiting

Since Admin API, qvm-ls takes a long time to complete. Therefore,
Corporate Headquarters commanded that a Enterprise Spinner is to be
implemented and mandated it's use unto us.

We take amusement from its endless gyrations.
Wojtek Porczyk 7 yıl önce
ebeveyn
işleme
57cabc395b
3 değiştirilmiş dosya ile 185 ekleme ve 4 silme
  1. 8 0
      doc/manpages/qvm-ls.rst
  2. 148 0
      qubesadmin/spinner.py
  3. 29 4
      qubesadmin/tools/qvm_ls.py

+ 8 - 0
doc/manpages/qvm-ls.rst

@@ -47,6 +47,14 @@ Options
 
    Decrease verbosity.
 
+.. option:: --spinner
+
+   Have a spinner spinning while the spinning mainloop spins new table cells.
+
+.. option:: --no-spinner
+
+   No spinner today.
+
 Authors
 -------
 | Joanna Rutkowska <joanna at invisiblethingslab dot com>

+ 148 - 0
qubesadmin/spinner.py

@@ -0,0 +1,148 @@
+# vim: fileencoding=utf-8
+
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2017  Wojtek Porczyk <woju@invisiblethingslab.com>
+#
+# 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.
+#
+
+'''Qubes CLI spinner
+
+A novice asked the master: “In the east there is a great tree-structure that
+men call `Corporate Headquarters'. It is bloated out of shape with vice
+presidents and accountants. It issues a multitude of memos, each saying `Go,
+Hence!' or `Go, Hither!' and nobody knows what is meant. Every year new names
+are put onto the branches, but all to no avail. How can such an unnatural
+entity be?"
+
+The master replied: “You perceive this immense structure and are disturbed that
+it has no rational purpose. Can you not take amusement from its endless
+gyrations? Do you not enjoy the untroubled ease of programming beneath its
+sheltering branches? Why are you bothered by its uselessness?”
+
+(Geoffrey James, “The Tao of Programming”, 7.1)
+'''
+
+import curses
+import io
+import itertools
+
+CHARSET = '-\\|/'
+ENTERPRISE_CHARSET = CHARSET * 4 + '-._.-^' * 2
+
+class AbstractSpinner(object):
+    '''The base class for all Spinners
+
+    :param stream: file-like object with ``.write()`` method
+    :param str charset: the sequence of characters to display
+
+    The spinner should be used as follows:
+        1. exactly one call to :py:meth:`show()`
+        2. zero or more calls to :py:meth:`update()`
+        3. exactly one call to :py:meth:`hide()`
+    '''
+    def __init__(self, stream, charset=CHARSET):
+        self.stream = stream
+        self.charset = itertools.cycle(charset)
+
+    def show(self, prompt):
+        '''Show the spinner, with a prompt
+
+        :param str prompt: prompt, like "please wait"
+        '''
+        raise NotImplementedError()
+
+    def hide(self):
+        '''Hide the spinner and the prompt'''
+        raise NotImplementedError()
+
+    def update(self):
+        '''Show next spinner character'''
+        raise NotImplementedError()
+
+
+class DummySpinner(AbstractSpinner):
+    '''Dummy spinner, does not do anything'''
+    def show(self, prompt):
+        pass
+
+    def hide(self):
+        pass
+
+    def update(self):
+        pass
+
+
+class QubesSpinner(AbstractSpinner):
+    '''Basic spinner
+
+    This spinner uses standard ASCII control characters'''
+    def __init__(self, *args, **kwargs):
+        super(QubesSpinner, self).__init__(*args, **kwargs)
+        self.hidelen = 0
+        self.cub1 = '\b'
+
+    def show(self, prompt):
+        self.hidelen = len(prompt) + 2
+        self.stream.write('{} {}'.format(prompt, next(self.charset)))
+        self.stream.flush()
+
+    def hide(self):
+        self.stream.write('\r' + ' ' * self.hidelen + '\r')
+        self.stream.flush()
+
+    def update(self):
+        self.stream.write(self.cub1 + next(self.charset))
+        self.stream.flush()
+
+
+class QubesSpinnerEnterpriseEdition(QubesSpinner):
+    '''Enterprise spinner
+
+    This is tty- and terminfo-aware spinner. Recommended.
+    '''
+    def __init__(self, stream, charset=None):
+        # our Enterprise logic follows
+        self.stream_isatty = stream.isatty()
+        if charset is None:
+            charset = ENTERPRISE_CHARSET if self.stream_isatty else '.'
+
+        super(QubesSpinnerEnterpriseEdition, self).__init__(stream, charset)
+
+        if self.stream_isatty:
+            try:
+                curses.setupterm()
+                self.has_terminfo = True
+                self.cub1 = curses.tigetstr('cub1').decode()
+            except (curses.error, io.UnsupportedOperation):
+                # we are in very non-Enterprise environment
+                self.has_terminfo = False
+        else:
+            self.cub1 = ''
+
+    def hide(self):
+        if self.stream_isatty:
+            hideseq = '\r' + ' ' * self.hidelen + '\r'
+            if self.has_terminfo:
+                hideseq_l = (curses.tigetstr('cr'), curses.tigetstr('clr_eol'))
+                if all(seq is not None for seq in hideseq_l):
+                    hideseq = ''.join(seq.decode() for seq in hideseq_l)
+        else:
+            hideseq = '\n'
+
+        self.stream.write(hideseq)
+        self.stream.flush()

+ 29 - 4
qubesadmin/tools/qvm_ls.py

@@ -34,6 +34,7 @@ import sys
 import textwrap
 
 import qubesadmin
+import qubesadmin.spinner
 import qubesadmin.tools
 import qubesadmin.utils
 import qubesadmin.vm
@@ -380,19 +381,23 @@ class Table(object):
     :param qubes.Qubes app: Qubes application object.
     :param list colnames: Names of the columns (need not to be uppercase).
     '''
-    def __init__(self, app, colnames, raw_data=False):
+    def __init__(self, app, colnames, spinner, raw_data=False):
         self.app = app
         self.columns = tuple(Column.columns[col.upper()] for col in colnames)
+        self.spinner = spinner
         self.raw_data = raw_data
 
-
     def get_head(self):
         '''Get table head data (all column heads).'''
         return [col.ls_head for col in self.columns]
 
     def get_row(self, vm):
         '''Get single table row data (all columns for one domain).'''
-        return [col.cell(vm) for col in self.columns]
+        ret = []
+        for col in self.columns:
+            ret.append(col.cell(vm))
+            self.spinner.update()
+        return ret
 
     def write_table(self, stream=sys.stdout):
         '''Write whole table to file-like object.
@@ -402,9 +407,12 @@ class Table(object):
 
         table_data = []
         if not self.raw_data:
+            self.spinner.show('please wait...')
             table_data.append(self.get_head())
+            self.spinner.update()
             for vm in sorted(self.app.domains):
                 table_data.append(self.get_row(vm))
+            self.spinner.hide()
             qubesadmin.tools.print_table(table_data, stream=stream)
         else:
             for vm in sorted(self.app.domains):
@@ -515,6 +523,16 @@ def get_parser():
         help='Display specify data of specified VMs. Intended for '
              'bash-parsing.')
 
+    parser.add_argument('--spinner',
+        action='store_true', dest='spinner',
+        help='reenable spinner')
+
+    parser.add_argument('--no-spinner',
+        action='store_false', dest='spinner',
+        help='disable spinner')
+
+    parser.set_defaults(spinner=True)
+
 #   parser.add_argument('--conf', '-c',
 #       action='store', metavar='CFGFILE',
 #       help='Qubes config file')
@@ -547,7 +565,14 @@ def main(args=None, app=None):
         if col.upper() not in Column.columns:
             PropertyColumn(col)
 
-    table = Table(args.app, columns)
+    if args.spinner:
+        # we need Enterprise Edition™, since it's the only one that detects TTY
+        # and uses dots if we are redirected somewhere else
+        spinner = qubesadmin.spinner.QubesSpinnerEnterpriseEdition(sys.stderr)
+    else:
+        spinner = qubesadmin.spinner.DummySpinner(sys.stderr)
+
+    table = Table(args.app, columns, spinner)
     table.write_table(sys.stdout)
 
     return 0