Ошибка сегментации при перенаправлении sys.stdout на виджет Tkinter.Text

Я нахожусь в процессе создания приложения на основе графического интерфейса пользователя с Python / Tkinter, которое строится поверх существующего модуля Python bdb. В этом приложении я хочу отключить весь stdout / stderr с консоли и перенаправить его в свой графический интерфейс. Для этого я написал специальный объект Tkinter.Text (код в конце сообщения).

Основная идея состоит в том, что когда что-то записывается в sys.stdout, это отображается как строка в «Тексте» с черным цветом. Если что-то записано в sys.stderr, это отображается в виде строки в «Тексте» красного цвета. Как только что-то написано, текст всегда прокручивается вниз, чтобы просмотреть самую последнюю строку.

Сейчас я использую Python 2.6.1. В Mac OS X 10.5 это, кажется, отлично работает. У меня не было проблем с этим. Однако в RedHat Enterprise Linux 5 я довольно надежно получаю ошибку сегментации во время выполнения сценария. Ошибка сегментации не всегда возникает в одном и том же месте, но почти всегда. Если я закомментирую строки sys.stdout= и sys.stderr= из своего кода, ошибки сегментации, похоже, исчезнут.

Я уверен, что есть другие способы обойти это, к которым мне, вероятно, придется прибегнуть, но может ли кто-нибудь увидеть что-нибудь, что я делаю явно неправильно, что могло бы вызвать эти ошибки сегментации? Это сводит меня с ума. Спасибо!

PS - Я понимаю, что перенаправление sys.stderr в графический интерфейс может быть не очень хорошей идеей, но я все равно получаю ошибки сегментации, даже когда я перенаправляю только sys.stdout, а не sys.stderr. Я также понимаю, что в данный момент я позволяю Тексту расти бесконечно.

class ConsoleText(tk.Text):
    '''A Tkinter Text widget that provides a scrolling display of console
    stderr and stdout.'''

    class IORedirector(object):
        '''A general class for redirecting I/O to this Text widget.'''
        def __init__(self,text_area):
            self.text_area = text_area

    class StdoutRedirector(IORedirector):
        '''A class for redirecting stdout to this Text widget.'''
        def write(self,str):
            self.text_area.write(str,False)

    class StderrRedirector(IORedirector):
        '''A class for redirecting stderr to this Text widget.'''
        def write(self,str):
            self.text_area.write(str,True)

    def __init__(self, master=None, cnf={}, **kw):
        '''See the __init__ for Tkinter.Text for most of this stuff.'''

        tk.Text.__init__(self, master, cnf, **kw)

        self.started = False
        self.write_lock = threading.Lock()

        self.tag_configure('STDOUT',background='white',foreground='black')
        self.tag_configure('STDERR',background='white',foreground='red')

        self.config(state=tk.DISABLED)

    def start(self):

        if self.started:
            return

        self.started = True

        self.original_stdout = sys.stdout
        self.original_stderr = sys.stderr

        stdout_redirector = ConsoleText.StdoutRedirector(self)
        stderr_redirector = ConsoleText.StderrRedirector(self)

        sys.stdout = stdout_redirector
        sys.stderr = stderr_redirector

    def stop(self):

        if not self.started:
            return

        self.started = False

        sys.stdout = self.original_stdout
        sys.stderr = self.original_stderr

    def write(self,val,is_stderr=False):

        #Fun Fact:  The way Tkinter Text objects work is that if they're disabled,
        #you can't write into them AT ALL (via the GUI or programatically).  Since we want them
        #disabled for the user, we have to set them to NORMAL (a.k.a. ENABLED), write to them,
        #then set their state back to DISABLED.

        self.write_lock.acquire()
        self.config(state=tk.NORMAL)

        self.insert('end',val,'STDERR' if is_stderr else 'STDOUT')
        self.see('end')

        self.config(state=tk.DISABLED)
        self.write_lock.release()

