diff options
author | Martin Sivak <msivak@redhat.com> | 2013-01-24 15:32:07 +0100 |
---|---|---|
committer | Martin Sivak <msivak@redhat.com> | 2013-01-30 14:37:32 +0100 |
commit | a1f057789c0d32903dd6428987c886485f691ee7 (patch) | |
tree | 4e8052d392003147d928555451dbffcb8c025f8b | |
parent | 8398783f1e381265aa84a74c809ac31cda0dd041 (diff) | |
download | anaconda-a1f057789c0d32903dd6428987c886485f691ee7.tar.gz anaconda-a1f057789c0d32903dd6428987c886485f691ee7.tar.xz anaconda-a1f057789c0d32903dd6428987c886485f691ee7.zip |
Teach TUI how to react on async events
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.
-rw-r--r-- | pyanaconda/ui/communication.py | 4 | ||||
-rw-r--r-- | pyanaconda/ui/tui/__init__.py | 3 | ||||
-rw-r--r-- | pyanaconda/ui/tui/hubs/progress.py | 12 | ||||
-rw-r--r-- | pyanaconda/ui/tui/simpleline/base.py | 104 | ||||
-rw-r--r-- | 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 19e3a242a..04bb512ba 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 83c2bebe8..8da9846e5 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 c80372efc..72cb5dcda 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 e3039d28c..f6ed9fba6 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 f812678ae..bd8160314 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 |