Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to initialize a window and wait for a process to end in Python 3 + GTK+ 3

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:

  • If I run 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;
  • If I run subprocess.call(["/usr/bin/pkexec", "/usr/bin/apt-get", "update"]) directly, update.Window is not shown until pkexec apt-get update has finished executing;
  • I tried 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.

Tl;dr

I'm at a loss to understand how to accomplish what I'm after, which is:

  1. Showing updateWindow (Gtk.Window)
  2. Updating label (Gtk.Label) in updateWindow
  3. Running pkexec apt-get update
  4. Updating 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;
  • Wrapping either of the two in function and run the function in a separate thread doesn't change anything.

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()
like image 794
kos Avatar asked Jan 27 '16 11:01

kos


3 Answers

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.")
like image 73
ptomato Avatar answered Nov 16 '22 21:11

ptomato


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.")

How to solve

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()

Alternatively

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()

EDIT

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()
like image 40
Jacob Vlijm Avatar answered Nov 16 '22 22:11

Jacob Vlijm


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()
like image 1
J.J. Hakala Avatar answered Nov 16 '22 21:11

J.J. Hakala