Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

wx.ProgressDialog causing seg fault and/or GTK_IS_WINDOW failure when being destroyed

This only happens on Linux (possible OS X also, can't test atm), works fine on Windows.

I have a wx.ProgressDialog that is spawned with the main thread. I send the work off to another thread, and it periodically calls back to a callback function in the main thread that will update the ProgressDialog or, at the end of the work, destroy it. However, I get an interesting message on Linux when this happens:

(python:12728): Gtk-CRITICAL **: IA__gtk_window_set_modal: assertion 'GTK_IS_WINDOW (window)' failed

The dialog does close, but if I try to spawn it again it looks like it's already almost finished. Sometimes a seg fault will follow this message as well.

I've tried to simulate it with a stripped down version here:

import wxversion
wxversion.select("2.8")
import wx
import sys
import threading

MAX_COUNT = 100

## This class is in a different area of the codebase and
class WorkerThread(threading.Thread):
    def __init__(self, callback):
        threading.Thread.__init__(self)
        self.callback = callback

    def run(self):
        # simulate work done. IRL, this calls another function in another
        # area of the codebase. This function would generate an XML document,
        # which loops through a list of items and creates a set of elements for
        # each item, calling back after each item. Here, we simply set up a for
        # loop and simulate work with wx.MilliSleep
        for i in xrange(MAX_COUNT):
            print i
            wx.MilliSleep(30)
            wx.CallAfter(self.callback, i)

        # Send done signal to GUI
        wx.CallAfter(self.callback, -1)

class Frame(wx.Frame):
    def __init__(self, title):
        wx.Frame.__init__(self, None, title=title, pos=(150,150), size=(350,200))

        panel = wx.Panel(self)
        box = wx.BoxSizer(wx.VERTICAL)

        m_btn = wx.Button(panel, wx.ID_ANY, "Run Stuff")
        m_btn.Bind(wx.EVT_BUTTON, self.OnRunButton)
        box.Add(m_btn, 0, wx.ALL, 10)

        panel.SetSizer(box)
        panel.Layout()

    def OnRunButton(self, event):
        self.progressDialog = wx.ProgressDialog("Doing work",
                          "Doing Work",
                          maximum=MAX_COUNT, parent=self,
                          style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME)
        self.worker(self.threadCallback)
        self.progressDialog.ShowModal()

    def worker(self, callback):
        # This bit is in another part of the codebase originally. In the test,
        # I could have added it to OnRunButton, but I wanted function calls to
        # be similar between test and actual code
        thread = WorkerThread(callback)
        thread.start()

    def threadCallback(self, info):
        # We update based on position, or destroy if we get a -1
        if info == -1:
            self.progressDialog.Destroy()
        else:
            self.progressDialog.Update(info)

app = wx.App(redirect=False)
top = Frame("ProgressDialog Test")
top.Show()
app.MainLoop()

(we select 2.8, but ideally any fix should work in both 2.8 and 3.0. I actually haven't been able to test it in 3.0 in linux due to a bad 3.0 build)

This does a good job at representing the issue: works fine in Windows, but seg fault when it tries to destroy the progress dialog. However, I can't get the example to show the GTK_IS_WINDOW

Ive tried searching for solutions. I've read that it might be due to the fact that the worker thread finishes too quickly, and thus leaves the GUI with some messages in it's queue. I'm not sure I completely understand this (never got the hang of Yields and messages, etc), but what I believe this to mean is that when the worker is at 100%, the ProgressDialog (being slower), might only be at 75%, and still has the extra 25% of messages to use to "Update" the GUI, but instead gets destroyed.

I'd like some clarification on if I'm understanding that correctly or not.

Also, I believe .Hide() works as a work around, but I'd like to Destroy it instead because that's the proper thing to do.

Regardless, any help would be greatly appreciated. =)

like image 681
blitzmann Avatar asked Jun 20 '15 23:06

blitzmann


1 Answers

I've tried your code, also many modifications been tried to overcome this issue, but failed. Anyway, I've created the following wxPython script to fulfill your purpose, see below:

import wxversion
wxversion.select("2.8") # version 3.0 works, too.
import wx
import sys
import threading
import time

MAX_COUNT = 200

class WorkerThread(threading.Thread):
    def __init__(self, target, countNum):
        threading.Thread.__init__(self, target = target)
        self.setDaemon(True)
        self.cnt = countNum
        self.target = target
        self.pb = self.target.pb

    def run(self):
        for i in xrange(self.cnt):
            print i+1
            wx.MilliSleep(50)
            wx.CallAfter(self.pb.SetValue, i+1)

        wx.CallAfter(self.target.MakeModal, False)
        wx.CallAfter(self.target.Close)

class ProgressBarFrame(wx.Frame):
    def __init__(self, parent, title, range = 100) :
        wx.Frame.__init__(self, parent = parent, title = title)
        self.range = range
        self.createProgressbar()
        self.SetMinSize((400, 10))
        self.Centre()
        self.Show()
        self.t0 = time.time()
        self.elapsed_time_timer.Start(1000)

    def createProgressbar(self):
        self.pb       = wx.Gauge(self)
        self.pb.SetRange(range = self.range)

        self.elapsed_time_st  = wx.StaticText(self, label = 'Elapsed Time:')
        self.elapsed_time_val = wx.StaticText(self, label = '00:00:00')

        vbox_main = wx.BoxSizer(wx.VERTICAL)
        hbox_time = wx.BoxSizer(wx.HORIZONTAL)
        hbox_time.Add(self.elapsed_time_st,  0, wx.ALIGN_LEFT | wx.EXPAND | wx.ALL, 5)
        hbox_time.Add(self.elapsed_time_val, 0, wx.ALIGN_LEFT | wx.EXPAND | wx.ALL, 5)
        vbox_main.Add(self.pb,   0, wx.EXPAND | wx.ALL, 5)
        vbox_main.Add(hbox_time, 0, wx.EXPAND | wx.ALL, 5)

        self.SetSizerAndFit(vbox_main)

        self.elapsed_time_timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.onTickTimer, self.elapsed_time_timer)

    def onTickTimer(self, event):
        fmt='%H:%M:%S'
        self.elapsed_time_val.SetLabel(time.strftime(fmt, time.gmtime(time.time()-self.t0)))

