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
On Thu, 2013-01-24 at 15:35 +0100, Martin Sivak wrote:
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.
Actually all this risen from the fact, that we need to get exception handling to the main thread. If an exception appears in a non-main thread, we cannot compete with the main thread for the standard input, we cannot use libreport that forks and execs and we cannot quit the installation neither by calling sys.exit() (because the main thread simply goes on) nor by sending SIGTERM to ourselves, because signals are not processed always (e.g. in blocking queue.pop() we use in the progress hub).
I have a patch for the aforementioned cases, it just needs some polishing.
--- pyanaconda/ui/tui/simpleline/base.py | 61 +++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 19 deletions(-)
diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py index f6ed9fb..cf2c36b 100644 --- a/pyanaconda/ui/tui/simpleline/base.py +++ b/pyanaconda/ui/tui/simpleline/base.py @@ -25,21 +25,21 @@ import readline import Queue import getpass from pyanaconda.threads import threadMgr, AnacondaThread -from pyanaconda.ui.communication import HUB_CODE_INPUT +from pyanaconda.ui.communication import HUB_CODE_INPUT, HUB_CODE_EXCEPTION
import gettext _ = lambda x: gettext.ldgettext("anaconda", x)
-class ExitAllMainLoops(Exception): - """This exception ends the whole App mainloop structure. App.run() quits - after it is processed.""" - pass - class ExitMainLoop(Exception): """This exception ends the outermost mainloop. Used internally when dialogs close.""" pass
+class ExitAllMainLoops(ExitMainLoop): + """This exception ends the whole App mainloop structure. App.run() quits + after it is processed.""" + 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. @@ -256,10 +256,16 @@ class App(object): input_needed = False else: # get the widget tree from the screen and show it in the screen - input_needed = screen.refresh(args) - screen.window.show_all() - self._redraw = False - + try: + input_needed = screen.refresh(args) + screen.window.show_all() + self._redraw = False + except ExitMainLoop: + raise + except Exception as ex: + self.queue.put((HUB_CODE_EXCEPTION, [ex])) + return False + return input_needed
def run(self): @@ -305,7 +311,13 @@ class App(object): last_screen = self._screens[-1][0]
# get the screen's prompt - prompt = last_screen.prompt(self._screens[-1][1]) + try: + prompt = last_screen.prompt(self._screens[-1][1]) + except ExitMainLoop: + raise + except Exception as ex: + self.queue.put((HUB_CODE_EXCEPTION, [ex])) + continue
# None means prompt handled the input by itself # ask for redraw and continue @@ -325,16 +337,16 @@ class App(object): if error_counter >= 5: self.redraw()
- # end just this loop - except ExitMainLoop: - break - # propagate higher to end all loops # not really needed here, but we might need # more processing in the future except ExitAllMainLoops: raise
+ # end just this loop + except ExitMainLoop: + break + 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 @@ -352,7 +364,12 @@ class App(object): return event elif event[0] in self._handlers: for handler, data in self._handlers[event[0]]: - handler(event, data) + try: + handler(event, data) + except ExitMainLoop: + raise + except Exception as ex: + self.queue.put((HUB_CODE_EXCEPTION, [ex]))
def raw_input(self, prompt, hidden=False): """This method reads one input from user. Its basic form has only one @@ -385,9 +402,15 @@ class App(object):
# delegate the handling to active screen first if self._screens: - key = self._screens[-1][0].input(args, key) - if key is None: - return True + try: + key = self._screens[-1][0].input(args, key) + if key is None: + return True + except ExitMainLoop: + raise + except Exception as ex: + self.queue.put((HUB_CODE_EXCEPTION, [ex])) + return False
# global close command if self._screens and (key == _('c')):
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.
I'd like to see convenience methods for these two new message types added, which can then be used in the next patch (and in this one, but I think it was only in one or two places).
Besides that, this is a pretty complex patch but I think it is okay. I wonder if we might later need something like unregister_event_handler or if that code will be useful elsewhere, but it's certainly not something to address now.
I'm also curious if AnacondaThread.run might need to be changed in light of all this threading exception handling you guys have been working on, and perhaps it's causing some of the problems in the first place.
- Chris
anaconda-patches@lists.fedorahosted.org