将 sys.stdout 重定向到 Tkinter.Text 小部件时出现分段错误

7
我正在使用Python/Tkinter构建基于GUI的应用程序,该应用程序基于现有的Python bdb模块。在此应用程序中,我希望将所有的stdout/stderr从控制台静音并重定向到我的GUI。为了实现这个目的,我编写了一个专门的Tkinter.Text对象(代码在帖子的末尾)。
基本思路是当有内容写入sys.stdout时,它会以黑色的形式显示在“Text”中。如果有内容写入sys.stderr,则以红色的形式显示在“Text”中。一旦有内容被写入,“Text”总是自动滚动到查看最新的行。
我目前使用的是Python 2.6.1。在Mac OS X 10.5上,这似乎非常好用。我没有遇到任何问题。但是,在RedHat Enterprise Linux 5上,我几乎总是在运行脚本时出现分段错误。分段错误不总是发生在相同的位置,但几乎总是发生。如果我从我的代码中注释掉sys.stdout=sys.stderr=这两行,分段错误似乎就消失了。
我相信还有其他方法可以解决这个问题,但有没有人能看出我在这里做错了什么,导致这些分段错误?这真让我抓狂。谢谢!
PS-我意识到将sys.stderr重定向到GUI可能不是一个好主意,但即使我只重定向sys.stdout而不是sys.stderr,我仍然会遇到分段错误。我也意识到目前允许“Text”无限增长。
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()

4
只是顺便说一下,我建议不要在所有情况下自动滚动。如果用户向上滚动查看某些内容,然后添加了新项,当他们正在查看的内容移出视野时,他们会成为不满意的用户。我使用的算法是,如果在输入新文本之前最后一行可见,我就会自动滚动。否则我不会滚动。 - Bryan Oakley
好的,很棒的决定。我相信这个问题很快就会出现在我的“待修复”列表中。 - Brent Writes Code
2个回答

4

好的,我已经找到了问题所在。在最初开发代码的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()

我希望我能解释为什么会发生分段错误,但我发现不断开启和关闭文本对象才是罪魁祸首。如果我将上面的代码改为:

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

希望这能帮助到遇到同样问题的任何人。

1
Nash: 不要忽略按键,另一种选择是从窗口部件的绑定标签中移除“Text”。 - Bryan Oakley

4

我假设这是一个更大的、有线程的程序的一部分。

不要使用锁,而是让你的代码写入一个线程安全的队列对象。然后,在你的主线程中轮询队列并写入文本小部件。你可以使用事件循环来进行轮询(而不是编写自己的循环),通过运行轮询作业,该作业在几毫秒后重新安排自己运行(几百毫秒可能足够了)。


很不幸,我认为我仍在调试一个更大更丑陋的问题,但我确实认为这个模式是更清晰和更好的显示方式。谢谢您的反馈。 - Brent Writes Code

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接