Skip to content

Commit

Permalink
qvm-ls: run a spinner while waiting
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
woju committed Jun 7, 2017
1 parent 2675d63 commit 57cabc3
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 4 deletions.
8 changes: 8 additions & 0 deletions doc/manpages/qvm-ls.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
148 changes: 148 additions & 0 deletions qubesadmin/spinner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# vim: fileencoding=utf-8

#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2017 Wojtek Porczyk <[email protected]>
#
# 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()
33 changes: 29 additions & 4 deletions qubesadmin/tools/qvm_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import textwrap

import qubesadmin
import qubesadmin.spinner
import qubesadmin.tools
import qubesadmin.utils
import qubesadmin.vm
Expand Down Expand Up @@ -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.
Expand All @@ -402,9 +407,12 @@ def write_table(self, stream=sys.stdout):

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):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 57cabc3

Please sign in to comment.