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.
|
Decrease verbosity.
|
||||||
|
|
||||||
|
.. option:: --spinner
|
||||||
|
|
||||||
|
Have a spinner spinning while the spinning mainloop spins new table cells.
|
||||||
|
|
||||||
|
.. option:: --no-spinner
|
||||||
|
|
||||||
|
No spinner today.
|
||||||
|
|
||||||
Authors
|
Authors
|
||||||
-------
|
-------
|
||||||
| Joanna Rutkowska <joanna at invisiblethingslab dot com>
|
| 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 textwrap
|
||||||
|
|
||||||
import qubesadmin
|
import qubesadmin
|
||||||
|
import qubesadmin.spinner
|
||||||
import qubesadmin.tools
|
import qubesadmin.tools
|
||||||
import qubesadmin.utils
|
import qubesadmin.utils
|
||||||
import qubesadmin.vm
|
import qubesadmin.vm
|
||||||
@ -380,19 +381,23 @@ class Table(object):
|
|||||||
:param qubes.Qubes app: Qubes application object.
|
:param qubes.Qubes app: Qubes application object.
|
||||||
:param list colnames: Names of the columns (need not to be uppercase).
|
: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.app = app
|
||||||
self.columns = tuple(Column.columns[col.upper()] for col in colnames)
|
self.columns = tuple(Column.columns[col.upper()] for col in colnames)
|
||||||
|
self.spinner = spinner
|
||||||
self.raw_data = raw_data
|
self.raw_data = raw_data
|
||||||
|
|
||||||
|
|
||||||
def get_head(self):
|
def get_head(self):
|
||||||
'''Get table head data (all column heads).'''
|
'''Get table head data (all column heads).'''
|
||||||
return [col.ls_head for col in self.columns]
|
return [col.ls_head for col in self.columns]
|
||||||
|
|
||||||
def get_row(self, vm):
|
def get_row(self, vm):
|
||||||
'''Get single table row data (all columns for one domain).'''
|
'''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):
|
def write_table(self, stream=sys.stdout):
|
||||||
'''Write whole table to file-like object.
|
'''Write whole table to file-like object.
|
||||||
@ -402,9 +407,12 @@ class Table(object):
|
|||||||
|
|
||||||
table_data = []
|
table_data = []
|
||||||
if not self.raw_data:
|
if not self.raw_data:
|
||||||
|
self.spinner.show('please wait...')
|
||||||
table_data.append(self.get_head())
|
table_data.append(self.get_head())
|
||||||
|
self.spinner.update()
|
||||||
for vm in sorted(self.app.domains):
|
for vm in sorted(self.app.domains):
|
||||||
table_data.append(self.get_row(vm))
|
table_data.append(self.get_row(vm))
|
||||||
|
self.spinner.hide()
|
||||||
qubesadmin.tools.print_table(table_data, stream=stream)
|
qubesadmin.tools.print_table(table_data, stream=stream)
|
||||||
else:
|
else:
|
||||||
for vm in sorted(self.app.domains):
|
for vm in sorted(self.app.domains):
|
||||||
@ -515,6 +523,16 @@ def get_parser():
|
|||||||
help='Display specify data of specified VMs. Intended for '
|
help='Display specify data of specified VMs. Intended for '
|
||||||
'bash-parsing.')
|
'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',
|
# parser.add_argument('--conf', '-c',
|
||||||
# action='store', metavar='CFGFILE',
|
# action='store', metavar='CFGFILE',
|
||||||
# help='Qubes config file')
|
# help='Qubes config file')
|
||||||
@ -547,7 +565,14 @@ def main(args=None, app=None):
|
|||||||
if col.upper() not in Column.columns:
|
if col.upper() not in Column.columns:
|
||||||
PropertyColumn(col)
|
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)
|
table.write_table(sys.stdout)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
Loading…
Reference in New Issue
Block a user