I'm new to object-oriented programming, Python and GTK+3, though I have a decent knowledge of procedural programming (mainly C).
I'm trying to build a simple Python + GTK+ 3 script to run pkexec apt-get update under Linux.
I have a mainWindow class (based on a Gtk.Window class) which contains a button object named button (based on a Gtk.Button class) which triggers a new_update_window() method defined in mainWindow upon a clicked event;
The new_update_window() method initializes an updateWindow object from an updateWindow class (based on a Gtk.Window class) which contains a label object named label (based on a Gtk.Label class) and calls the methods show_all() and update() defined in updateWindow;
The update() method is supposed to change label, run pkexec apt-get update and change label again.
The problem is no matter what I do one of the following occurs:
subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"]) directly, update.Window is shown but label is set immediately to the value it should be set only after pkexec apt-get update has finished executing;subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"]) directly, update.Window is not shown until pkexec apt-get update has finished executing;importing threading, defining a separate run_update() method in updateWindow and start the function in a separate thread using thread = threading.Thread(target=self.run_update), thread.start(), thread.join(), but still depending on which method I call in run_update() (subprocess.call() or subprocess.Popen) the relative behavior described above exhibits.I'm at a loss to understand how to accomplish what I'm after, which is:
updateWindow (Gtk.Window)label (Gtk.Label) in updateWindow
pkexec apt-get update
label in updateWindow
subprocess.Popen(): update.Window is shown but label is set immediately to the value it should be set only after pkexec apt-get update has finished executing;subprocess.call(): update.Window is not shown until pkexec apt-get update has finished executing;Here's the code;
Not using a thread (case 1, in this case using subprocess.Popen()):
#!/usr/bin/python3
from gi.repository import Gtk
import subprocess
class mainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updater")
button = Gtk.Button()
button.set_label("Update")
button.connect("clicked", self.new_update_window)
self.add(button)
def new_update_window(self, button):
update = updateWindow()
update.show_all()
update.update()
class updateWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
def update(self):
self.label.set_text("Updating... Please wait.")
subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
self.label.set_text("Updated.")
def run_update(self):
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()
Using a thread (case 3, in this case using subprocess.Popen()):
#!/usr/bin/python3
from gi.repository import Gtk
import threading
import subprocess
class mainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updater")
button = Gtk.Button()
button.set_label("Update")
button.connect("clicked", self.new_update_window)
self.add(button)
def new_update_window(self, button):
update = updateWindow()
update.show_all()
update.update()
class updateWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
def update(self):
self.label.set_text("Updating... Please wait.")
thread = threading.Thread(target=self.run_update)
thread.start()
thread.join()
self.label.set_text("Updated.")
def run_update(self):
subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()
Instead of using Python's subprocess module, you could use Gio.Subprocess which integrates with GTK's main loop:
#!/usr/bin/python3
from gi.repository import Gtk, Gio
# ...
class updateWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
def update(self):
self.label.set_text("Updating... Please wait.")
subprocess = Gio.Subprocess.new(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"], 0)
subprocess.wait_check_async(None, self._on_update_finished)
def _on_update_finished(self, subprocess, result):
subprocess.wait_check_finish(result)
self.label.set_text("Updated.")
You were almost there...
and the solution pretty simple :)
The issue you are having is that subprocess.call() would freeze the GUI (loop), and thus prevent the window to appear, while subprocess.Popen() would throw out the command and jump to self.label.set_text("Updated.")
You can simply solve it by running a separate thread, calling your command:
subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
and move the label- change command
self.label.set_text("Updated.")
into the thread, positioned after the first command. The thread then does not freeze the interface, while label is not changed too early, since subprocess.call() will prevent that.
The code then becomes:
#!/usr/bin/python3
from gi.repository import Gtk
from threading import Thread
import subprocess
class mainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updater")
button = Gtk.Button()
button.set_label("Update")
button.connect("clicked", self.new_update_window)
self.add(button)
def new_update_window(self, button):
update = updateWindow()
update.show_all()
update.update()
class updateWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
def update(self):
self.label.set_text("Updating... Please wait.")
Thread(target = self.run_update).start()
def run_update(self):
subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
self.label.set_text("Updated.")
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()
If you'd like to avoid using Thread, you could use Gtk.main_iteration() to prevent the interface from freezing while the process runs:
#!/usr/bin/python3
from gi.repository import Gtk
import subprocess
class mainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updater")
button = Gtk.Button()
button.set_label("Update")
button.connect("clicked", self.new_update_window)
self.add(button)
def new_update_window(self, button):
update = updateWindow()
update.show_all()
update.update()
class updateWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
def update(self):
self.label.set_text("Updating... Please wait.")
subprocess.Popen(["gedit"])
self.hold()
self.label.set_text("Updated.")
def run_update(self):
subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
def hold(self):
while True:
Gtk.main_iteration()
try:
subprocess.check_output(["pgrep", "apt-get"]).decode("utf-8")
except subprocess.CalledProcessError:
break
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()
Ongoing insight learned there is a better way to use threads then posted above in my answer.
You can use threads in a Gtk GUI, using
GObject.threads_init()
Then, to update the interface from the thread, use
GObject.idle_add()
from this (slgihtly outdated) link:
...call gobject.threads_init() at application initialization. Then you launch your threads normally, but make sure the threads never do any GUI tasks directly. Instead, you use gobject.idle_add to schedule GUI task to executed in the main thread
When we replace gobject.threads_init() by GObject.threads_init() and gobject.idle_add by GObject.idle_add(), we pretty much have the updated version of how to run threads in a Gtk application.
Applied in your code (using second example, using threads):
#!/usr/bin/python3
from gi.repository import Gtk, GObject
from threading import Thread
import subprocess
class mainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updater")
button = Gtk.Button()
button.set_label("Update")
button.connect("clicked", self.new_update_window)
self.add(button)
def new_update_window(self, button):
update = updateWindow()
update.show_all()
update.update()
class updateWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
def update(self):
# self.thread = threading.Thread(target=self.run_update)
thread = Thread(target=self.run_update)
thread.start()
def run_update(self):
GObject.idle_add(
self.label.set_text, "Updating... Please wait.",
priority=GObject.PRIORITY_DEFAULT
)
subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
GObject.idle_add(
self.label.set_text, "Updated.",
priority=GObject.PRIORITY_DEFAULT
)
GObject.threads_init()
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()
In
def run_update(self):
subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
you are not waiting the process to terminate, try
def run_update(self):
proc = subprocess.Popen(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"])
proc.wait()
instead. This should wait for the completion properly, but it won't help much since updateWindow.update will be called from mainWindow.new_update_window and GUI thread will be waiting for the process to complete.
Custom signals can be used to communicate when the process started by subprocess.Popen or subprocess.call completes:
#!/usr/bin/python3
from gi.repository import Gtk, GObject
import threading
import subprocess
class mainWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title = "Updater")
button = Gtk.Button()
button.set_label("Update")
button.connect("clicked", self.new_update_window)
self.add(button)
def new_update_window(self, button):
update = updateWindow(self)
update.show_all()
update.start_update()
class updateWindow(Gtk.Window):
def __init__(self, parent):
Gtk.Window.__init__(self, title = "Updating...")
self.label = Gtk.Label()
self.label.set_text("Idling...")
self.add(self.label)
self.parent = parent
GObject.signal_new('update_complete', self, GObject.SIGNAL_RUN_LAST,
None, (int,))
self.connect('update_complete', self.on_update_complete)
def on_update_complete(self, widget, rc):
self.label.set_text("Updated {:d}".format(rc))
# emit a signal to mainwindow if needed, self.parent.emit(...)
def start_update(self):
self.label.set_text("Updating... Please wait.")
thread = threading.Thread(target=self.run_update)
thread.start()
def run_update(self):
rc = subprocess.call(["/usr/bin/pkexec", "apt-get", "update"],
shell=False)
self.emit('update_complete', rc)
main = mainWindow()
main.connect("delete-event", Gtk.main_quit)
main.show_all()
Gtk.main()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With