class Frame(wx.Frame):
    def __init__(self, title):
        wx.Frame.__init__(self, None, title=title, pos=(150,150), size=(350,200))

        panel = wx.Panel(self)
        box = wx.BoxSizer(wx.VERTICAL)

        m_btn = wx.Button(panel, wx.ID_ANY, "Run Stuff")
        self.Bind(wx.EVT_BUTTON, self.OnRunButton, m_btn)
        box.Add(m_btn, 0, wx.ALL, 10)

        panel.SetSizer(box)

    def OnRunButton(self, event):
        self.progressbar = ProgressBarFrame(self, 'Working Processing', MAX_COUNT)
        self.progressbar.MakeModal(True)
        worker = WorkerThread(self.progressbar, MAX_COUNT)
        worker.start()

app = wx.App(redirect=False)
top = Frame("ProgressDialog Test")
top.Show()
app.MainLoop()

I'm using wx.Gauge to do what wx.ProgressDialog does, as well as an additional wx.Timer to show the elapsed time. MakeModal() method is used to mimic the ShowModal effect which is the default style that Dialog shows, do not forget to release the Modal status by MakeModal(False) or the frame would be freezed. You can add more stuff in the ProgressBarFrame class.

I'm thinking the segment fault error may arise from the events calling, especially when multithreading issue is involved, maybe carefully inspect into the wx.ProgressDialog class would show some clue.

Screenshot of progressbar demo

like image 54
Tong Avatar answered Nov 14 '22 23:11

Tong