person Brent Writes Code    schedule 26.05.2010    source источник
comment
просто в сторону, я бы посоветовал не автоматически прокручивать вниз во всех случаях. Если пользователь прокрутил страницу вверх, чтобы посмотреть на что-то, а затем был добавлен новый элемент, он будет недовольным пользователем, когда то, на что он смотрит, ускользнет из поля зрения. Я использую алгоритм: если последняя строка видна до ввода нового текста, я автоматически прокручиваю. В противном случае я этого не сделаю.   -  person Bryan Oakley    schedule 26.05.2010
comment
Хороший звонок. Я уверен, что достаточно скоро один из них появился бы в моем списке исправлений.   -  person Brent Writes Code    schedule 01.06.2010


Ответы (2)


Я предполагаю, что это часть более крупной многопоточной программы.

Вместо использования блокировки пусть ваш код записывается в потокобезопасный объект очереди. Затем в основном потоке вы опрашиваете очередь и пишете в текстовый виджет. Вы можете выполнить опрос, используя цикл событий (вместо написания собственного цикла), запустив задание опроса, которое перепланировало себя для запуска через несколько мсек позже, используя after (пары сотен мс, вероятно, вполне достаточно).

person Bryan Oakley    schedule 26.05.2010
comment
К сожалению, я думаю, что у меня есть более серьезная и уродливая проблема, которую я все еще отлаживаю, но я действительно думаю, что эта модель является более чистым и лучшим способом отображения. Спасибо за ваш отзыв! - person Brent Writes Code; 01.06.2010

Хорошо, мне удалось отследить проблему. Мне так и не удалось воссоздать эту проблему в Mac OS X 10.5.8, где я изначально разработал код. Ошибки сегментации, похоже, возникают только в RedHat Enterprise Linux 5.

Оказывается, виноват этот фрагмент кода:

def write(self,val,is_stderr=False):

        #Fun Fact:  The way Tkinter Text objects work is that if they're disabled,
        #you can't write into them AT ALL (via the GUI or programatically).  Since we want them
        #disabled for the user, we have to set them to NORMAL (a.k.a. ENABLED), write to them,
        #then set their state back to DISABLED.

        self.write_lock.acquire()
        self.config(state=tk.NORMAL)

        self.insert('end',val,'STDERR' if is_stderr else 'STDOUT')
        self.see('end')

        self.config(state=tk.DISABLED)
        self.write_lock.release()

Я хотел бы получить объяснение почему ошибки сегментации, но я обнаружил, что постоянное включение и отключение объекта Text является виновником. Если я изменю приведенный выше фрагмент кода на это:

def write(self,val,is_stderr=False):

        self.write_lock.acquire()

        self.insert('end',val,'STDERR' if is_stderr else 'STDOUT')
        self.see('end')

        self.write_lock.release()

Мои ошибки сегментации исчезают, когда я удаляю self.config(state=...) вызовы. Весь смысл вызовов self.config(state=...) заключался в том, чтобы сделать так, чтобы пользователь не мог редактировать текстовое поле. Однако, когда текстовое поле находится в состоянии tk.DISABLED, вызовы self.insert(...) также не работают.

Обходное решение, которое я придумал, - оставить текстовое поле включенным, но заставить текстовое поле игнорировать весь ввод с клавиатуры (что создает иллюзию поведения только для чтения, если пользователь пытается использовать клавиатуру). Самый простой способ сделать это - изменить метод __init__, чтобы он выглядел следующим образом (измените состояние на tk.NORMAL и измените привязку для событий <Key>):

def __init__(self, master=None, cnf={}, **kw):
        '''See the __init__ for Tkinter.Text for most of this stuff.'''

        tk.Text.__init__(self, master, cnf, **kw)

        self.started = False
        self.write_lock = threading.Lock()

        self.tag_configure('STDOUT',background='white',foreground='black')
        self.tag_configure('STDERR',background='white',foreground='red')

        self.config(state=tk.NORMAL)
        self.bind('<Key>',lambda e: 'break') #ignore all key presses

Надеюсь, что это поможет любому, кто столкнется с той же проблемой.

person Brent Writes Code    schedule 02.06.2010
comment
Нэш: Вместо того, чтобы игнорировать нажатия клавиш, альтернативой является удаление текста из тегов привязки для виджета. - person Bryan Oakley; 26.07.2010