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.
This commit is contained in:
parent
2675d63579
commit
57cabc395b
@ -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
qubesadmin/spinner.py
Normal file
148
qubesadmin/spinner.py
Normal file
@ -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()
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user