This involves couple of places:
- TUI is blocking and waiting for user input
- Progress HUB is in charge and is reading the progress queue
- any other part wanting to react to a message like READY, ..
This patch directly fixes the first two and adds a mechanism
to register callbacks that will react to the third one.
We (me and vpodzime) expect python-meh integration to use
this to file exception reports from text mode installation.
---
pyanaconda/ui/communication.py | 4 ++
pyanaconda/ui/tui/__init__.py | 3 +-
pyanaconda/ui/tui/hubs/progress.py | 12 +++-
pyanaconda/ui/tui/simpleline/base.py | 104 +++++++++++++++++++++++++++++++++--
pyanaconda/ui/tui/spokes/password.py | 5 +-
5 files changed, 117 insertions(+), 11 deletions(-)
diff --git a/pyanaconda/ui/communication.py b/pyanaconda/ui/communication.py
index 19e3a24..04bb512 100644
--- a/pyanaconda/ui/communication.py
+++ b/pyanaconda/ui/communication.py
@@ -37,9 +37,13 @@ hubQ = Queue.Queue()
# _READY - [spoke_name, justUpdate]
# _NOT_READY - [spoke_name]
# _MESSAGE - [spoke_name, string]
+# _INPUT - [string]
+# _EXCEPTION - [exc]
HUB_CODE_READY = 0
HUB_CODE_NOT_READY = 1
HUB_CODE_MESSAGE = 2
+HUB_CODE_INPUT = 3
+HUB_CODE_EXCEPTION = 4
# Convenience methods to put things into the queue without the user having to
# know the details of the queue.
diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py
index 83c2beb..8da9846 100644
--- a/pyanaconda/ui/tui/__init__.py
+++ b/pyanaconda/ui/tui/__init__.py
@@ -21,6 +21,7 @@
from pyanaconda import ui
from pyanaconda.ui import common
+from pyanaconda.ui import communication
from pyanaconda.flags import flags
import simpleline as tui
from hubs.summary import SummaryHub
@@ -170,7 +171,7 @@ class TextUserInterface(ui.UserInterface):
"""Construct all the objects required to implement this interface.
This method must be provided by all subclasses.
"""
- self._app = tui.App(u"Anaconda", yes_or_no_question = YesNoDialog)
+ self._app = tui.App(u"Anaconda", yes_or_no_question=YesNoDialog, queue=communication.hubQ)
_hubs = self._list_hubs()
# First, grab a list of all the standalone spokes.
diff --git a/pyanaconda/ui/tui/hubs/progress.py b/pyanaconda/ui/tui/hubs/progress.py
index c80372e..72cb5dc 100644
--- a/pyanaconda/ui/tui/hubs/progress.py
+++ b/pyanaconda/ui/tui/hubs/progress.py
@@ -52,7 +52,17 @@ class ProgressHub(TUIHub):
while True:
# Attempt to get a message out of the queue for how we should update
# the progress bar. If there's no message, don't error out.
- (code, args) = q.get()
+ # Also flush the communication Queue at least once a second and
+ # process it's events so we can react to async evens (like a thread
+ # throwing an exception)
+ while True:
+ try:
+ (code, args) = q.get(timeout = 1)
+ break
+ except Queue.Empty:
+ pass
+ finally:
+ self.app.process_events()
if code == progress.PROGRESS_CODE_INIT:
# Text mode doesn't have a finite progress bar
diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py
index e3039d2..f6ed9fb 100644
--- a/pyanaconda/ui/tui/simpleline/base.py
+++ b/pyanaconda/ui/tui/simpleline/base.py
@@ -22,6 +22,10 @@
__all__ = ["App", "UIScreen", "Widget"]
import readline
+import Queue
+import getpass
+from pyanaconda.threads import threadMgr, AnacondaThread
+from pyanaconda.ui.communication import HUB_CODE_INPUT
import gettext
_ = lambda x: gettext.ldgettext("anaconda", x)
@@ -36,7 +40,6 @@ class ExitMainLoop(Exception):
close."""
pass
-
class App(object):
"""This is the main class for TUI screen handling. It is responsible for
mainloop control and keeping track of the screen stack.
@@ -55,7 +58,7 @@ class App(object):
STOP_MAINLOOP = False
NOP = None
- def __init__(self, title, yes_or_no_question = None, width = 80):
+ def __init__(self, title, yes_or_no_question = None, width = 80, queue = None):
"""
:param title: application title for whenever we need to display app name
:type title: unicode
@@ -72,6 +75,20 @@ class App(object):
self._width = width
self.quit_question = yes_or_no_question
+ # async control queue
+ if queue:
+ self.queue = queue
+ else:
+ self.queue = Queue.Queue()
+
+ # ensure unique thread names
+ self._in_thread_counter = 0
+
+ # event handlers
+ # key: event id
+ # value: list of tuples (callback, data)
+ self._handlers = {}
+
# screen stack contains triplets
# UIScreen to show
# arguments for it's show method
@@ -81,6 +98,50 @@ class App(object):
# - False = already running loop, exit when window closes
self._screens = []
+ def register_event_handler(self, event, callback, data = None):
+ """This method registers a callback which will be called
+ when message "event" is encountered during process_events.
+
+ The callback has to accept two arguments:
+ - the received message in the form of (type, [arguments])
+ - the data registered with the handler
+
+ :param event: the id of the event we want to react on
+ :type event: number|string
+
+ :param callback: the callback function
+ :type callback: func(event_message, data)
+
+ :param data: optional data to pass to callback
+ :type data: anything
+ """
+ if not event in self._handlers:
+ self._handlers[event] = []
+ self._handlers[event].append((callback, data))
+
+ def _thread_input(self, queue, prompt, hidden):
+ """This method is responsible for interruptible user input. It is expected
+ to be used in a thread started on demand by the App class and returns the
+ input via the communication Queue.
+
+ :param queue: communication queue to be used
+ :type queue: Queue.Queue instance
+
+ :param prompt: prompt to be displayed
+ :type prompt: str
+
+ :param hidden: whether typed characters should be echoed or not
+ :type hidden: bool
+
+ """
+
+ if hidden:
+ data = getpass.getpass(prompt)
+ else:
+ data = raw_input(prompt)
+
+ queue.put((HUB_CODE_INPUT, [data]))
+
def switch_screen(self, ui, args = None):
"""Schedules a screen to replace the current one.
@@ -223,6 +284,9 @@ class App(object):
# run until there is nothing else to display
while self._screens:
+ # process asynchronous events
+ self.process_events()
+
# if redraw is needed, separate the content on the screen from the
# stuff we are about to display now
if self._redraw:
@@ -271,10 +335,38 @@ class App(object):
except ExitAllMainLoops:
raise
- def raw_input(self, prompt):
- """This method reads one input from user. Its basic form has only one line,
- but we might need to override it for more complex apps or testing."""
- return raw_input(prompt)
+ def process_events(self, return_at = None):
+ """This method processes incoming async messages and returns
+ when a specific message is encountered or when the queue
+ is empty.
+
+ If return_at message was specified, the received
+ message is returned.
+
+ If the message does not fit return_at, but handlers are
+ defined then it processes all handlers for this message
+ """
+ while return_at or not self.queue.empty():
+ event = self.queue.get()
+ if event[0] == return_at:
+ return event
+ elif event[0] in self._handlers:
+ for handler, data in self._handlers[event[0]]:
+ handler(event, data)
+
+ def raw_input(self, prompt, hidden=False):
+ """This method reads one input from user. Its basic form has only one
+ line, but we might need to override it for more complex apps or testing."""
+
+ thread_name = "AnaInputThread%d" % self._in_thread_counter
+ self._in_thread_counter += 1
+ input_thread = AnacondaThread(name=thread_name,
+ target=self._thread_input,
+ args=(self.queue, prompt, hidden))
+ input_thread.daemon = True
+ threadMgr.add(input_thread)
+ event = self.process_events(return_at=HUB_CODE_INPUT)
+ return event[1][0] # return the user input
def input(self, args, key):
"""Method called internally to process unhandled input key presses.
diff --git a/pyanaconda/ui/tui/spokes/password.py b/pyanaconda/ui/tui/spokes/password.py
index f812678..bd81603 100644
--- a/pyanaconda/ui/tui/spokes/password.py
+++ b/pyanaconda/ui/tui/spokes/password.py
@@ -25,7 +25,6 @@ from pyanaconda.ui.tui.simpleline import TextWidget
from pyanaconda.ui.tui import YesNoDialog
from pyanaconda.users import validatePassword
from pwquality import PWQError
-import getpass
import gettext
_ = lambda x: gettext.ldgettext("anaconda", x)
@@ -65,8 +64,8 @@ class PasswordSpoke(NormalTUISpoke):
def prompt(self, args = None):
"""Overriden prompt as password typing is special."""
- pw = getpass.getpass(_("Password: "))
- confirm = getpass.getpass(_("Password (confirm): "))
+ pw = self._app.raw_input(_("Password: "), hidden=True)
+ confirm = self._app.raw_input(_("Password (confirm): "), hidden=True)
error = None
# just returning an error is either blank or mismatched
--
1.7.11.7